diff --git a/api/jobs.go b/api/jobs.go index e45e67990..a00c3a169 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -106,6 +106,21 @@ func (j *Jobs) ForceEvaluate(jobID string, q *WriteOptions) (string, *WriteMeta, return resp.EvalID, wm, nil } +// PeriodicForce spawns a new instance of the periodic job and returns the eval ID +func (j *Jobs) PeriodicForce(jobID string, q *WriteOptions) (string, *WriteMeta, error) { + var resp periodicForceResponse + wm, err := j.client.write("/v1/job/"+jobID+"/periodic/force", nil, &resp, q) + if err != nil { + return "", nil, err + } + return resp.EvalID, wm, nil +} + +// periodicForceResponse is used to deserialize a force response +type periodicForceResponse struct { + EvalID string +} + // UpdateStrategy is for serializing update strategy for a job. type UpdateStrategy struct { Stagger time.Duration diff --git a/api/jobs_test.go b/api/jobs_test.go index 3ab49a185..cb998ac89 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -5,6 +5,8 @@ import ( "sort" "strings" "testing" + + "github.com/hashicorp/nomad/testutil" ) func TestJobs_Register(t *testing.T) { @@ -295,6 +297,58 @@ func TestJobs_ForceEvaluate(t *testing.T) { t.Fatalf("evaluation %q missing", evalID) } +func TestJobs_PeriodicForce(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Force-eval on a non-existent job fails + _, _, err := jobs.PeriodicForce("job1", nil) + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected not found error, got: %#v", err) + } + + // Create a new job + job := testPeriodicJob() + _, _, err = jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + testutil.WaitForResult(func() (bool, error) { + out, _, err := jobs.Info(job.ID, nil) + if err != nil || out == nil || out.ID != job.ID { + return false, err + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Try force again + evalID, wm, err := jobs.PeriodicForce(job.ID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + if evalID == "" { + t.Fatalf("empty evalID") + } + + // Retrieve the eval + evals := c.Evaluations() + eval, qm, err := evals.Info(evalID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + if eval.ID == evalID { + return + } + t.Fatalf("evaluation %q missing", evalID) +} + func TestJobs_NewBatchJob(t *testing.T) { job := NewBatchJob("job1", "myjob", "region1", 5) expect := &Job{ diff --git a/api/periodic.go b/api/periodic.go deleted file mode 100644 index c9c05fc40..000000000 --- a/api/periodic.go +++ /dev/null @@ -1,26 +0,0 @@ -package api - -// Periodic is used to access periodic job endpoints -type Periodic struct { - client *Client -} - -// Periodic returns a handle to access periodic job endpoints. -func (c *Client) PeriodicJobs() *Periodic { - return &Periodic{client: c} -} - -// Force spawns a new instance of the periodic job and returns the eval ID -func (p *Periodic) Force(jobID string, q *WriteOptions) (string, *WriteMeta, error) { - var resp periodicForceResponse - wm, err := p.client.write("/v1/periodic/"+jobID+"/force", nil, &resp, q) - if err != nil { - return "", nil, err - } - return resp.EvalID, wm, nil -} - -// periodicForceResponse is used to deserialize a force response -type periodicForceResponse struct { - EvalID string -} diff --git a/api/periodic_test.go b/api/periodic_test.go deleted file mode 100644 index f65d67719..000000000 --- a/api/periodic_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package api - -import ( - "strings" - "testing" - - "github.com/hashicorp/nomad/testutil" -) - -func TestPeriodicForce(t *testing.T) { - c, s := makeClient(t, nil, nil) - defer s.Stop() - jobs := c.Jobs() - periodic := c.PeriodicJobs() - - // Force-eval on a non-existent job fails - _, _, err := periodic.Force("job1", nil) - if err == nil || !strings.Contains(err.Error(), "not found") { - t.Fatalf("expected not found error, got: %#v", err) - } - - // Create a new job - job := testPeriodicJob() - _, _, err = jobs.Register(job, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - testutil.WaitForResult(func() (bool, error) { - out, _, err := jobs.Info(job.ID, nil) - if err != nil || out == nil || out.ID != job.ID { - return false, err - } - return true, nil - }, func(err error) { - t.Fatalf("err: %s", err) - }) - - // Try force again - evalID, wm, err := periodic.Force(job.ID, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - assertWriteMeta(t, wm) - - if evalID == "" { - t.Fatalf("empty evalID") - } - - // Retrieve the eval - evals := c.Evaluations() - eval, qm, err := evals.Info(evalID, nil) - if err != nil { - t.Fatalf("err: %s", err) - } - assertQueryMeta(t, qm) - if eval.ID == evalID { - return - } - t.Fatalf("evaluation %q missing", evalID) -} diff --git a/command/agent/http.go b/command/agent/http.go index e1ab90a22..4c46ee8b4 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -114,8 +114,6 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/status/leader", s.wrap(s.StatusLeaderRequest)) s.mux.HandleFunc("/v1/status/peers", s.wrap(s.StatusPeersRequest)) - s.mux.HandleFunc("/v1/periodic/", s.wrap(s.PeriodicSpecificReqeust)) - if enableDebug { s.mux.HandleFunc("/debug/pprof/", pprof.Index) s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 67672d26b..d66954a3c 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -48,6 +48,9 @@ func (s *HTTPServer) JobSpecificRequest(resp http.ResponseWriter, req *http.Requ case strings.HasSuffix(path, "/evaluations"): jobName := strings.TrimSuffix(path, "/evaluations") return s.jobEvaluations(resp, req, jobName) + case strings.HasSuffix(path, "/periodic/force"): + jobName := strings.TrimSuffix(path, "/periodic/force") + return s.periodicForceRequest(resp, req, jobName) default: return s.jobCRUD(resp, req, path) } @@ -71,6 +74,25 @@ func (s *HTTPServer) jobForceEvaluate(resp http.ResponseWriter, req *http.Reques return out, nil } +func (s *HTTPServer) periodicForceRequest(resp http.ResponseWriter, req *http.Request, + jobName string) (interface{}, error) { + if req.Method != "PUT" && req.Method != "POST" { + return nil, CodedError(405, ErrInvalidMethod) + } + + args := structs.PeriodicForceRequest{ + JobID: jobName, + } + s.parseRegion(req, &args.Region) + + var out structs.PeriodicForceResponse + if err := s.agent.RPC("Periodic.Force", &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.Index) + return out, nil +} + func (s *HTTPServer) jobAllocations(resp http.ResponseWriter, req *http.Request, jobName string) (interface{}, error) { if req.Method != "GET" { diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index cdb1bc88f..62643ac51 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -443,3 +443,42 @@ func TestHTTP_JobAllocations(t *testing.T) { } }) } + +func TestHTTP_PeriodicForce(t *testing.T) { + httpTest(t, nil, func(s *TestServer) { + // Create and register a periodic job. + job := mock.PeriodicJob() + args := structs.JobRegisterRequest{ + Job: job, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.JobRegisterResponse + if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"periodic/force", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.JobSpecificRequest(respW, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for the index + if respW.HeaderMap.Get("X-Nomad-Index") == "" { + t.Fatalf("missing index") + } + + // Check the response + r := obj.(structs.PeriodicForceResponse) + if r.EvalID == "" { + t.Fatalf("bad: %#v", r) + } + }) +} diff --git a/command/agent/periodic_endpoint.go b/command/agent/periodic_endpoint.go deleted file mode 100644 index eec622127..000000000 --- a/command/agent/periodic_endpoint.go +++ /dev/null @@ -1,40 +0,0 @@ -package agent - -import ( - "net/http" - "strings" - - "github.com/hashicorp/nomad/nomad/structs" -) - -func (s *HTTPServer) PeriodicSpecificReqeust(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - path := strings.TrimPrefix(req.URL.Path, "/v1/periodic/") - switch { - case strings.HasSuffix(path, "/force"): - jobName := strings.TrimSuffix(path, "/force") - return s.periodicForceRequest(resp, req, jobName) - default: - return nil, CodedError(405, ErrInvalidMethod) - - } - -} - -func (s *HTTPServer) periodicForceRequest(resp http.ResponseWriter, req *http.Request, - jobName string) (interface{}, error) { - if req.Method != "PUT" && req.Method != "POST" { - return nil, CodedError(405, ErrInvalidMethod) - } - - args := structs.PeriodicForceRequest{ - JobID: jobName, - } - s.parseRegion(req, &args.Region) - - var out structs.PeriodicForceResponse - if err := s.agent.RPC("Periodic.Force", &args, &out); err != nil { - return nil, err - } - setIndex(resp, out.Index) - return out, nil -} diff --git a/command/agent/periodic_endpoint_test.go b/command/agent/periodic_endpoint_test.go deleted file mode 100644 index 500034cdf..000000000 --- a/command/agent/periodic_endpoint_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package agent - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/hashicorp/nomad/nomad/mock" - "github.com/hashicorp/nomad/nomad/structs" -) - -func TestHTTP_PeriodicForce(t *testing.T) { - httpTest(t, nil, func(s *TestServer) { - // Create and register a periodic job. - job := mock.PeriodicJob() - args := structs.JobRegisterRequest{ - Job: job, - WriteRequest: structs.WriteRequest{Region: "global"}, - } - var resp structs.JobRegisterResponse - if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { - t.Fatalf("err: %v", err) - } - - // Make the HTTP request - req, err := http.NewRequest("POST", "/v1/periodic/"+job.ID+"/force", nil) - if err != nil { - t.Fatalf("err: %v", err) - } - respW := httptest.NewRecorder() - - // Make the request - obj, err := s.Server.PeriodicSpecificReqeust(respW, req) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Check for the index - if respW.HeaderMap.Get("X-Nomad-Index") == "" { - t.Fatalf("missing index") - } - - // Check the response - r := obj.(structs.PeriodicForceResponse) - if r.EvalID == "" { - t.Fatalf("bad: %#v", r) - } - }) -} diff --git a/website/source/docs/http/job.html.md b/website/source/docs/http/job.html.md index 05484aec9..867389bcc 100644 --- a/website/source/docs/http/job.html.md +++ b/website/source/docs/http/job.html.md @@ -313,6 +313,39 @@ region is used; another region can be specified using the `?region=` query param +