From 76c7584eafb7736be0a75318a6abf4dad1d14249 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 11:24:28 -0700 Subject: [PATCH 01/22] scripts: fix build warning --- scripts/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.sh b/scripts/build.sh index 109b792de..015187610 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -46,7 +46,7 @@ go get \ # Build! echo "--> Building..." go build \ - -ldflags "${CGO_LDFLAGS} -X main.GitCommit ${GIT_COMMIT}${GIT_DIRTY} -X main.GitDescribe ${GIT_DESCRIBE}" \ + -ldflags "${CGO_LDFLAGS} -X main.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X main.GitDescribe=${GIT_DESCRIBE}" \ -v \ -o bin/nomad${EXTENSION} cp bin/nomad${EXTENSION} ${GOPATHSINGLE}/bin From f5c57cedfa273140aa577c798aa291458a1f04f6 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 11:41:03 -0700 Subject: [PATCH 02/22] api: add agent self call --- api/agent.go | 32 ++++++++++++++++++++++++++++++++ api/agent_test.go | 24 ++++++++++++++++++++++++ api/api_test.go | 2 +- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 api/agent.go create mode 100644 api/agent_test.go diff --git a/api/agent.go b/api/agent.go new file mode 100644 index 000000000..aeec83e21 --- /dev/null +++ b/api/agent.go @@ -0,0 +1,32 @@ +package api + +import ( + "fmt" +) + +// Agent encapsulates an API client which talks to Nomad's +// agent endpoints for a specific node. +type Agent struct { + client *Client + node string +} + +// Agent returns a new agent which can be used to query +// the agent-specific endpoints. +func (c *Client) Agent() *Agent { + return &Agent{client: c} +} + +// Self is used to query the /v1/agent/self endpoint and +// returns information specific to the running agent. +func (a *Agent) Self() (map[string]map[string]interface{}, error) { + var out map[string]map[string]interface{} + + // Query the self endpoint on the agent + _, err := a.client.query("/v1/agent/self", &out, nil) + if err != nil { + return nil, fmt.Errorf("failed querying self endpoint: %s", err) + } + + return out, nil +} diff --git a/api/agent_test.go b/api/agent_test.go new file mode 100644 index 000000000..fd038970d --- /dev/null +++ b/api/agent_test.go @@ -0,0 +1,24 @@ +package api + +import ( + "testing" +) + +func TestAgent_Self(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + + // Get a handle on the Agent endpoints + a := c.Agent() + + // Query the endpoint + res, err := a.Self() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check that we got a valid response + if name, ok := res["member"]["Name"]; !ok || name == "" { + t.Fatalf("bad member name in response: %#v", res) + } +} diff --git a/api/api_test.go b/api/api_test.go index 466d6e011..49d277553 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -22,7 +22,7 @@ func makeClient(t *testing.T, cb1 configCallback, // Create server server := testutil.NewTestServer(t, cb2) - conf.URL = server.HTTPAddr + conf.URL = "http://" + server.HTTPAddr // Create client client, err := NewClient(conf) From c1f4faaab375743f94ccc2ff416ed0df46265943 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 11:51:20 -0700 Subject: [PATCH 03/22] api: allow querying node name from agent --- api/agent.go | 22 +++++++++++++++++++++- api/agent_test.go | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/api/agent.go b/api/agent.go index aeec83e21..e80523c42 100644 --- a/api/agent.go +++ b/api/agent.go @@ -8,7 +8,9 @@ import ( // agent endpoints for a specific node. type Agent struct { client *Client - node string + + // Cache static agent info + nodeName string } // Agent returns a new agent which can be used to query @@ -30,3 +32,21 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return out, nil } + +// NodeName is used to query the Nomad agent for its node name. +func (a *Agent) NodeName() (string, error) { + // Return from cache if we have it + if a.nodeName != "" { + return a.nodeName, nil + } + + // Query the node name + info, err := a.Self() + if err != nil { + return "", err + } + if name, ok := info["member"]["Name"]; ok { + a.nodeName = name.(string) + } + return a.nodeName, nil +} diff --git a/api/agent_test.go b/api/agent_test.go index fd038970d..177db0e87 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -22,3 +22,25 @@ func TestAgent_Self(t *testing.T) { t.Fatalf("bad member name in response: %#v", res) } } + +func TestAgent_NodeName(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Agent() + + // Query the agent for the node name + res, err := a.NodeName() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure we got a node name back + if res == "" { + t.Fatalf("expected node name, got nothing") + } + + // Check that we cached the node name + if a.nodeName == "" { + t.Fatalf("should have cached node name") + } +} From 500daf5d7f88914a4ae7e1364b4999276efbc5fc Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 12:01:54 -0700 Subject: [PATCH 04/22] api: datacenter is cached and queryable --- api/agent.go | 26 ++++++++++++++++++++++---- api/agent_test.go | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/api/agent.go b/api/agent.go index e80523c42..95cecbfa4 100644 --- a/api/agent.go +++ b/api/agent.go @@ -10,7 +10,8 @@ type Agent struct { client *Client // Cache static agent info - nodeName string + nodeName string + datacenter string } // Agent returns a new agent which can be used to query @@ -45,8 +46,25 @@ func (a *Agent) NodeName() (string, error) { if err != nil { return "", err } - if name, ok := info["member"]["Name"]; ok { - a.nodeName = name.(string) - } + a.nodeName, _ = info["member"]["Name"].(string) return a.nodeName, nil } + +// Datacenter is used to return the name of the datacenter which +// the agent is a member of. +func (a *Agent) Datacenter() (string, error) { + // Return from cache if we have it + if a.datacenter != "" { + return a.datacenter, nil + } + + // Query the agent for the DC + info, err := a.Self() + if err != nil { + return "", err + } + if tags, ok := info["member"]["Tags"].(map[string]interface{}); ok { + a.datacenter, _ = tags["dc"].(string) + } + return a.datacenter, nil +} diff --git a/api/agent_test.go b/api/agent_test.go index 177db0e87..8d74b7a2e 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -44,3 +44,25 @@ func TestAgent_NodeName(t *testing.T) { t.Fatalf("should have cached node name") } } + +func TestAgent_Datacenter(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Agent() + + // Query the agent for the datacenter + dc, err := a.Datacenter() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check that we got the DC name back + if dc != "dc1" { + t.Fatalf("expected dc1, got: %q", dc) + } + + // Check that the datacenter name was cached + if a.datacenter == "" { + t.Fatalf("should have cached datacenter") + } +} From 5a8bc52c565cd1f0dc274e508ca85630c7ff9abd Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 12:13:39 -0700 Subject: [PATCH 05/22] api: region is queryable, optimize agent caching --- api/agent.go | 51 ++++++++++++++++++++++++++++++++++------------- api/agent_test.go | 19 +++++------------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/api/agent.go b/api/agent.go index 95cecbfa4..e43009f21 100644 --- a/api/agent.go +++ b/api/agent.go @@ -12,6 +12,7 @@ type Agent struct { // Cache static agent info nodeName string datacenter string + region string } // Agent returns a new agent which can be used to query @@ -31,9 +32,29 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return nil, fmt.Errorf("failed querying self endpoint: %s", err) } + // Populate the cache for faster queries + a.populateCache(out) + return out, nil } +// populateCache is used to insert various pieces of static +// data into the agent handle. This is used during subsequent +// lookups for the same data later on to save the round trip. +func (a *Agent) populateCache(info map[string]map[string]interface{}) { + if a.nodeName == "" { + a.nodeName, _ = info["member"]["Name"].(string) + } + if tags, ok := info["member"]["Tags"].(map[string]interface{}); ok { + if a.datacenter == "" { + a.datacenter, _ = tags["dc"].(string) + } + if a.region == "" { + a.region, _ = tags["region"].(string) + } + } +} + // NodeName is used to query the Nomad agent for its node name. func (a *Agent) NodeName() (string, error) { // Return from cache if we have it @@ -42,12 +63,8 @@ func (a *Agent) NodeName() (string, error) { } // Query the node name - info, err := a.Self() - if err != nil { - return "", err - } - a.nodeName, _ = info["member"]["Name"].(string) - return a.nodeName, nil + _, err := a.Self() + return a.nodeName, err } // Datacenter is used to return the name of the datacenter which @@ -59,12 +76,18 @@ func (a *Agent) Datacenter() (string, error) { } // Query the agent for the DC - info, err := a.Self() - if err != nil { - return "", err - } - if tags, ok := info["member"]["Tags"].(map[string]interface{}); ok { - a.datacenter, _ = tags["dc"].(string) - } - return a.datacenter, nil + _, err := a.Self() + return a.datacenter, err +} + +// Region is used to look up the region the agent is in. +func (a *Agent) Region() (string, error) { + // Return from cache if we have it + if a.region != "" { + return a.region, nil + } + + // Query the agent for the region + _, err := a.Self() + return a.region, err } diff --git a/api/agent_test.go b/api/agent_test.go index 8d74b7a2e..a3bfd630a 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -21,6 +21,11 @@ func TestAgent_Self(t *testing.T) { if name, ok := res["member"]["Name"]; !ok || name == "" { t.Fatalf("bad member name in response: %#v", res) } + + // Local cache was populated + if a.nodeName == "" || a.datacenter == "" || a.region == "" { + t.Fatalf("cache should be populated, got: %#v", a) + } } func TestAgent_NodeName(t *testing.T) { @@ -33,16 +38,9 @@ func TestAgent_NodeName(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - - // Ensure we got a node name back if res == "" { t.Fatalf("expected node name, got nothing") } - - // Check that we cached the node name - if a.nodeName == "" { - t.Fatalf("should have cached node name") - } } func TestAgent_Datacenter(t *testing.T) { @@ -55,14 +53,7 @@ func TestAgent_Datacenter(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - - // Check that we got the DC name back if dc != "dc1" { t.Fatalf("expected dc1, got: %q", dc) } - - // Check that the datacenter name was cached - if a.datacenter == "" { - t.Fatalf("should have cached datacenter") - } } From 9bd115e0fd9175a0b6684a8167b2865419698954 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 14:26:26 -0700 Subject: [PATCH 06/22] api: allow query parameters in query/write/delete --- api/api.go | 11 ++++++++++- api/api_test.go | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 48e4bdf5b..e1bb8e520 100644 --- a/api/api.go +++ b/api/api.go @@ -195,13 +195,14 @@ func (r *request) toHTTP() (*http.Request, error) { // newRequest is used to create a new request func (c *Client) newRequest(method, path string) *request { base, _ := url.Parse(c.config.URL) + u, _ := url.Parse(path) r := &request{ config: &c.config, method: method, url: &url.URL{ Scheme: base.Scheme, Host: base.Host, - Path: path, + Path: u.Path, }, params: make(map[string][]string), } @@ -211,6 +212,14 @@ func (c *Client) newRequest(method, path string) *request { if c.config.WaitTime != 0 { r.params.Set("wait", durToMsec(r.config.WaitTime)) } + + // Add in the query parameters, if any + for key, values := range u.Query() { + for _, value := range values { + r.params.Add(key, value) + } + } + return r } diff --git a/api/api_test.go b/api/api_test.go index 49d277553..e9114cf31 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -155,3 +155,22 @@ func TestParseWriteMeta(t *testing.T) { t.Fatalf("Bad: %v", wm) } } + +func TestQueryString(t *testing.T) { + // TODO t.Parallel() + c, s := makeClient(t, nil, nil) + defer s.Stop() + + r := c.newRequest("PUT", "/v1/abc?foo=bar&baz=zip") + q := &WriteOptions{Region: "foo"} + r.setWriteOptions(q) + + req, err := r.toHTTP() + if err != nil { + t.Fatalf("err: %s", err) + } + + if uri := req.URL.RequestURI(); uri != "/v1/abc?baz=zip&foo=bar®ion=foo" { + t.Fatalf("bad uri: %q", uri) + } +} From 801db291eda4e6e116b39fc4437b7a5d55ce0606 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 14:26:53 -0700 Subject: [PATCH 07/22] api: agent join api works --- api/agent.go | 31 +++++++++++++++++++++++++++++++ api/agent_test.go | 19 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/api/agent.go b/api/agent.go index e43009f21..10248d9b6 100644 --- a/api/agent.go +++ b/api/agent.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "net/url" ) // Agent encapsulates an API client which talks to Nomad's @@ -91,3 +92,33 @@ func (a *Agent) Region() (string, error) { _, err := a.Self() return a.region, err } + +// Join is used to instruct a server node to join another server +// via the gossip protocol. Multiple addresses may be specified. +// We attempt to join all of the hosts in the list. If one or +// more nodes have a successful result, no error is returned. +func (a *Agent) Join(addrs ...string) error { + // Accumulate the addresses + v := url.Values{} + for _, addr := range addrs { + v.Add("address", addr) + } + + // Send the join request + var resp joinResponse + _, err := a.client.write("/v1/agent/join?"+v.Encode(), nil, &resp, nil) + if err != nil { + return fmt.Errorf("failed joining: %s", err) + } + if resp.Error != "" { + return fmt.Errorf("failed joining: %s", resp.Error) + } + return nil +} + +// joinResponse is used to decode the response we get while +// sending a member join request. +type joinResponse struct { + NumNodes int `json:"num_nodes"` + Error string `json:"error"` +} diff --git a/api/agent_test.go b/api/agent_test.go index a3bfd630a..54de793ad 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -57,3 +57,22 @@ func TestAgent_Datacenter(t *testing.T) { t.Fatalf("expected dc1, got: %q", dc) } } + +func TestAgent_Join(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Agent() + + // Attempting to join a non-existent host returns error + if err := a.Join("nope"); err == nil { + t.Fatalf("expected error, got nothing") + } + + // TODO(ryanuber): This is pretty much a worthless test, + // since we are just joining ourselves. Once the agent + // respects config options, change this to actually make + // two nodes and join them. + if err := a.Join("127.0.0.1"); err != nil { + t.Fatalf("err: %s", err) + } +} From 40a5696fb29aba2af8e72d6aa92fa9a216452c70 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 14:47:29 -0700 Subject: [PATCH 08/22] api: finish agent endpoint --- api/agent.go | 33 +++++++++++++++++++++++++++++++++ api/agent_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/api/agent.go b/api/agent.go index 10248d9b6..ae2eb1a61 100644 --- a/api/agent.go +++ b/api/agent.go @@ -116,9 +116,42 @@ func (a *Agent) Join(addrs ...string) error { return nil } +// Members is used to query all of the known server members +func (a *Agent) Members() ([]*AgentMember, error) { + var resp []*AgentMember + + // Query the known members + _, err := a.client.query("/v1/agent/members", &resp, nil) + if err != nil { + return nil, err + } + return resp, nil +} + +// ForceLeave is used to eject an existing node from the cluster. +func (a *Agent) ForceLeave(node string) error { + _, err := a.client.write("/v1/agent/force-leave?node="+node, nil, nil, nil) + return err +} + // joinResponse is used to decode the response we get while // sending a member join request. type joinResponse struct { NumNodes int `json:"num_nodes"` Error string `json:"error"` } + +// AgentMember represents a cluster member known to the agent +type AgentMember struct { + Name string + Addr string + Port uint16 + Tags map[string]string + Status int + ProtocolMin uint8 + ProtocolMax uint8 + ProtocolCur uint8 + DelegateMin uint8 + DelegateMax uint8 + DelegateCur uint8 +} diff --git a/api/agent_test.go b/api/agent_test.go index 54de793ad..7eb891ea1 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -76,3 +76,36 @@ func TestAgent_Join(t *testing.T) { t.Fatalf("err: %s", err) } } + +func TestAgent_Members(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Agent() + + // Query nomad for all the known members + mem, err := a.Members() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check that we got the expected result + if n := len(mem); n != 1 { + t.Fatalf("expected 1 member, got: %d", n) + } + if m := mem[0]; m.Name == "" || m.Addr == "" || m.Port == 0 { + t.Fatalf("bad member: %#v", m) + } +} + +func TestAgent_ForceLeave(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Agent() + + // Force-leave on a non-existent node does not error + if err := a.ForceLeave("nope"); err != nil { + t.Fatalf("err: %s", err) + } + + // TODO: test force-leave on an existing node +} From 4ff1753466a8857b06ba1adb21c4476f1e0b0fe1 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 15:37:07 -0700 Subject: [PATCH 09/22] api: starting on allocs --- api/allocs.go | 37 +++++++++++++++++++++++++++++++++++++ api/allocs_test.go | 20 ++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 api/allocs.go create mode 100644 api/allocs_test.go diff --git a/api/allocs.go b/api/allocs.go new file mode 100644 index 000000000..7928b92bd --- /dev/null +++ b/api/allocs.go @@ -0,0 +1,37 @@ +package api + +// Allocs is used to query the alloc-related endpoints. +type Allocs struct { + client *Client +} + +// Allocs returns a handle on the allocs endpoints. +func (c *Client) Allocs() *Allocs { + return &Allocs{client: c} +} + +// List returns a list of all of the allocations. +func (a *Allocs) List() ([]*Alloc, error) { + var resp []*Alloc + _, err := a.client.query("/v1/allocations", &resp, nil) + if err != nil { + return nil, err + } + return resp, nil +} + +// Alloc is used for serialization of allocations. +type Alloc struct { + ID string + EvalID string + Name string + NodeID string + JobID string + TaskGroup string + DesiredStatus string + DesiredDescription string + ClientStatus string + ClientDescription string + CreateIndex uint64 + ModifyIndex uint64 +} diff --git a/api/allocs_test.go b/api/allocs_test.go new file mode 100644 index 000000000..95561042f --- /dev/null +++ b/api/allocs_test.go @@ -0,0 +1,20 @@ +package api + +import ( + "testing" +) + +func TestAllocs_List(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + a := c.Allocs() + + // Querying when no allocs exist returns nothing + allocs, err := a.List() + if err != nil { + t.Fatalf("err: %s", err) + } + if n := len(allocs); n != 0 { + t.Fatalf("expected 0 allocs, got: %d", n) + } +} From 0f31b16fae10209fd68215841a1d62d1de766df3 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 16:24:26 -0700 Subject: [PATCH 10/22] api: starting on jobs --- api/job.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ api/job_test.go | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 api/job.go create mode 100644 api/job_test.go diff --git a/api/job.go b/api/job.go new file mode 100644 index 000000000..beec9910b --- /dev/null +++ b/api/job.go @@ -0,0 +1,57 @@ +package api + +// Jobs is used to access the job-specific endpoints. +type Jobs struct { + client *Client +} + +// Jobs returns a handle on the jobs endpoints. +func (c *Client) Jobs() *Jobs { + return &Jobs{client: c} +} + +// Register is used to register a new job. It returns the ID +// of the evaluation, along with any errors encountered. +func (j *Jobs) Register(job *Job, q *WriteOptions) (string, *WriteMeta, error) { + var resp registerJobResponse + + req := ®isterJobRequest{job} + wm, err := j.client.write("/v1/jobs", req, &resp, q) + if err != nil { + return "", nil, err + } + return resp.EvalID, wm, nil +} + +// List is used to list all of the existing jobs. +func (j *Jobs) List() ([]*Job, error) { + var resp []*Job + _, err := j.client.query("/v1/jobs", &resp, nil) + if err != nil { + return nil, err + } + return resp, nil +} + +// Job is used to serialize a job. +type Job struct { + ID string + Name string + Type string + Priority int + AllAtOnce bool + Datacenters []string + Meta map[string]string + Status string + StatusDescription string +} + +// registerJobRequest is used to serialize a job registration +type registerJobRequest struct { + Job *Job +} + +// registerJobResponse is used to deserialize a job response +type registerJobResponse struct { + EvalID string +} diff --git a/api/job_test.go b/api/job_test.go new file mode 100644 index 000000000..bf9c38d40 --- /dev/null +++ b/api/job_test.go @@ -0,0 +1,48 @@ +package api + +import ( + "reflect" + "testing" +) + +func TestJobs_Register(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Listing jobs before registering returns nothing + resp, err := jobs.List() + if err != nil { + t.Fatalf("err: %s", err) + } + if n := len(resp); n != 0 { + t.Fatalf("expected 0 jobs, got: %d", n) + } + + // Create a job and attempt to register it + job := &Job{ + ID: "job1", + Name: "Job #1", + Type: "service", + Priority: 1, + } + eval, _, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if eval == "" { + t.Fatalf("missing eval id") + } + + // Query the jobs back out again + resp, err = jobs.List() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Check that we got the expected response + expect := []*Job{job} + if !reflect.DeepEqual(resp, expect) { + t.Fatalf("bad: %#v", resp[0]) + } +} From d47772f8a66567900548fe5c7d804baf7df1825a Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 16:45:16 -0700 Subject: [PATCH 11/22] api: return query meta/write meta --- api/allocs.go | 10 ++++------ api/allocs_test.go | 5 ++++- api/job.go | 8 ++++---- api/job_test.go | 15 ++++++++++++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/api/allocs.go b/api/allocs.go index 7928b92bd..02ebfb9e4 100644 --- a/api/allocs.go +++ b/api/allocs.go @@ -11,13 +11,13 @@ func (c *Client) Allocs() *Allocs { } // List returns a list of all of the allocations. -func (a *Allocs) List() ([]*Alloc, error) { +func (a *Allocs) List() ([]*Alloc, *QueryMeta, error) { var resp []*Alloc - _, err := a.client.query("/v1/allocations", &resp, nil) + qm, err := a.client.query("/v1/allocations", &resp, nil) if err != nil { - return nil, err + return nil, nil, err } - return resp, nil + return resp, qm, nil } // Alloc is used for serialization of allocations. @@ -32,6 +32,4 @@ type Alloc struct { DesiredDescription string ClientStatus string ClientDescription string - CreateIndex uint64 - ModifyIndex uint64 } diff --git a/api/allocs_test.go b/api/allocs_test.go index 95561042f..6063dfd1a 100644 --- a/api/allocs_test.go +++ b/api/allocs_test.go @@ -10,10 +10,13 @@ func TestAllocs_List(t *testing.T) { a := c.Allocs() // Querying when no allocs exist returns nothing - allocs, err := a.List() + allocs, qm, err := a.List() if err != nil { t.Fatalf("err: %s", err) } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } if n := len(allocs); n != 0 { t.Fatalf("expected 0 allocs, got: %d", n) } diff --git a/api/job.go b/api/job.go index beec9910b..01ecbb908 100644 --- a/api/job.go +++ b/api/job.go @@ -24,13 +24,13 @@ func (j *Jobs) Register(job *Job, q *WriteOptions) (string, *WriteMeta, error) { } // List is used to list all of the existing jobs. -func (j *Jobs) List() ([]*Job, error) { +func (j *Jobs) List() ([]*Job, *QueryMeta, error) { var resp []*Job - _, err := j.client.query("/v1/jobs", &resp, nil) + qm, err := j.client.query("/v1/jobs", &resp, nil) if err != nil { - return nil, err + return nil, qm, err } - return resp, nil + return resp, qm, nil } // Job is used to serialize a job. diff --git a/api/job_test.go b/api/job_test.go index bf9c38d40..832ad56ed 100644 --- a/api/job_test.go +++ b/api/job_test.go @@ -11,10 +11,13 @@ func TestJobs_Register(t *testing.T) { jobs := c.Jobs() // Listing jobs before registering returns nothing - resp, err := jobs.List() + resp, qm, err := jobs.List() if err != nil { t.Fatalf("err: %s", err) } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } if n := len(resp); n != 0 { t.Fatalf("expected 0 jobs, got: %d", n) } @@ -26,19 +29,25 @@ func TestJobs_Register(t *testing.T) { Type: "service", Priority: 1, } - eval, _, err := jobs.Register(job, nil) + eval, wm, err := jobs.Register(job, nil) if err != nil { t.Fatalf("err: %s", err) } if eval == "" { t.Fatalf("missing eval id") } + if wm.LastIndex == 0 { + t.Fatalf("bad index: %d", wm.LastIndex) + } // Query the jobs back out again - resp, err = jobs.List() + resp, qm, err = jobs.List() if err != nil { t.Fatalf("err: %s", err) } + if qm.LastIndex == 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } // Check that we got the expected response expect := []*Job{job} From 4196ab8fb14645d49cabab402decc62d568e8331 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 17:20:52 -0700 Subject: [PATCH 12/22] api: allow retrieving jobs by ID --- api/allocs_test.go | 28 +++++++++++++++++++++++ api/{job.go => jobs.go} | 11 +++++++++ api/{job_test.go => jobs_test.go} | 37 +++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) rename api/{job.go => jobs.go} (82%) rename api/{job_test.go => jobs_test.go} (56%) diff --git a/api/allocs_test.go b/api/allocs_test.go index 6063dfd1a..59e1b4baa 100644 --- a/api/allocs_test.go +++ b/api/allocs_test.go @@ -20,4 +20,32 @@ func TestAllocs_List(t *testing.T) { if n := len(allocs); n != 0 { t.Fatalf("expected 0 allocs, got: %d", n) } + + // TODO: do something that causes an allocation to actually happen + // so we can query for them. + return + + job := &Job{ + ID: "job1", + Name: "Job #1", + Type: "service", + } + eval, _, err := c.Jobs().Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // List the allocations again + allocs, qm, err = a.List() + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex == 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + + // Check that we got the allocation back + if len(allocs) == 0 || allocs[0].EvalID != eval { + t.Fatalf("bad: %#v", allocs) + } } diff --git a/api/job.go b/api/jobs.go similarity index 82% rename from api/job.go rename to api/jobs.go index 01ecbb908..9004b419f 100644 --- a/api/job.go +++ b/api/jobs.go @@ -33,6 +33,17 @@ func (j *Jobs) List() ([]*Job, *QueryMeta, error) { return resp, qm, nil } +// GetByID is used to retrieve information about a particular +// job given its unique ID. +func (j *Jobs) GetByID(id string) (*Job, *QueryMeta, error) { + var resp Job + qm, err := j.client.query("/v1/job/"+id, &resp, nil) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} + // Job is used to serialize a job. type Job struct { ID string diff --git a/api/job_test.go b/api/jobs_test.go similarity index 56% rename from api/job_test.go rename to api/jobs_test.go index 832ad56ed..ae6834c37 100644 --- a/api/job_test.go +++ b/api/jobs_test.go @@ -2,6 +2,7 @@ package api import ( "reflect" + "strings" "testing" ) @@ -55,3 +56,39 @@ func TestJobs_Register(t *testing.T) { t.Fatalf("bad: %#v", resp[0]) } } + +func TestJobs_GetByID(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Trying to retrieve a job by ID before it exists + // returns an error + _, _, err := jobs.GetByID("job1") + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected not found error, got: %#v", err) + } + + // Register the job + job := &Job{ + ID: "job1", + Name: "Job #1", + Type: "service", + Priority: 1, + } + if _, _, err := jobs.Register(job, nil); err != nil { + t.Fatalf("err: %s", err) + } + + // Query the job again and ensure it exists + result, qm, err := jobs.GetByID("job1") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex == 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if !reflect.DeepEqual(result, job) { + t.Fatalf("expect: %#v, got: %#v", job, result) + } +} From 05cd0b7990902b9354b4d723fc2a60ee3bd756a3 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 17:49:31 -0700 Subject: [PATCH 13/22] api: look up allocations by job ID --- api/{allocs.go => allocations.go} | 18 +++++++------- api/{allocs_test.go => allocations_test.go} | 4 +-- api/jobs.go | 16 +++++++++--- api/jobs_test.go | 27 ++++++++++++++++++--- 4 files changed, 48 insertions(+), 17 deletions(-) rename api/{allocs.go => allocations.go} (55%) rename api/{allocs_test.go => allocations_test.go} (93%) diff --git a/api/allocs.go b/api/allocations.go similarity index 55% rename from api/allocs.go rename to api/allocations.go index 02ebfb9e4..c67400bad 100644 --- a/api/allocs.go +++ b/api/allocations.go @@ -1,18 +1,18 @@ package api -// Allocs is used to query the alloc-related endpoints. -type Allocs struct { +// Allocations is used to query the alloc-related endpoints. +type Allocations struct { client *Client } -// Allocs returns a handle on the allocs endpoints. -func (c *Client) Allocs() *Allocs { - return &Allocs{client: c} +// Allocations returns a handle on the allocs endpoints. +func (c *Client) Allocations() *Allocations { + return &Allocations{client: c} } // List returns a list of all of the allocations. -func (a *Allocs) List() ([]*Alloc, *QueryMeta, error) { - var resp []*Alloc +func (a *Allocations) List() ([]*Allocation, *QueryMeta, error) { + var resp []*Allocation qm, err := a.client.query("/v1/allocations", &resp, nil) if err != nil { return nil, nil, err @@ -20,8 +20,8 @@ func (a *Allocs) List() ([]*Alloc, *QueryMeta, error) { return resp, qm, nil } -// Alloc is used for serialization of allocations. -type Alloc struct { +// Allocation is used for serialization of allocations. +type Allocation struct { ID string EvalID string Name string diff --git a/api/allocs_test.go b/api/allocations_test.go similarity index 93% rename from api/allocs_test.go rename to api/allocations_test.go index 59e1b4baa..d3189ec38 100644 --- a/api/allocs_test.go +++ b/api/allocations_test.go @@ -4,10 +4,10 @@ import ( "testing" ) -func TestAllocs_List(t *testing.T) { +func TestAllocations_List(t *testing.T) { c, s := makeClient(t, nil, nil) defer s.Stop() - a := c.Allocs() + a := c.Allocations() // Querying when no allocs exist returns nothing allocs, qm, err := a.List() diff --git a/api/jobs.go b/api/jobs.go index 9004b419f..76e64a73b 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -33,17 +33,27 @@ func (j *Jobs) List() ([]*Job, *QueryMeta, error) { return resp, qm, nil } -// GetByID is used to retrieve information about a particular +// Info is used to retrieve information about a particular // job given its unique ID. -func (j *Jobs) GetByID(id string) (*Job, *QueryMeta, error) { +func (j *Jobs) Info(jobID string) (*Job, *QueryMeta, error) { var resp Job - qm, err := j.client.query("/v1/job/"+id, &resp, nil) + qm, err := j.client.query("/v1/job/"+jobID, &resp, nil) if err != nil { return nil, nil, err } return &resp, qm, nil } +// Allocations is used to return the allocs for a given job ID. +func (j *Jobs) Allocations(jobID string) ([]*Allocation, *QueryMeta, error) { + var resp []*Allocation + qm, err := j.client.query("/v1/job/"+jobID+"/allocations", &resp, nil) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + // Job is used to serialize a job. type Job struct { ID string diff --git a/api/jobs_test.go b/api/jobs_test.go index ae6834c37..a4220a888 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -57,14 +57,14 @@ func TestJobs_Register(t *testing.T) { } } -func TestJobs_GetByID(t *testing.T) { +func TestJobs_Info(t *testing.T) { c, s := makeClient(t, nil, nil) defer s.Stop() jobs := c.Jobs() // Trying to retrieve a job by ID before it exists // returns an error - _, _, err := jobs.GetByID("job1") + _, _, err := jobs.Info("job1") if err == nil || !strings.Contains(err.Error(), "not found") { t.Fatalf("expected not found error, got: %#v", err) } @@ -81,7 +81,7 @@ func TestJobs_GetByID(t *testing.T) { } // Query the job again and ensure it exists - result, qm, err := jobs.GetByID("job1") + result, qm, err := jobs.Info("job1") if err != nil { t.Fatalf("err: %s", err) } @@ -92,3 +92,24 @@ func TestJobs_GetByID(t *testing.T) { t.Fatalf("expect: %#v, got: %#v", job, result) } } + +func TestJobs_Allocations(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Looking up by a non-existent job returns nothing + allocs, qm, err := jobs.Allocations("job1") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("expected 0, got: %d", qm.LastIndex) + } + if n := len(allocs); n != 0 { + t.Fatalf("expected 0 allocs, got: %d", n) + } + + // TODO: do something here to create some allocations for + // an existing job, lookup again. +} From 19047248391686edda84d84ee268b0e647e23808 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 18:42:34 -0700 Subject: [PATCH 14/22] api: finishing jobs --- api/evaluations.go | 22 +++++++++ api/jobs.go | 20 ++++++++ api/jobs_test.go | 111 +++++++++++++++++++++++++++++++++++---------- api/util_test.go | 35 ++++++++++++++ 4 files changed, 165 insertions(+), 23 deletions(-) create mode 100644 api/evaluations.go create mode 100644 api/util_test.go diff --git a/api/evaluations.go b/api/evaluations.go new file mode 100644 index 000000000..4bb5b21ae --- /dev/null +++ b/api/evaluations.go @@ -0,0 +1,22 @@ +package api + +import ( + "time" +) + +// Evaluation is used to serialize an evaluation. +type Evaluation struct { + ID string + Priority int + Type string + TriggeredBy string + JobID string + JobModifyIndex uint64 + NodeID string + NodeModifyIndex uint64 + Status string + StatusDescription string + Wait time.Duration + NextEval string + PreviousEval string +} diff --git a/api/jobs.go b/api/jobs.go index 76e64a73b..66080381e 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -54,6 +54,26 @@ func (j *Jobs) Allocations(jobID string) ([]*Allocation, *QueryMeta, error) { return resp, qm, nil } +// Evaluations is used to query the evaluations associated with +// the given job ID. +func (j *Jobs) Evaluations(jobID string) ([]*Evaluation, *QueryMeta, error) { + var resp []*Evaluation + qm, err := j.client.query("/v1/job/"+jobID+"/evaluations", &resp, nil) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + +// Delete is used to remove an existing job. +func (j *Jobs) Delete(jobID string, q *WriteOptions) (*WriteMeta, error) { + wm, err := j.client.delete("/v1/job/"+jobID, nil, q) + if err != nil { + return nil, err + } + return wm, nil +} + // Job is used to serialize a job. type Job struct { ID string diff --git a/api/jobs_test.go b/api/jobs_test.go index a4220a888..99d5a009a 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -24,12 +24,7 @@ func TestJobs_Register(t *testing.T) { } // Create a job and attempt to register it - job := &Job{ - ID: "job1", - Name: "Job #1", - Type: "service", - Priority: 1, - } + job := testJob() eval, wm, err := jobs.Register(job, nil) if err != nil { t.Fatalf("err: %s", err) @@ -37,18 +32,14 @@ func TestJobs_Register(t *testing.T) { if eval == "" { t.Fatalf("missing eval id") } - if wm.LastIndex == 0 { - t.Fatalf("bad index: %d", wm.LastIndex) - } + assertWriteMeta(t, wm) // Query the jobs back out again resp, qm, err = jobs.List() if err != nil { t.Fatalf("err: %s", err) } - if qm.LastIndex == 0 { - t.Fatalf("bad index: %d", qm.LastIndex) - } + assertQueryMeta(t, qm) // Check that we got the expected response expect := []*Job{job} @@ -70,24 +61,21 @@ func TestJobs_Info(t *testing.T) { } // Register the job - job := &Job{ - ID: "job1", - Name: "Job #1", - Type: "service", - Priority: 1, - } - if _, _, err := jobs.Register(job, nil); err != nil { + job := testJob() + _, wm, err := jobs.Register(job, nil) + if err != nil { t.Fatalf("err: %s", err) } + assertWriteMeta(t, wm) // Query the job again and ensure it exists result, qm, err := jobs.Info("job1") if err != nil { t.Fatalf("err: %s", err) } - if qm.LastIndex == 0 { - t.Fatalf("bad index: %d", qm.LastIndex) - } + assertQueryMeta(t, qm) + + // Check that the result is what we expect if !reflect.DeepEqual(result, job) { t.Fatalf("expect: %#v, got: %#v", job, result) } @@ -104,7 +92,7 @@ func TestJobs_Allocations(t *testing.T) { t.Fatalf("err: %s", err) } if qm.LastIndex != 0 { - t.Fatalf("expected 0, got: %d", qm.LastIndex) + t.Fatalf("bad index: %d", qm.LastIndex) } if n := len(allocs); n != 0 { t.Fatalf("expected 0 allocs, got: %d", n) @@ -113,3 +101,80 @@ func TestJobs_Allocations(t *testing.T) { // TODO: do something here to create some allocations for // an existing job, lookup again. } + +func TestJobs_Evaluations(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Looking up by a non-existent job ID returns nothing + evals, qm, err := jobs.Evaluations("job1") + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(evals); n != 0 { + t.Fatalf("expected 0 evals, got: %d", n) + } + + // Insert a job. This also creates an evaluation so we should + // be able to query that out after. + job := testJob() + evalID, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Look up the evaluations again. + evals, qm, err = jobs.Evaluations("job1") + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + + // Check that we got the evals back + if n := len(evals); n == 0 || evals[0].ID != evalID { + t.Fatalf("expected 1 eval (%s), got: %#v", evalID, evals) + } +} + +func TestJobs_Delete(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Register a new job + job := testJob() + _, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Attempting delete on non-existing job does not error + wm2, err := jobs.Delete("nope", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm2) + + // Deleting an existing job works + wm3, err := jobs.Delete("job1", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm3) + + // Check that the job is really gone + result, qm, err := jobs.List() + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + if n := len(result); n != 0 { + t.Fatalf("expected 0 jobs, got: %d", n) + } +} diff --git a/api/util_test.go b/api/util_test.go new file mode 100644 index 000000000..4a7eb660e --- /dev/null +++ b/api/util_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "testing" +) + +func assertQueryMeta(t *testing.T, qm *QueryMeta) { + if qm.LastIndex == 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if qm.RequestTime == 0 { + t.Fatalf("bad request time: %d", qm.RequestTime) + } + if !qm.KnownLeader { + t.Fatalf("expected known leader, got none") + } +} + +func assertWriteMeta(t *testing.T, wm *WriteMeta) { + if wm.LastIndex == 0 { + t.Fatalf("bad index: %d", wm.LastIndex) + } + if wm.RequestTime == 0 { + t.Fatalf("bad request time: %d", wm.RequestTime) + } +} + +func testJob() *Job { + return &Job{ + ID: "job1", + Name: "redis", + Type: "service", + Priority: 1, + } +} From 7acc90aa320843ac7050f8a53eea3971dfd59a92 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 8 Sep 2015 19:27:04 -0700 Subject: [PATCH 15/22] api: add constraints generators --- api/jobs.go | 33 +++++++++++++++++++++++++++++++++ api/jobs_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/api/jobs.go b/api/jobs.go index 66080381e..6b457ada9 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -82,6 +82,7 @@ type Job struct { Priority int AllAtOnce bool Datacenters []string + Constraints []*Constraint Meta map[string]string Status string StatusDescription string @@ -96,3 +97,35 @@ type registerJobRequest struct { type registerJobResponse struct { EvalID string } + +// Constraint is used to serialize a job placement constraint. +type Constraint struct { + Hard bool + LTarget string + RTarget string + Operand string + Weight int +} + +// HardConstraint is used to create a new hard constraint. +func HardConstraint(left, operand, right string) *Constraint { + return constraint(left, operand, right, true, 0) +} + +// SoftConstraint is used to create a new soft constraint. It +// takes an additional weight parameter to allow balancing +// multiple soft constraints amongst eachother. +func SoftConstraint(left, operand, right string, weight int) *Constraint { + return constraint(left, operand, right, false, weight) +} + +// constraint generates a new job placement constraint. +func constraint(left, operand, right string, hard bool, weight int) *Constraint { + return &Constraint{ + Hard: hard, + LTarget: left, + RTarget: right, + Operand: operand, + Weight: weight, + } +} diff --git a/api/jobs_test.go b/api/jobs_test.go index 99d5a009a..db844d45d 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -178,3 +178,33 @@ func TestJobs_Delete(t *testing.T) { t.Fatalf("expected 0 jobs, got: %d", n) } } + +func TestJobs_Constraints(t *testing.T) { + { + c := HardConstraint("kernel.name", "=", "darwin") + expect := &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "darwin", + Operand: "=", + Weight: 0, + } + if !reflect.DeepEqual(c, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, c) + } + } + + { + c := SoftConstraint("memory.totalbytes", ">=", "250000000", 5) + expect := &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "250000000", + Operand: ">=", + Weight: 5, + } + if !reflect.DeepEqual(c, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, c) + } + } +} From 3ec197d44082bb77757f30b0c125b0ac1afedfad Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 00:33:56 -0700 Subject: [PATCH 16/22] api: structs are composable --- api/jobs.go | 110 +++++++++++++++++++++++++++++++++++++++++++++++ api/jobs_test.go | 49 +++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/api/jobs.go b/api/jobs.go index 6b457ada9..ea3f351c8 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -129,3 +129,113 @@ func constraint(left, operand, right string, hard bool, weight int) *Constraint Weight: weight, } } + +// TaskGroup is the unit of scheduling. +type TaskGroup struct { + Name string + Count int + Constraints []*Constraint + Tasks []*Task + Meta map[string]string +} + +// NewTaskGroup creates a new TaskGroup. +func NewTaskGroup(name string, count int) *TaskGroup { + return &TaskGroup{ + Name: name, + Count: count, + } +} + +// Constrain is used to add a constraint to a task group. +func (g *TaskGroup) Constrain(c *Constraint) *TaskGroup { + g.Constraints = append(g.Constraints, c) + return g +} + +// AddMeta is used to add a meta k/v pair to a task group +func (g *TaskGroup) AddMeta(key, val string) *TaskGroup { + if g.Meta == nil { + g.Meta = make(map[string]string) + } + g.Meta[key] = val + return g +} + +// AddTask is used to add a new task to a task group. +func (g *TaskGroup) AddTask(t *Task) *TaskGroup { + g.Tasks = append(g.Tasks, t) + return g +} + +// Task is a single process in a task group. +type Task struct { + Name string + Driver string + Config map[string]string + Constraints []*Constraint + Resources *Resources + Meta map[string]string +} + +// NewTask creates and initializes a new Task. +func NewTask(name, driver string) *Task { + return &Task{ + Name: name, + Driver: driver, + } +} + +// Configure is used to configure a single k/v pair on +// the task. +func (t *Task) Configure(key, val string) *Task { + if t.Config == nil { + t.Config = make(map[string]string) + } + t.Config[key] = val + return t +} + +// AddMeta is used to add metadata k/v pairs to the task. +func (t *Task) AddMeta(key, val string) *Task { + if t.Meta == nil { + t.Meta = make(map[string]string) + } + t.Meta[key] = val + return t +} + +// Require is used to add resource requirements to a task. +// It creates and initializes the task resources. +func (t *Task) Require(r *Resources) *Task { + if t.Resources == nil { + t.Resources = &Resources{} + } + if r == nil { + return t + } + t.Resources.CPU += r.CPU + t.Resources.MemoryMB += r.MemoryMB + t.Resources.DiskMB += r.DiskMB + t.Resources.IOPS += r.IOPS + return t +} + +// Resources encapsulates the required resources of +// a given task or task group. +type Resources struct { + CPU float64 + MemoryMB int + DiskMB int + IOPS int + Networks []*NetworkResource +} + +// NetworkResource is used to describe required network +// resources of a given task. +type NetworkResource struct { + Public bool + CIDR string + ReservedPorts []int + MBits int +} diff --git a/api/jobs_test.go b/api/jobs_test.go index db844d45d..d7756cd2a 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -208,3 +208,52 @@ func TestJobs_Constraints(t *testing.T) { } } } + +func TestJobs_Compose(t *testing.T) { + // Compose a task + task := NewTask("mytask", "docker"). + Require(&Resources{CPU: 1.25, MemoryMB: 1024}). + Require(&Resources{DiskMB: 2048}). + Configure("foo", "bar"). + Configure("baz", "zip") + + // Compose a task group + grp := NewTaskGroup("mytask", 2). + Constrain(HardConstraint("kernel.name", "=", "linux")). + Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 1)). + AddMeta("foo", "bar"). + AddMeta("baz", "zip"). + AddTask(task) + + // Check that the composed result looks correct + expect := &TaskGroup{ + Name: "mytask", + Count: 2, + Constraints: []*Constraint{ + HardConstraint("kernel.name", "=", "linux"), + SoftConstraint("memory.totalbytes", ">=", "128000000", 1), + }, + Tasks: []*Task{ + &Task{ + Name: "mytask", + Driver: "docker", + Resources: &Resources{ + CPU: 1.25, + MemoryMB: 1024, + DiskMB: 2048, + }, + Config: map[string]string{ + "foo": "bar", + "baz": "zip", + }, + }, + }, + Meta: map[string]string{ + "foo": "bar", + "baz": "zip", + }, + } + if !reflect.DeepEqual(grp, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp) + } +} From aff13fc84ccea1b49414d2755b7235c4e7906b33 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 13:02:39 -0700 Subject: [PATCH 17/22] api: restructure client --- api/compose_test.go | 88 +++++++++++++++++++++++++ api/constraint.go | 33 ++++++++++ api/constraint_test.go | 32 ++++++++++ api/jobs.go | 142 ----------------------------------------- api/jobs_test.go | 79 ----------------------- api/resources.go | 20 ++++++ api/tasks.go | 82 ++++++++++++++++++++++++ 7 files changed, 255 insertions(+), 221 deletions(-) create mode 100644 api/compose_test.go create mode 100644 api/constraint.go create mode 100644 api/constraint_test.go create mode 100644 api/resources.go create mode 100644 api/tasks.go diff --git a/api/compose_test.go b/api/compose_test.go new file mode 100644 index 000000000..49cff536e --- /dev/null +++ b/api/compose_test.go @@ -0,0 +1,88 @@ +package api + +import ( + "reflect" + "testing" +) + +func TestCompose(t *testing.T) { + // Compose a task + task := NewTask("mytask", "docker"). + SetConfig("foo", "bar"). + SetConfig("baz", "zip") + + // Require some amount of resources + task.Require(&Resources{ + CPU: 1.25, + MemoryMB: 1024, + DiskMB: 2048, + IOPS: 1024, + Networks: []*NetworkResource{ + &NetworkResource{ + CIDR: "0.0.0.0/0", + MBits: 100, + ReservedPorts: []int{80, 443}, + }, + }, + }) + + // Compose a task group + grp := NewTaskGroup("mygroup", 2). + Constrain(HardConstraint("kernel.name", "=", "linux")). + Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 1)). + SetMeta("foo", "bar"). + SetMeta("baz", "zip"). + AddTask(task) + + // Check that the composed result looks correct + expect := &TaskGroup{ + Name: "mygroup", + Count: 2, + Constraints: []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "linux", + Operand: "=", + Weight: 0, + }, + &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "128000000", + Operand: ">=", + Weight: 1, + }, + }, + Tasks: []*Task{ + &Task{ + Name: "mytask", + Driver: "docker", + Resources: &Resources{ + CPU: 1.25, + MemoryMB: 1024, + DiskMB: 2048, + IOPS: 1024, + Networks: []*NetworkResource{ + &NetworkResource{ + CIDR: "0.0.0.0/0", + MBits: 100, + ReservedPorts: []int{80, 443}, + }, + }, + }, + Config: map[string]string{ + "foo": "bar", + "baz": "zip", + }, + }, + }, + Meta: map[string]string{ + "foo": "bar", + "baz": "zip", + }, + } + if !reflect.DeepEqual(grp, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp) + } +} diff --git a/api/constraint.go b/api/constraint.go new file mode 100644 index 000000000..1a0f3233a --- /dev/null +++ b/api/constraint.go @@ -0,0 +1,33 @@ +package api + +// Constraint is used to serialize a job placement constraint. +type Constraint struct { + Hard bool + LTarget string + RTarget string + Operand string + Weight int +} + +// HardConstraint is used to create a new hard constraint. +func HardConstraint(left, operand, right string) *Constraint { + return constraint(left, operand, right, true, 0) +} + +// SoftConstraint is used to create a new soft constraint. It +// takes an additional weight parameter to allow balancing +// multiple soft constraints amongst eachother. +func SoftConstraint(left, operand, right string, weight int) *Constraint { + return constraint(left, operand, right, false, weight) +} + +// constraint generates a new job placement constraint. +func constraint(left, operand, right string, hard bool, weight int) *Constraint { + return &Constraint{ + Hard: hard, + LTarget: left, + RTarget: right, + Operand: operand, + Weight: weight, + } +} diff --git a/api/constraint_test.go b/api/constraint_test.go new file mode 100644 index 000000000..a8c7b9838 --- /dev/null +++ b/api/constraint_test.go @@ -0,0 +1,32 @@ +package api + +import ( + "reflect" + "testing" +) + +func TestCompose_Constraints(t *testing.T) { + c := HardConstraint("kernel.name", "=", "darwin") + expect := &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "darwin", + Operand: "=", + Weight: 0, + } + if !reflect.DeepEqual(c, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, c) + } + + c = SoftConstraint("memory.totalbytes", ">=", "250000000", 5) + expect = &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "250000000", + Operand: ">=", + Weight: 5, + } + if !reflect.DeepEqual(c, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, c) + } +} diff --git a/api/jobs.go b/api/jobs.go index ea3f351c8..308538a6a 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -97,145 +97,3 @@ type registerJobRequest struct { type registerJobResponse struct { EvalID string } - -// Constraint is used to serialize a job placement constraint. -type Constraint struct { - Hard bool - LTarget string - RTarget string - Operand string - Weight int -} - -// HardConstraint is used to create a new hard constraint. -func HardConstraint(left, operand, right string) *Constraint { - return constraint(left, operand, right, true, 0) -} - -// SoftConstraint is used to create a new soft constraint. It -// takes an additional weight parameter to allow balancing -// multiple soft constraints amongst eachother. -func SoftConstraint(left, operand, right string, weight int) *Constraint { - return constraint(left, operand, right, false, weight) -} - -// constraint generates a new job placement constraint. -func constraint(left, operand, right string, hard bool, weight int) *Constraint { - return &Constraint{ - Hard: hard, - LTarget: left, - RTarget: right, - Operand: operand, - Weight: weight, - } -} - -// TaskGroup is the unit of scheduling. -type TaskGroup struct { - Name string - Count int - Constraints []*Constraint - Tasks []*Task - Meta map[string]string -} - -// NewTaskGroup creates a new TaskGroup. -func NewTaskGroup(name string, count int) *TaskGroup { - return &TaskGroup{ - Name: name, - Count: count, - } -} - -// Constrain is used to add a constraint to a task group. -func (g *TaskGroup) Constrain(c *Constraint) *TaskGroup { - g.Constraints = append(g.Constraints, c) - return g -} - -// AddMeta is used to add a meta k/v pair to a task group -func (g *TaskGroup) AddMeta(key, val string) *TaskGroup { - if g.Meta == nil { - g.Meta = make(map[string]string) - } - g.Meta[key] = val - return g -} - -// AddTask is used to add a new task to a task group. -func (g *TaskGroup) AddTask(t *Task) *TaskGroup { - g.Tasks = append(g.Tasks, t) - return g -} - -// Task is a single process in a task group. -type Task struct { - Name string - Driver string - Config map[string]string - Constraints []*Constraint - Resources *Resources - Meta map[string]string -} - -// NewTask creates and initializes a new Task. -func NewTask(name, driver string) *Task { - return &Task{ - Name: name, - Driver: driver, - } -} - -// Configure is used to configure a single k/v pair on -// the task. -func (t *Task) Configure(key, val string) *Task { - if t.Config == nil { - t.Config = make(map[string]string) - } - t.Config[key] = val - return t -} - -// AddMeta is used to add metadata k/v pairs to the task. -func (t *Task) AddMeta(key, val string) *Task { - if t.Meta == nil { - t.Meta = make(map[string]string) - } - t.Meta[key] = val - return t -} - -// Require is used to add resource requirements to a task. -// It creates and initializes the task resources. -func (t *Task) Require(r *Resources) *Task { - if t.Resources == nil { - t.Resources = &Resources{} - } - if r == nil { - return t - } - t.Resources.CPU += r.CPU - t.Resources.MemoryMB += r.MemoryMB - t.Resources.DiskMB += r.DiskMB - t.Resources.IOPS += r.IOPS - return t -} - -// Resources encapsulates the required resources of -// a given task or task group. -type Resources struct { - CPU float64 - MemoryMB int - DiskMB int - IOPS int - Networks []*NetworkResource -} - -// NetworkResource is used to describe required network -// resources of a given task. -type NetworkResource struct { - Public bool - CIDR string - ReservedPorts []int - MBits int -} diff --git a/api/jobs_test.go b/api/jobs_test.go index d7756cd2a..99d5a009a 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -178,82 +178,3 @@ func TestJobs_Delete(t *testing.T) { t.Fatalf("expected 0 jobs, got: %d", n) } } - -func TestJobs_Constraints(t *testing.T) { - { - c := HardConstraint("kernel.name", "=", "darwin") - expect := &Constraint{ - Hard: true, - LTarget: "kernel.name", - RTarget: "darwin", - Operand: "=", - Weight: 0, - } - if !reflect.DeepEqual(c, expect) { - t.Fatalf("expect: %#v, got: %#v", expect, c) - } - } - - { - c := SoftConstraint("memory.totalbytes", ">=", "250000000", 5) - expect := &Constraint{ - Hard: false, - LTarget: "memory.totalbytes", - RTarget: "250000000", - Operand: ">=", - Weight: 5, - } - if !reflect.DeepEqual(c, expect) { - t.Fatalf("expect: %#v, got: %#v", expect, c) - } - } -} - -func TestJobs_Compose(t *testing.T) { - // Compose a task - task := NewTask("mytask", "docker"). - Require(&Resources{CPU: 1.25, MemoryMB: 1024}). - Require(&Resources{DiskMB: 2048}). - Configure("foo", "bar"). - Configure("baz", "zip") - - // Compose a task group - grp := NewTaskGroup("mytask", 2). - Constrain(HardConstraint("kernel.name", "=", "linux")). - Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 1)). - AddMeta("foo", "bar"). - AddMeta("baz", "zip"). - AddTask(task) - - // Check that the composed result looks correct - expect := &TaskGroup{ - Name: "mytask", - Count: 2, - Constraints: []*Constraint{ - HardConstraint("kernel.name", "=", "linux"), - SoftConstraint("memory.totalbytes", ">=", "128000000", 1), - }, - Tasks: []*Task{ - &Task{ - Name: "mytask", - Driver: "docker", - Resources: &Resources{ - CPU: 1.25, - MemoryMB: 1024, - DiskMB: 2048, - }, - Config: map[string]string{ - "foo": "bar", - "baz": "zip", - }, - }, - }, - Meta: map[string]string{ - "foo": "bar", - "baz": "zip", - }, - } - if !reflect.DeepEqual(grp, expect) { - t.Fatalf("expect: %#v, got: %#v", expect, grp) - } -} diff --git a/api/resources.go b/api/resources.go new file mode 100644 index 000000000..64739df5a --- /dev/null +++ b/api/resources.go @@ -0,0 +1,20 @@ +package api + +// Resources encapsulates the required resources of +// a given task or task group. +type Resources struct { + CPU float64 + MemoryMB int + DiskMB int + IOPS int + Networks []*NetworkResource +} + +// NetworkResource is used to describe required network +// resources of a given task. +type NetworkResource struct { + Public bool + CIDR string + ReservedPorts []int + MBits int +} diff --git a/api/tasks.go b/api/tasks.go new file mode 100644 index 000000000..a1858a0d0 --- /dev/null +++ b/api/tasks.go @@ -0,0 +1,82 @@ +package api + +// TaskGroup is the unit of scheduling. +type TaskGroup struct { + Name string + Count int + Constraints []*Constraint + Tasks []*Task + Meta map[string]string +} + +// NewTaskGroup creates a new TaskGroup. +func NewTaskGroup(name string, count int) *TaskGroup { + return &TaskGroup{ + Name: name, + Count: count, + } +} + +// Constrain is used to add a constraint to a task group. +func (g *TaskGroup) Constrain(c *Constraint) *TaskGroup { + g.Constraints = append(g.Constraints, c) + return g +} + +// AddMeta is used to add a meta k/v pair to a task group +func (g *TaskGroup) SetMeta(key, val string) *TaskGroup { + if g.Meta == nil { + g.Meta = make(map[string]string) + } + g.Meta[key] = val + return g +} + +// AddTask is used to add a new task to a task group. +func (g *TaskGroup) AddTask(t *Task) *TaskGroup { + g.Tasks = append(g.Tasks, t) + return g +} + +// Task is a single process in a task group. +type Task struct { + Name string + Driver string + Config map[string]string + Constraints []*Constraint + Resources *Resources + Meta map[string]string +} + +// NewTask creates and initializes a new Task. +func NewTask(name, driver string) *Task { + return &Task{ + Name: name, + Driver: driver, + } +} + +// Configure is used to configure a single k/v pair on +// the task. +func (t *Task) SetConfig(key, val string) *Task { + if t.Config == nil { + t.Config = make(map[string]string) + } + t.Config[key] = val + return t +} + +// SetMeta is used to add metadata k/v pairs to the task. +func (t *Task) SetMeta(key, val string) *Task { + if t.Meta == nil { + t.Meta = make(map[string]string) + } + t.Meta[key] = val + return t +} + +// Require is used to add resource requirements to a task. +func (t *Task) Require(r *Resources) *Task { + t.Resources = r + return t +} From a807612091a2f648662878329a42743903623f42 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 13:18:50 -0700 Subject: [PATCH 18/22] api: allow query options everywhere --- api/allocations.go | 14 ++++++++++++-- api/allocations_test.go | 4 ++-- api/jobs.go | 16 ++++++++-------- api/jobs_test.go | 16 ++++++++-------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/api/allocations.go b/api/allocations.go index c67400bad..f247fed2a 100644 --- a/api/allocations.go +++ b/api/allocations.go @@ -11,15 +11,25 @@ func (c *Client) Allocations() *Allocations { } // List returns a list of all of the allocations. -func (a *Allocations) List() ([]*Allocation, *QueryMeta, error) { +func (a *Allocations) List(q *QueryOptions) ([]*Allocation, *QueryMeta, error) { var resp []*Allocation - qm, err := a.client.query("/v1/allocations", &resp, nil) + qm, err := a.client.query("/v1/allocations", &resp, q) if err != nil { return nil, nil, err } return resp, qm, nil } +// Info is used to retrieve a single allocation. +func (a *Allocations) Info(allocID string, q *QueryOptions) (*Allocation, *QueryMeta, error) { + var resp Allocation + qm, err := a.client.query("/v1/allocation/"+allocID, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} + // Allocation is used for serialization of allocations. type Allocation struct { ID string diff --git a/api/allocations_test.go b/api/allocations_test.go index d3189ec38..ab45381f2 100644 --- a/api/allocations_test.go +++ b/api/allocations_test.go @@ -10,7 +10,7 @@ func TestAllocations_List(t *testing.T) { a := c.Allocations() // Querying when no allocs exist returns nothing - allocs, qm, err := a.List() + allocs, qm, err := a.List(nil) if err != nil { t.Fatalf("err: %s", err) } @@ -36,7 +36,7 @@ func TestAllocations_List(t *testing.T) { } // List the allocations again - allocs, qm, err = a.List() + allocs, qm, err = a.List(nil) if err != nil { t.Fatalf("err: %s", err) } diff --git a/api/jobs.go b/api/jobs.go index 308538a6a..ccd96ad5c 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -24,9 +24,9 @@ func (j *Jobs) Register(job *Job, q *WriteOptions) (string, *WriteMeta, error) { } // List is used to list all of the existing jobs. -func (j *Jobs) List() ([]*Job, *QueryMeta, error) { +func (j *Jobs) List(q *QueryOptions) ([]*Job, *QueryMeta, error) { var resp []*Job - qm, err := j.client.query("/v1/jobs", &resp, nil) + qm, err := j.client.query("/v1/jobs", &resp, q) if err != nil { return nil, qm, err } @@ -35,9 +35,9 @@ func (j *Jobs) List() ([]*Job, *QueryMeta, error) { // Info is used to retrieve information about a particular // job given its unique ID. -func (j *Jobs) Info(jobID string) (*Job, *QueryMeta, error) { +func (j *Jobs) Info(jobID string, q *QueryOptions) (*Job, *QueryMeta, error) { var resp Job - qm, err := j.client.query("/v1/job/"+jobID, &resp, nil) + qm, err := j.client.query("/v1/job/"+jobID, &resp, q) if err != nil { return nil, nil, err } @@ -45,9 +45,9 @@ func (j *Jobs) Info(jobID string) (*Job, *QueryMeta, error) { } // Allocations is used to return the allocs for a given job ID. -func (j *Jobs) Allocations(jobID string) ([]*Allocation, *QueryMeta, error) { +func (j *Jobs) Allocations(jobID string, q *QueryOptions) ([]*Allocation, *QueryMeta, error) { var resp []*Allocation - qm, err := j.client.query("/v1/job/"+jobID+"/allocations", &resp, nil) + qm, err := j.client.query("/v1/job/"+jobID+"/allocations", &resp, q) if err != nil { return nil, nil, err } @@ -56,9 +56,9 @@ func (j *Jobs) Allocations(jobID string) ([]*Allocation, *QueryMeta, error) { // Evaluations is used to query the evaluations associated with // the given job ID. -func (j *Jobs) Evaluations(jobID string) ([]*Evaluation, *QueryMeta, error) { +func (j *Jobs) Evaluations(jobID string, q *QueryOptions) ([]*Evaluation, *QueryMeta, error) { var resp []*Evaluation - qm, err := j.client.query("/v1/job/"+jobID+"/evaluations", &resp, nil) + qm, err := j.client.query("/v1/job/"+jobID+"/evaluations", &resp, q) if err != nil { return nil, nil, err } diff --git a/api/jobs_test.go b/api/jobs_test.go index 99d5a009a..b1e77114c 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -12,7 +12,7 @@ func TestJobs_Register(t *testing.T) { jobs := c.Jobs() // Listing jobs before registering returns nothing - resp, qm, err := jobs.List() + resp, qm, err := jobs.List(nil) if err != nil { t.Fatalf("err: %s", err) } @@ -35,7 +35,7 @@ func TestJobs_Register(t *testing.T) { assertWriteMeta(t, wm) // Query the jobs back out again - resp, qm, err = jobs.List() + resp, qm, err = jobs.List(nil) if err != nil { t.Fatalf("err: %s", err) } @@ -55,7 +55,7 @@ func TestJobs_Info(t *testing.T) { // Trying to retrieve a job by ID before it exists // returns an error - _, _, err := jobs.Info("job1") + _, _, err := jobs.Info("job1", nil) if err == nil || !strings.Contains(err.Error(), "not found") { t.Fatalf("expected not found error, got: %#v", err) } @@ -69,7 +69,7 @@ func TestJobs_Info(t *testing.T) { assertWriteMeta(t, wm) // Query the job again and ensure it exists - result, qm, err := jobs.Info("job1") + result, qm, err := jobs.Info("job1", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -87,7 +87,7 @@ func TestJobs_Allocations(t *testing.T) { jobs := c.Jobs() // Looking up by a non-existent job returns nothing - allocs, qm, err := jobs.Allocations("job1") + allocs, qm, err := jobs.Allocations("job1", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -108,7 +108,7 @@ func TestJobs_Evaluations(t *testing.T) { jobs := c.Jobs() // Looking up by a non-existent job ID returns nothing - evals, qm, err := jobs.Evaluations("job1") + evals, qm, err := jobs.Evaluations("job1", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -129,7 +129,7 @@ func TestJobs_Evaluations(t *testing.T) { assertWriteMeta(t, wm) // Look up the evaluations again. - evals, qm, err = jobs.Evaluations("job1") + evals, qm, err = jobs.Evaluations("job1", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -169,7 +169,7 @@ func TestJobs_Delete(t *testing.T) { assertWriteMeta(t, wm3) // Check that the job is really gone - result, qm, err := jobs.List() + result, qm, err := jobs.List(nil) if err != nil { t.Fatalf("err: %s", err) } From 2cbdd4c1c377b1ec298d6c570c4c271e3f425fe9 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 13:48:56 -0700 Subject: [PATCH 19/22] api: working on evaluations --- api/evaluations.go | 41 ++++++++++++++++++ api/evaluations_test.go | 96 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 api/evaluations_test.go diff --git a/api/evaluations.go b/api/evaluations.go index 4bb5b21ae..2956664b4 100644 --- a/api/evaluations.go +++ b/api/evaluations.go @@ -4,6 +4,47 @@ import ( "time" ) +// Evaluations is used to query the evaluation endpoints. +type Evaluations struct { + client *Client +} + +// Evaluations returns a new handle on the evaluations. +func (c *Client) Evaluations() *Evaluations { + return &Evaluations{client: c} +} + +// List is used to dump all of the evaluations. +func (e *Evaluations) List(q *QueryOptions) ([]*Evaluation, *QueryMeta, error) { + var resp []*Evaluation + qm, err := e.client.query("/v1/evaluations", &resp, q) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + +// Info is used to query a single evaluation by its ID. +func (e *Evaluations) Info(evalID string, q *QueryOptions) (*Evaluation, *QueryMeta, error) { + var resp Evaluation + qm, err := e.client.query("/v1/evaluation/"+evalID, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} + +// Allocations is used to retrieve a set of allocations given +// an evaluation ID. +func (e *Evaluations) Allocations(evalID string, q *QueryOptions) ([]*Allocation, *QueryMeta, error) { + var resp []*Allocation + qm, err := e.client.query("/v1/evaluation/"+evalID+"/allocations", &resp, q) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + // Evaluation is used to serialize an evaluation. type Evaluation struct { ID string diff --git a/api/evaluations_test.go b/api/evaluations_test.go new file mode 100644 index 000000000..03bd1be53 --- /dev/null +++ b/api/evaluations_test.go @@ -0,0 +1,96 @@ +package api + +import ( + "strings" + "testing" +) + +func TestEvaluations_List(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + e := c.Evaluations() + + // Listing when nothing exists returns empty + result, qm, err := e.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(result); n != 0 { + t.Fatalf("expected 0 evaluations, got: %d", n) + } + + // Register a job. This will create an evaluation. + jobs := c.Jobs() + job := testJob() + evalID, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Check the evaluations again + result, qm, err = e.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + + // Check if we have the right list + if len(result) != 1 || result[0].ID != evalID { + t.Fatalf("bad: %#v", result) + } +} + +func TestEvaluations_Info(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + e := c.Evaluations() + + // Querying a non-existent evaluation returns error + _, _, err := e.Info("8E231CF4-CA48-43FF-B694-5801E69E22FA", nil) + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected not found error, got: %s", err) + } + + // Register a job. Creates a new evaluation. + jobs := c.Jobs() + job := testJob() + evalID, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Try looking up by the new eval ID + result, qm, err := e.Info(evalID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + + // Check that we got the right result + if result == nil || result.ID != evalID { + t.Fatalf("expected eval %q, got: %#v", evalID, result) + } +} + +func TestEvaluations_Allocations(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + e := c.Evaluations() + + // Returns empty if no allocations + allocs, qm, err := e.Allocations("8E231CF4-CA48-43FF-B694-5801E69E22FA", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if qm.LastIndex != 0 { + t.Fatalf("bad index: %d", qm.LastIndex) + } + if n := len(allocs); n != 0 { + t.Fatalf("expected 0 allocs, got: %d", n) + } +} From 7926aed642eddf8739c8d4c80baff1d3fb596f8d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 17:29:43 -0700 Subject: [PATCH 20/22] api: more tests for jobs --- api/allocations_test.go | 2 +- api/compose_test.go | 100 +++++++++++++++++++++++++--------------- api/jobs.go | 60 ++++++++++++++++++++++++ api/jobs_test.go | 85 ++++++++++++++++++++++++++++++++++ api/tasks.go | 15 +++++- api/util_test.go | 2 +- 6 files changed, 222 insertions(+), 42 deletions(-) diff --git a/api/allocations_test.go b/api/allocations_test.go index ab45381f2..be1860eee 100644 --- a/api/allocations_test.go +++ b/api/allocations_test.go @@ -28,7 +28,7 @@ func TestAllocations_List(t *testing.T) { job := &Job{ ID: "job1", Name: "Job #1", - Type: "service", + Type: JobTypeService, } eval, _, err := c.Jobs().Register(job, nil) if err != nil { diff --git a/api/compose_test.go b/api/compose_test.go index 49cff536e..743633847 100644 --- a/api/compose_test.go +++ b/api/compose_test.go @@ -9,10 +9,8 @@ func TestCompose(t *testing.T) { // Compose a task task := NewTask("mytask", "docker"). SetConfig("foo", "bar"). - SetConfig("baz", "zip") - - // Require some amount of resources - task.Require(&Resources{ + Constrain(HardConstraint("kernel.name", "=", "linux")). + Require(&Resources{ CPU: 1.25, MemoryMB: 1024, DiskMB: 2048, @@ -27,17 +25,26 @@ func TestCompose(t *testing.T) { }) // Compose a task group - grp := NewTaskGroup("mygroup", 2). + grp := NewTaskGroup("mygroup"). + SetCount(2). Constrain(HardConstraint("kernel.name", "=", "linux")). - Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 1)). SetMeta("foo", "bar"). - SetMeta("baz", "zip"). AddTask(task) + // Compose a job + job := NewServiceJob("job1", "myjob", 2). + SetMeta("foo", "bar"). + AddDatacenter("dc1"). + Constrain(HardConstraint("kernel.name", "=", "linux")). + AddTaskGroup(grp) + // Check that the composed result looks correct - expect := &TaskGroup{ - Name: "mygroup", - Count: 2, + expect := &Job{ + ID: "job1", + Name: "myjob", + Type: JobTypeService, + Priority: 2, + Datacenters: []string{"dc1"}, Constraints: []*Constraint{ &Constraint{ Hard: true, @@ -46,43 +53,60 @@ func TestCompose(t *testing.T) { Operand: "=", Weight: 0, }, - &Constraint{ - Hard: false, - LTarget: "memory.totalbytes", - RTarget: "128000000", - Operand: ">=", - Weight: 1, - }, }, - Tasks: []*Task{ - &Task{ - Name: "mytask", - Driver: "docker", - Resources: &Resources{ - CPU: 1.25, - MemoryMB: 1024, - DiskMB: 2048, - IOPS: 1024, - Networks: []*NetworkResource{ - &NetworkResource{ - CIDR: "0.0.0.0/0", - MBits: 100, - ReservedPorts: []int{80, 443}, + TaskGroups: []*TaskGroup{ + &TaskGroup{ + Name: "mygroup", + Count: 2, + Constraints: []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "linux", + Operand: "=", + Weight: 0, + }, + }, + Tasks: []*Task{ + &Task{ + Name: "mytask", + Driver: "docker", + Resources: &Resources{ + CPU: 1.25, + MemoryMB: 1024, + DiskMB: 2048, + IOPS: 1024, + Networks: []*NetworkResource{ + &NetworkResource{ + CIDR: "0.0.0.0/0", + MBits: 100, + ReservedPorts: []int{80, 443}, + }, + }, + }, + Constraints: []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "linux", + Operand: "=", + Weight: 0, + }, + }, + Config: map[string]string{ + "foo": "bar", + "baz": "zip", }, }, }, - Config: map[string]string{ + Meta: map[string]string{ "foo": "bar", "baz": "zip", }, }, }, - Meta: map[string]string{ - "foo": "bar", - "baz": "zip", - }, } - if !reflect.DeepEqual(grp, expect) { - t.Fatalf("expect: %#v, got: %#v", expect, grp) + if !reflect.DeepEqual(job, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, job) } } diff --git a/api/jobs.go b/api/jobs.go index ccd96ad5c..36b2898d5 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -1,5 +1,13 @@ package api +const ( + // JobTypeService indicates a long-running processes + JobTypeService = "service" + + // JobTypeBatch indicates a short-lived process + JobTypeBatch = "batch" +) + // Jobs is used to access the job-specific endpoints. type Jobs struct { client *Client @@ -83,11 +91,63 @@ type Job struct { AllAtOnce bool Datacenters []string Constraints []*Constraint + TaskGroups []*TaskGroup Meta map[string]string Status string StatusDescription string } +// NewServiceJob creates and returns a new service-style job +// for long-lived processes using the provided name, ID, and +// relative job priority. +func NewServiceJob(id, name string, pri int) *Job { + return newJob(id, name, JobTypeService, pri) +} + +// NewBatchJob creates and returns a new batch-style job for +// short-lived processes using the provided name and ID along +// with the relative job priority. +func NewBatchJob(id, name string, pri int) *Job { + return newJob(id, name, JobTypeBatch, pri) +} + +// newJob is used to create a new Job struct. +func newJob(jobID, jobName, jobType string, pri int) *Job { + return &Job{ + ID: jobID, + Name: jobName, + Type: jobType, + Priority: pri, + } +} + +// SetMeta is used to set arbitrary k/v pairs of metadata on a job. +func (j *Job) SetMeta(key, val string) *Job { + if j.Meta == nil { + j.Meta = make(map[string]string) + } + j.Meta[key] = val + return j +} + +// AddDatacenter is used to add a datacenter to a job. +func (j *Job) AddDatacenter(dc string) *Job { + j.Datacenters = append(j.Datacenters, dc) + return j +} + +// Constrain is used to add a constraint to a job. +func (j *Job) Constrain(c *Constraint) *Job { + j.Constraints = append(j.Constraints, c) + return j +} + +// AddTaskGroup adds a task group to an existing job. +func (j *Job) AddTaskGroup(grp *TaskGroup) *Job { + j.TaskGroups = append(j.TaskGroups, grp) + return j +} + // registerJobRequest is used to serialize a job registration type registerJobRequest struct { Job *Job diff --git a/api/jobs_test.go b/api/jobs_test.go index b1e77114c..5ff3e5166 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -178,3 +178,88 @@ func TestJobs_Delete(t *testing.T) { t.Fatalf("expected 0 jobs, got: %d", n) } } + +func TestJobs_NewBatchJob(t *testing.T) { + job := NewBatchJob("job1", "myjob", 5) + expect := &Job{ + ID: "job1", + Name: "myjob", + Type: JobTypeBatch, + Priority: 5, + } + if !reflect.DeepEqual(job, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, job) + } +} + +func TestJobs_NewServiceJob(t *testing.T) { + job := NewServiceJob("job1", "myjob", 5) + expect := &Job{ + ID: "job1", + Name: "myjob", + Type: JobTypeService, + Priority: 5, + } + if !reflect.DeepEqual(job, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, job) + } +} + +func TestJobs_SetMeta(t *testing.T) { + job := &Job{Meta: nil} + + // Initializes a nil map + out := job.SetMeta("foo", "bar") + if job.Meta == nil { + t.Fatalf("should initialize metadata") + } + + // Check that the job was returned + if job != out { + t.Fatalf("expect: %#v, got: %#v", job, out) + } + + // Setting another pair is additive + job.SetMeta("baz", "zip") + expect := map[string]string{"foo": "bar", "baz": "zip"} + if !reflect.DeepEqual(job.Meta, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, job.Meta) + } +} + +func TestJobs_Constrain(t *testing.T) { + job := &Job{Constraints: nil} + + // Create and add a constraint + out := job.Constrain(HardConstraint("kernel.name", "=", "darwin")) + if n := len(job.Constraints); n != 1 { + t.Fatalf("expected 1 constraint, got: %d", n) + } + + // Check that the job was returned + if job != out { + t.Fatalf("expect: %#v, got: %#v", job, out) + } + + // Adding another constraint preserves the original + job.Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 2)) + expect := []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "darwin", + Operand: "=", + Weight: 0, + }, + &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "128000000", + Operand: ">=", + Weight: 2, + }, + } + if !reflect.DeepEqual(job.Constraints, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, job.Constraints) + } +} diff --git a/api/tasks.go b/api/tasks.go index a1858a0d0..a02de6a41 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -10,13 +10,18 @@ type TaskGroup struct { } // NewTaskGroup creates a new TaskGroup. -func NewTaskGroup(name string, count int) *TaskGroup { +func NewTaskGroup(name string) *TaskGroup { return &TaskGroup{ Name: name, - Count: count, + Count: 1, } } +func (g *TaskGroup) SetCount(count int) *TaskGroup { + g.Count = count + return g +} + // Constrain is used to add a constraint to a task group. func (g *TaskGroup) Constrain(c *Constraint) *TaskGroup { g.Constraints = append(g.Constraints, c) @@ -80,3 +85,9 @@ func (t *Task) Require(r *Resources) *Task { t.Resources = r return t } + +// Constraint adds a new constraints to a single task. +func (t *Task) Constrain(c *Constraint) *Task { + t.Constraints = append(t.Constraints, c) + return t +} diff --git a/api/util_test.go b/api/util_test.go index 4a7eb660e..87066444b 100644 --- a/api/util_test.go +++ b/api/util_test.go @@ -29,7 +29,7 @@ func testJob() *Job { return &Job{ ID: "job1", Name: "redis", - Type: "service", + Type: JobTypeService, Priority: 1, } } From 5e1932136af928f8dd280b6b80f024eb3b7dd92c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 17:59:18 -0700 Subject: [PATCH 21/22] api: more tests --- api/compose_test.go | 41 ++++---- api/tasks.go | 9 +- api/tasks_test.go | 227 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 23 deletions(-) create mode 100644 api/tasks_test.go diff --git a/api/compose_test.go b/api/compose_test.go index 743633847..b8f6d781a 100644 --- a/api/compose_test.go +++ b/api/compose_test.go @@ -7,8 +7,9 @@ import ( func TestCompose(t *testing.T) { // Compose a task - task := NewTask("mytask", "docker"). + task := NewTask("task1", "exec"). SetConfig("foo", "bar"). + SetMeta("foo", "bar"). Constrain(HardConstraint("kernel.name", "=", "linux")). Require(&Resources{ CPU: 1.25, @@ -25,8 +26,7 @@ func TestCompose(t *testing.T) { }) // Compose a task group - grp := NewTaskGroup("mygroup"). - SetCount(2). + grp := NewTaskGroup("grp1", 2). Constrain(HardConstraint("kernel.name", "=", "linux")). SetMeta("foo", "bar"). AddTask(task) @@ -40,11 +40,16 @@ func TestCompose(t *testing.T) { // Check that the composed result looks correct expect := &Job{ - ID: "job1", - Name: "myjob", - Type: JobTypeService, - Priority: 2, - Datacenters: []string{"dc1"}, + ID: "job1", + Name: "myjob", + Type: JobTypeService, + Priority: 2, + Datacenters: []string{ + "dc1", + }, + Meta: map[string]string{ + "foo": "bar", + }, Constraints: []*Constraint{ &Constraint{ Hard: true, @@ -56,7 +61,7 @@ func TestCompose(t *testing.T) { }, TaskGroups: []*TaskGroup{ &TaskGroup{ - Name: "mygroup", + Name: "grp1", Count: 2, Constraints: []*Constraint{ &Constraint{ @@ -69,8 +74,8 @@ func TestCompose(t *testing.T) { }, Tasks: []*Task{ &Task{ - Name: "mytask", - Driver: "docker", + Name: "task1", + Driver: "exec", Resources: &Resources{ CPU: 1.25, MemoryMB: 1024, @@ -78,9 +83,12 @@ func TestCompose(t *testing.T) { IOPS: 1024, Networks: []*NetworkResource{ &NetworkResource{ - CIDR: "0.0.0.0/0", - MBits: 100, - ReservedPorts: []int{80, 443}, + CIDR: "0.0.0.0/0", + MBits: 100, + ReservedPorts: []int{ + 80, + 443, + }, }, }, }, @@ -95,13 +103,14 @@ func TestCompose(t *testing.T) { }, Config: map[string]string{ "foo": "bar", - "baz": "zip", + }, + Meta: map[string]string{ + "foo": "bar", }, }, }, Meta: map[string]string{ "foo": "bar", - "baz": "zip", }, }, }, diff --git a/api/tasks.go b/api/tasks.go index a02de6a41..23e9ca638 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -10,18 +10,13 @@ type TaskGroup struct { } // NewTaskGroup creates a new TaskGroup. -func NewTaskGroup(name string) *TaskGroup { +func NewTaskGroup(name string, count int) *TaskGroup { return &TaskGroup{ Name: name, - Count: 1, + Count: count, } } -func (g *TaskGroup) SetCount(count int) *TaskGroup { - g.Count = count - return g -} - // Constrain is used to add a constraint to a task group. func (g *TaskGroup) Constrain(c *Constraint) *TaskGroup { g.Constraints = append(g.Constraints, c) diff --git a/api/tasks_test.go b/api/tasks_test.go new file mode 100644 index 000000000..f5442ee98 --- /dev/null +++ b/api/tasks_test.go @@ -0,0 +1,227 @@ +package api + +import ( + "reflect" + "testing" +) + +func TestTaskGroup_NewTaskGroup(t *testing.T) { + grp := NewTaskGroup("grp1", 2) + expect := &TaskGroup{ + Name: "grp1", + Count: 2, + } + if !reflect.DeepEqual(grp, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp) + } +} + +func TestTaskGroup_Constrain(t *testing.T) { + grp := NewTaskGroup("grp1", 1) + + // Add a constraint to the group + out := grp.Constrain(HardConstraint("kernel.name", "=", "darwin")) + if n := len(grp.Constraints); n != 1 { + t.Fatalf("expected 1 constraint, got: %d", n) + } + + // Check that the group was returned + if out != grp { + t.Fatalf("expected: %#v, got: %#v", grp, out) + } + + // Add a second constraint + grp.Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 2)) + expect := []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "darwin", + Operand: "=", + Weight: 0, + }, + &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "128000000", + Operand: ">=", + Weight: 2, + }, + } + if !reflect.DeepEqual(grp.Constraints, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp.Constraints) + } +} + +func TestTaskGroup_SetMeta(t *testing.T) { + grp := NewTaskGroup("grp1", 1) + + // Initializes an empty map + out := grp.SetMeta("foo", "bar") + if grp.Meta == nil { + t.Fatalf("should be initialized") + } + + // Check that we returned the group + if out != grp { + t.Fatalf("expect: %#v, got: %#v", grp, out) + } + + // Add a second meta k/v + grp.SetMeta("baz", "zip") + expect := map[string]string{"foo": "bar", "baz": "zip"} + if !reflect.DeepEqual(grp.Meta, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp.Meta) + } +} + +func TestTaskGroup_AddTask(t *testing.T) { + grp := NewTaskGroup("grp1", 1) + + // Add the task to the task group + out := grp.AddTask(NewTask("task1", "java")) + if n := len(grp.Tasks); n != 1 { + t.Fatalf("expected 1 task, got: %d", n) + } + + // Check that we returned the group + if out != grp { + t.Fatalf("expect: %#v, got: %#v", grp, out) + } + + // Add a second task + grp.AddTask(NewTask("task2", "exec")) + expect := []*Task{ + &Task{ + Name: "task1", + Driver: "java", + }, + &Task{ + Name: "task2", + Driver: "exec", + }, + } + if !reflect.DeepEqual(grp.Tasks, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, grp.Tasks) + } +} + +func TestTask_NewTask(t *testing.T) { + task := NewTask("task1", "exec") + expect := &Task{ + Name: "task1", + Driver: "exec", + } + if !reflect.DeepEqual(task, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, task) + } +} + +func TestTask_SetConfig(t *testing.T) { + task := NewTask("task1", "exec") + + // Initializes an empty map + out := task.SetConfig("foo", "bar") + if task.Config == nil { + t.Fatalf("should be initialized") + } + + // Check that we returned the task + if out != task { + t.Fatalf("expect: %#v, got: %#v", task, out) + } + + // Set another config value + task.SetConfig("baz", "zip") + expect := map[string]string{"foo": "bar", "baz": "zip"} + if !reflect.DeepEqual(task.Config, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, task.Config) + } +} + +func TestTask_SetMeta(t *testing.T) { + task := NewTask("task1", "exec") + + // Initializes an empty map + out := task.SetMeta("foo", "bar") + if task.Meta == nil { + t.Fatalf("should be initialized") + } + + // Check that we returned the task + if out != task { + t.Fatalf("expect: %#v, got: %#v", task, out) + } + + // Set another meta k/v + task.SetMeta("baz", "zip") + expect := map[string]string{"foo": "bar", "baz": "zip"} + if !reflect.DeepEqual(task.Meta, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, task.Meta) + } +} + +func TestTask_Require(t *testing.T) { + task := NewTask("task1", "exec") + + // Create some require resources + resources := &Resources{ + CPU: 1.25, + MemoryMB: 128, + DiskMB: 2048, + IOPS: 1024, + Networks: []*NetworkResource{ + &NetworkResource{ + CIDR: "0.0.0.0/0", + MBits: 100, + ReservedPorts: []int{80, 443}, + }, + }, + } + out := task.Require(resources) + if !reflect.DeepEqual(task.Resources, resources) { + t.Fatalf("expect: %#v, got: %#v", resources, task.Resources) + } + + // Check that we returned the task + if out != task { + t.Fatalf("expect: %#v, got: %#v", task, out) + } +} + +func TestTask_Constrain(t *testing.T) { + task := NewTask("task1", "exec") + + // Add a constraint to the task + out := task.Constrain(HardConstraint("kernel.name", "=", "darwin")) + if n := len(task.Constraints); n != 1 { + t.Fatalf("expected 1 constraint, got: %d", n) + } + + // Check that the task was returned + if out != task { + t.Fatalf("expected: %#v, got: %#v", task, out) + } + + // Add a second constraint + task.Constrain(SoftConstraint("memory.totalbytes", ">=", "128000000", 2)) + expect := []*Constraint{ + &Constraint{ + Hard: true, + LTarget: "kernel.name", + RTarget: "darwin", + Operand: "=", + Weight: 0, + }, + &Constraint{ + Hard: false, + LTarget: "memory.totalbytes", + RTarget: "128000000", + Operand: ">=", + Weight: 2, + }, + } + if !reflect.DeepEqual(task.Constraints, expect) { + t.Fatalf("expect: %#v, got: %#v", expect, task.Constraints) + } +} From fae5a396668acd8d586cee4df6a20664a305e91b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 9 Sep 2015 18:39:24 -0700 Subject: [PATCH 22/22] api: support force-eval --- api/jobs.go | 10 ++++++++++ api/jobs_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/api/jobs.go b/api/jobs.go index 36b2898d5..6898b6278 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -82,6 +82,16 @@ func (j *Jobs) Delete(jobID string, q *WriteOptions) (*WriteMeta, error) { return wm, nil } +// ForceEvaluate is used to force-evaluate an existing job. +func (j *Jobs) ForceEvaluate(jobID string, q *WriteOptions) (string, *WriteMeta, error) { + var resp registerJobResponse + wm, err := j.client.write("/v1/job/"+jobID+"/evaluate", nil, &resp, q) + if err != nil { + return "", nil, err + } + return resp.EvalID, wm, nil +} + // Job is used to serialize a job. type Job struct { ID string diff --git a/api/jobs_test.go b/api/jobs_test.go index 5ff3e5166..8b9d1366c 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -179,6 +179,45 @@ func TestJobs_Delete(t *testing.T) { } } +func TestJobs_ForceEvaluate(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.ForceEvaluate("job1", nil) + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected not found error, got: %#v", err) + } + + // Create a new job + _, wm, err := jobs.Register(testJob(), nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Try force-eval again + evalID, wm, err := jobs.ForceEvaluate("job1", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Retrieve the evals and see if we get a matching one + evals, qm, err := jobs.Evaluations("job1", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + for _, eval := range evals { + if eval.ID == evalID { + return + } + } + t.Fatalf("evaluation %q missing", evalID) +} + func TestJobs_NewBatchJob(t *testing.T) { job := NewBatchJob("job1", "myjob", 5) expect := &Job{