From 614a93e1e85f34a3fddb4ce567ca50d182a6dfeb Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 11 May 2016 18:51:48 -0700 Subject: [PATCH] Plan api --- api/jobs.go | 88 +++++++++++++++++++++++++++++++++++ api/jobs_test.go | 70 ++++++++++++++++++++++++++++ command/agent/job_endpoint.go | 1 + nomad/job_endpoint.go | 1 + nomad/structs/structs.go | 2 +- 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/api/jobs.go b/api/jobs.go index 9ff4d8376..eb097090a 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "sort" "time" ) @@ -116,6 +117,24 @@ func (j *Jobs) PeriodicForce(jobID string, q *WriteOptions) (string, *WriteMeta, return resp.EvalID, wm, nil } +func (j *Jobs) Plan(job *Job, diff bool, q *WriteOptions) (*JobPlanResponse, *WriteMeta, error) { + if job == nil { + return nil, nil, fmt.Errorf("must pass non-nil job") + } + + var resp JobPlanResponse + req := &JobPlanRequest{ + Job: job, + Diff: diff, + } + wm, err := j.client.write("/v1/job/"+job.ID+"/plan", req, &resp, q) + if err != nil { + return nil, nil, err + } + + return &resp, wm, nil +} + // periodicForceResponse is used to deserialize a force response type periodicForceResponse struct { EvalID string @@ -256,3 +275,72 @@ type registerJobResponse struct { type deregisterJobResponse struct { EvalID string } + +type JobPlanRequest struct { + Job *Job + Diff bool +} + +type JobPlanResponse struct { + Cas uint64 + CreatedEvals []*Evaluation + Diff *JobDiff + SchedulerOutput *SchedulerOutput `json:"Plan"` +} + +type JobDiff struct { + Type string + ID string + Fields []*FieldDiff + Objects []*ObjectDiff + TaskGroups []*TaskGroupDiff +} + +type TaskGroupDiff struct { + Type string + Name string + Fields []*FieldDiff + Objects []*ObjectDiff + Tasks []*TaskDiff + Updates map[string]uint64 +} + +type TaskDiff struct { + Type string + Name string + Fields []*FieldDiff + Objects []*ObjectDiff + Annotations []string +} + +type FieldDiff struct { + Type string + Name string + Old, New string + Annotations []string +} + +type ObjectDiff struct { + Type string + Name string + Fields []*FieldDiff + Objects []*ObjectDiff +} + +type SchedulerOutput struct { + FailedAllocs []*Allocations + Annotations *PlanAnnotations +} + +type PlanAnnotations struct { + DesiredTGUpdates map[string]*DesiredUpdates +} + +type DesiredUpdates struct { + Ignore uint64 + Place uint64 + Migrate uint64 + Stop uint64 + InPlaceUpdate uint64 + DestructiveUpdate uint64 +} diff --git a/api/jobs_test.go b/api/jobs_test.go index 45e6325df..50616da6d 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -350,6 +350,76 @@ func TestJobs_PeriodicForce(t *testing.T) { t.Fatalf("evaluation %q missing", evalID) } +func TestJobs_Plan(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Create a job and attempt to register it + job := testJob() + eval, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if eval == "" { + t.Fatalf("missing eval id") + } + assertWriteMeta(t, wm) + + // Check that passing a nil job fails + if _, _, err := jobs.Plan(nil, true, nil); err == nil { + t.Fatalf("expect an error when job isn't provided") + } + + // Make a plan request + planResp, wm, err := jobs.Plan(job, true, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if planResp == nil { + t.Fatalf("nil response") + } + + if planResp.Cas == 0 { + t.Fatalf("bad Cas value: %#v", planResp) + } + if planResp.Diff == nil { + t.Fatalf("got nil diff: %#v", planResp) + } + if planResp.SchedulerOutput == nil { + t.Fatalf("got nil scheduler output: %#v", planResp) + } + // Can make this assertion because there are no clients. + if len(planResp.CreatedEvals) == 0 { + t.Fatalf("got no CreatedEvals: %#v", planResp) + } + + // Make a plan request w/o the diff + planResp, wm, err = jobs.Plan(job, false, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + if planResp == nil { + t.Fatalf("nil response") + } + + if planResp.Cas == 0 { + t.Fatalf("bad Cas value: %d", planResp.Cas) + } + if planResp.Diff != nil { + t.Fatalf("got non-nil diff: %#v", planResp) + } + if planResp.SchedulerOutput == nil { + t.Fatalf("got nil scheduler output: %#v", planResp) + } + // Can make this assertion because there are no clients. + if len(planResp.CreatedEvals) == 0 { + t.Fatalf("got no CreatedEvals: %#v", planResp) + } +} + func TestJobs_NewBatchJob(t *testing.T) { job := NewBatchJob("job1", "myjob", "region1", 5) expect := &Job{ diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 74a6e9fff..12e40e542 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -99,6 +99,7 @@ func (s *HTTPServer) jobPlan(resp http.ResponseWriter, req *http.Request, if err := s.agent.RPC("Job.Plan", &args, &out); err != nil { return nil, err } + setIndex(resp, out.Index) return out, nil } diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index 1e567a150..9c0aa15fc 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -474,6 +474,7 @@ func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) reply.Cas = index reply.Plan = planner.Plan reply.CreatedEvals = planner.CreatedEvals + reply.Index = index return nil } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 7355d03ee..86ab17012 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -416,7 +416,7 @@ type JobPlanResponse struct { // causes an in-place update or create/destroy Diff *JobDiff - QueryMeta + WriteMeta } // SingleAllocResponse is used to return a single allocation