diff --git a/api/nodes.go b/api/nodes.go index 4f3d66489..d72d2753f 100644 --- a/api/nodes.go +++ b/api/nodes.go @@ -61,6 +61,18 @@ func (n *Nodes) Allocations(nodeID string, q *QueryOptions) ([]*Allocation, *Que return resp, qm, nil } +// ClientAllocations is used to return a lightweight list of allocations associated with a node. +// It is primarily used by the client in order to determine which allocations actually need +// an update. +func (n *Nodes) ClientAllocations(nodeID string, q *QueryOptions) (map[string]uint64, *QueryMeta, error) { + var resp map[string]uint64 + qm, err := n.client.query("/v1/node/"+nodeID+"/clientallocations", &resp, q) + if err != nil { + return nil, nil, err + } + return resp, qm, nil +} + // ForceEvaluate is used to force-evaluate an existing node. func (n *Nodes) ForceEvaluate(nodeID string, q *WriteOptions) (string, *WriteMeta, error) { var resp nodeEvalResponse diff --git a/api/nodes_test.go b/api/nodes_test.go index 0a5732176..81fb30db7 100644 --- a/api/nodes_test.go +++ b/api/nodes_test.go @@ -207,6 +207,24 @@ func TestNodes_Allocations(t *testing.T) { } } +func TestNodes_ClientAllocations(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + nodes := c.Nodes() + + // Looking up by a non-existent node returns nothing. We + // don't check the index here because it's possible the node + // has already registered, in which case we will get a non- + // zero result anyways. + allocs, _, err := nodes.ClientAllocations("nope", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if n := len(allocs); n != 0 { + t.Fatalf("expected 0 allocs, got: %d", n) + } +} + func TestNodes_ForceEvaluate(t *testing.T) { c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) { c.DevMode = true diff --git a/command/agent/node_endpoint.go b/command/agent/node_endpoint.go index c872912e0..f249212c3 100644 --- a/command/agent/node_endpoint.go +++ b/command/agent/node_endpoint.go @@ -36,6 +36,9 @@ func (s *HTTPServer) NodeSpecificRequest(resp http.ResponseWriter, req *http.Req case strings.HasSuffix(path, "/evaluate"): nodeName := strings.TrimSuffix(path, "/evaluate") return s.nodeForceEvaluate(resp, req, nodeName) + case strings.HasSuffix(path, "/clientallocations"): + nodeName := strings.TrimSuffix(path, "/clientallocations") + return s.nodeClientAllocations(resp, req, nodeName) case strings.HasSuffix(path, "/allocations"): nodeName := strings.TrimSuffix(path, "/allocations") return s.nodeAllocations(resp, req, nodeName) @@ -89,6 +92,27 @@ func (s *HTTPServer) nodeAllocations(resp http.ResponseWriter, req *http.Request return out.Allocs, nil } +func (s *HTTPServer) nodeClientAllocations(resp http.ResponseWriter, req *http.Request, + nodeID string) (interface{}, error) { + if req.Method != "GET" { + return nil, CodedError(405, ErrInvalidMethod) + } + args := structs.NodeSpecificRequest{ + NodeID: nodeID, + } + if s.parse(resp, req, &args.Region, &args.QueryOptions) { + return nil, nil + } + + var out structs.NodeClientAllocsResponse + if err := s.agent.RPC("Node.GetClientAllocs", &args, &out); err != nil { + return nil, err + } + + setMeta(resp, &out.QueryMeta) + return out.Allocs, nil +} + func (s *HTTPServer) nodeToggleDrain(resp http.ResponseWriter, req *http.Request, nodeID string) (interface{}, error) { if req.Method != "PUT" && req.Method != "POST" { diff --git a/command/agent/node_endpoint_test.go b/command/agent/node_endpoint_test.go index a63739a18..d4ec061b9 100644 --- a/command/agent/node_endpoint_test.go +++ b/command/agent/node_endpoint_test.go @@ -214,6 +214,60 @@ func TestHTTP_NodeAllocations(t *testing.T) { }) } +func TestHTTP_NodeClientAllocations(t *testing.T) { + httpTest(t, nil, func(s *TestServer) { + // Create the job + node := mock.Node() + args := structs.NodeRegisterRequest{ + Node: node, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.NodeUpdateResponse + if err := s.Agent.RPC("Node.Register", &args, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Directly manipulate the state + state := s.Agent.server.State() + alloc1 := mock.Alloc() + alloc1.NodeID = node.ID + err := state.UpsertAllocs(1000, []*structs.Allocation{alloc1}) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/node/"+node.ID+"/clientallocations", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + respW := httptest.NewRecorder() + + // Make the request + obj, err := s.Server.NodeSpecificRequest(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") + } + if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { + t.Fatalf("missing known leader") + } + if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { + t.Fatalf("missing last contact") + } + + // Check the node + allocs := obj.(map[string]uint64) + if len(allocs) != 1 || allocs[alloc1.ID] != 1000 { + t.Fatalf("bad: %#v", allocs) + } + }) +} + func TestHTTP_NodeDrain(t *testing.T) { httpTest(t, nil, func(s *TestServer) { // Create the node