diff --git a/agent/consul/coordinate_endpoint.go b/agent/consul/coordinate_endpoint.go index a001fad7b0..f683e48742 100644 --- a/agent/consul/coordinate_endpoint.go +++ b/agent/consul/coordinate_endpoint.go @@ -139,9 +139,6 @@ func (c *Coordinate) Update(args *structs.CoordinateUpdateRequest, reply *struct return err } if rule != nil && c.srv.config.ACLEnforceVersion8 { - // We don't enforce the sentinel policy here, since at this time - // sentinel only applies to creating or updating node or service - // info, not updating coordinates. if !rule.NodeWrite(args.Node, nil) { return acl.ErrPermissionDenied } @@ -200,3 +197,41 @@ func (c *Coordinate) ListNodes(args *structs.DCSpecificRequest, reply *structs.I return nil }) } + +// Node returns the raw coordinates for a single node. +func (c *Coordinate) Node(args *structs.NodeSpecificRequest, reply *structs.IndexedCoordinates) error { + if done, err := c.srv.forward("Coordinate.Node", args, args, reply); done { + return err + } + + // Fetch the ACL token, if any, and enforce the node policy if enabled. + rule, err := c.srv.resolveToken(args.Token) + if err != nil { + return err + } + if rule != nil && c.srv.config.ACLEnforceVersion8 { + if !rule.NodeRead(args.Node) { + return acl.ErrPermissionDenied + } + } + + return c.srv.blockingQuery(&args.QueryOptions, + &reply.QueryMeta, + func(ws memdb.WatchSet, state *state.Store) error { + index, nodeCoords, err := state.Coordinate(args.Node, ws) + if err != nil { + return err + } + + var coords structs.Coordinates + for segment, coord := range nodeCoords { + coords = append(coords, &structs.Coordinate{ + Node: args.Node, + Segment: segment, + Coord: coord, + }) + } + reply.Index, reply.Coordinates = index, coords + return nil + }) +} diff --git a/agent/consul/coordinate_endpoint_test.go b/agent/consul/coordinate_endpoint_test.go index ba9ded9a7f..58eb13fabe 100644 --- a/agent/consul/coordinate_endpoint_test.go +++ b/agent/consul/coordinate_endpoint_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "math/rand" + "net/rpc" "os" "strings" "testing" @@ -50,16 +51,8 @@ func TestCoordinate_Update(t *testing.T) { // Register some nodes. nodes := []string{"node1", "node2"} - for _, node := range nodes { - req := structs.RegisterRequest{ - Datacenter: "dc1", - Node: node, - Address: "127.0.0.1", - } - var reply struct{} - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { - t.Fatalf("err: %v", err) - } + if err := registerNodes(nodes, codec); err != nil { + t.Fatal(err) } // Send an update for the first node. @@ -86,13 +79,13 @@ func TestCoordinate_Update(t *testing.T) { // Make sure the updates did not yet apply because the update period // hasn't expired. state := s1.fsm.State() - c, err := state.Coordinate("node1") + _, c, err := state.Coordinate("node1", nil) if err != nil { t.Fatalf("err: %v", err) } verify.Values(t, "", c, lib.CoordinateSet{}) - c, err = state.Coordinate("node2") + _, c, err = state.Coordinate("node2", nil) if err != nil { t.Fatalf("err: %v", err) } @@ -107,7 +100,7 @@ func TestCoordinate_Update(t *testing.T) { // Wait a while and the updates should get picked up. time.Sleep(3 * s1.config.CoordinateUpdatePeriod) - c, err = state.Coordinate("node1") + _, c, err = state.Coordinate("node1", nil) if err != nil { t.Fatalf("err: %v", err) } @@ -116,7 +109,7 @@ func TestCoordinate_Update(t *testing.T) { } verify.Values(t, "", c, expected) - c, err = state.Coordinate("node2") + _, c, err = state.Coordinate("node2", nil) if err != nil { t.Fatalf("err: %v", err) } @@ -155,7 +148,7 @@ func TestCoordinate_Update(t *testing.T) { time.Sleep(3 * s1.config.CoordinateUpdatePeriod) numDropped := 0 for i := 0; i < spamLen; i++ { - c, err = state.Coordinate(fmt.Sprintf("bogusnode%d", i)) + _, c, err = state.Coordinate(fmt.Sprintf("bogusnode%d", i), nil) if err != nil { t.Fatalf("err: %v", err) } @@ -201,16 +194,8 @@ func TestCoordinate_Update_ACLDeny(t *testing.T) { // Register some nodes. nodes := []string{"node1", "node2"} - for _, node := range nodes { - req := structs.RegisterRequest{ - Datacenter: "dc1", - Node: node, - Address: "127.0.0.1", - } - var reply struct{} - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { - t.Fatalf("err: %v", err) - } + if err := registerNodes(nodes, codec); err != nil { + t.Fatal(err) } // Send an update for the first node. This should go through since we @@ -309,16 +294,8 @@ func TestCoordinate_ListNodes(t *testing.T) { // Register some nodes. nodes := []string{"foo", "bar", "baz"} - for _, node := range nodes { - req := structs.RegisterRequest{ - Datacenter: "dc1", - Node: node, - Address: "127.0.0.1", - } - var reply struct{} - if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { - t.Fatalf("err: %v", err) - } + if err := registerNodes(nodes, codec); err != nil { + t.Fatal(err) } // Send coordinate updates for a few nodes. @@ -502,3 +479,164 @@ node "foo" { t.Fatalf("bad: %#v", resp.Coordinates) } } + +func TestCoordinate_Node(t *testing.T) { + t.Parallel() + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + codec := rpcClient(t, s1) + defer codec.Close() + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Register some nodes. + nodes := []string{"foo", "bar"} + if err := registerNodes(nodes, codec); err != nil { + t.Fatal(err) + } + + // Send coordinate updates for each node. + arg1 := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "foo", + Coord: generateRandomCoordinate(), + } + var out struct{} + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &arg1, &out); err != nil { + t.Fatalf("err: %v", err) + } + + arg2 := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "bar", + Coord: generateRandomCoordinate(), + } + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &arg2, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Now query back for a specific node (make sure we only get coordinates for foo). + retry.Run(t, func(r *retry.R) { + arg := structs.NodeSpecificRequest{ + Node: "foo", + Datacenter: "dc1", + } + resp := structs.IndexedCoordinates{} + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp); err != nil { + r.Fatalf("err: %v", err) + } + if len(resp.Coordinates) != 1 || + resp.Coordinates[0].Node != "foo" { + r.Fatalf("bad: %v", resp.Coordinates) + } + verify.Values(t, "", resp.Coordinates[0].Coord, arg1.Coord) // foo + }) +} + +func TestCoordinate_Node_ACLDeny(t *testing.T) { + t.Parallel() + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + c.ACLEnforceVersion8 = false + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Register some nodes. + nodes := []string{"node1", "node2"} + if err := registerNodes(nodes, codec); err != nil { + t.Fatal(err) + } + + coord := generateRandomCoordinate() + req := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "node1", + Coord: coord, + } + var out struct{} + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Try a read for the first node. This should go through since we + // don't have version 8 ACLs enforced yet. + arg := structs.NodeSpecificRequest{ + Node: "node1", + Datacenter: "dc1", + } + resp := structs.IndexedCoordinates{} + retry.Run(t, func(r *retry.R) { + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp); err != nil { + r.Fatalf("err: %v", err) + } + if len(resp.Coordinates) != 1 || + resp.Coordinates[0].Node != "node1" { + r.Fatalf("bad: %v", resp.Coordinates) + } + verify.Values(t, "", resp.Coordinates[0].Coord, coord) + }) + + // Now turn on version 8 enforcement and try again. + s1.config.ACLEnforceVersion8 = true + err := msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + + // Create an ACL that can read from the node. + aclReq := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: ` +node "node1" { + policy = "read" +} +`, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var id string + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &aclReq, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // With the token, it should now go through. + arg.Token = id + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // But it should be blocked for the other node. + arg.Node = "node2" + err = msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("err: %v", err) + } +} + +func registerNodes(nodes []string, codec rpc.ClientCodec) error { + for _, node := range nodes { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: node, + Address: "127.0.0.1", + } + var reply struct{} + if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { + return err + } + } + + return nil +} diff --git a/agent/consul/rtt.go b/agent/consul/rtt.go index 84a39f34dd..8bb1bcaec0 100644 --- a/agent/consul/rtt.go +++ b/agent/consul/rtt.go @@ -22,7 +22,7 @@ func (s *Server) newNodeSorter(cs lib.CoordinateSet, nodes structs.Nodes) (sort. state := s.fsm.State() vec := make([]float64, len(nodes)) for i, node := range nodes { - other, err := state.Coordinate(node.Node) + _, other, err := state.Coordinate(node.Node, nil) if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (s *Server) newServiceNodeSorter(cs lib.CoordinateSet, nodes structs.Servic state := s.fsm.State() vec := make([]float64, len(nodes)) for i, node := range nodes { - other, err := state.Coordinate(node.Node) + _, other, err := state.Coordinate(node.Node, nil) if err != nil { return nil, err } @@ -102,7 +102,7 @@ func (s *Server) newHealthCheckSorter(cs lib.CoordinateSet, checks structs.Healt state := s.fsm.State() vec := make([]float64, len(checks)) for i, check := range checks { - other, err := state.Coordinate(check.Node) + _, other, err := state.Coordinate(check.Node, nil) if err != nil { return nil, err } @@ -142,7 +142,7 @@ func (s *Server) newCheckServiceNodeSorter(cs lib.CoordinateSet, nodes structs.C state := s.fsm.State() vec := make([]float64, len(nodes)) for i, node := range nodes { - other, err := state.Coordinate(node.Node.Node) + _, other, err := state.Coordinate(node.Node.Node, nil) if err != nil { return nil, err } @@ -203,7 +203,7 @@ func (s *Server) sortNodesByDistanceFrom(source structs.QuerySource, subj interf // There won't always be coordinates for the source node. If there are // none then we can bail out because there's no meaning for the sort. state := s.fsm.State() - cs, err := state.Coordinate(source.Node) + _, cs, err := state.Coordinate(source.Node, nil) if err != nil { return err } diff --git a/agent/consul/state/coordinate.go b/agent/consul/state/coordinate.go index 83db264553..c810a8151f 100644 --- a/agent/consul/state/coordinate.go +++ b/agent/consul/state/coordinate.go @@ -42,21 +42,24 @@ func (s *Restore) Coordinates(idx uint64, updates structs.Coordinates) error { // Coordinate returns a map of coordinates for the given node, indexed by // network segment. -func (s *Store) Coordinate(node string) (lib.CoordinateSet, error) { +func (s *Store) Coordinate(node string, ws memdb.WatchSet) (uint64, lib.CoordinateSet, error) { tx := s.db.Txn(false) defer tx.Abort() + tableIdx := maxIndexTxn(tx, "coordinates") + iter, err := tx.Get("coordinates", "node", node) if err != nil { - return nil, fmt.Errorf("failed coordinate lookup: %s", err) + return 0, nil, fmt.Errorf("failed coordinate lookup: %s", err) } + ws.Add(iter.WatchCh()) results := make(lib.CoordinateSet) for raw := iter.Next(); raw != nil; raw = iter.Next() { coord := raw.(*structs.Coordinate) results[coord.Segment] = coord.Coord } - return results, nil + return tableIdx, results, nil } // Coordinates queries for all nodes with coordinates. diff --git a/agent/consul/state/coordinate_test.go b/agent/consul/state/coordinate_test.go index b126e44787..7bd9625214 100644 --- a/agent/consul/state/coordinate_test.go +++ b/agent/consul/state/coordinate_test.go @@ -42,7 +42,8 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { } verify.Values(t, "", all, structs.Coordinates{}) - coords, err := s.Coordinate("nope") + coordinateWs := memdb.NewWatchSet() + _, coords, err := s.Coordinate("nope", coordinateWs) if err != nil { t.Fatalf("err: %s", err) } @@ -63,7 +64,7 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { if err := s.CoordinateBatchUpdate(1, updates); err != nil { t.Fatalf("err: %s", err) } - if watchFired(ws) { + if watchFired(ws) || watchFired(coordinateWs) { t.Fatalf("bad") } @@ -79,13 +80,22 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { } verify.Values(t, "", all, structs.Coordinates{}) + coordinateWs = memdb.NewWatchSet() + idx, coords, err = s.Coordinate("node1", coordinateWs) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 1 { + t.Fatalf("bad index: %d", idx) + } + // Register the nodes then do the update again. testRegisterNode(t, s, 1, "node1") testRegisterNode(t, s, 2, "node2") if err := s.CoordinateBatchUpdate(3, updates); err != nil { t.Fatalf("err: %s", err) } - if !watchFired(ws) { + if !watchFired(ws) || !watchFired(coordinateWs) { t.Fatalf("bad") } @@ -101,11 +111,16 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { verify.Values(t, "", all, updates) // Also verify the per-node coordinate interface. - for _, update := range updates { - coords, err := s.Coordinate(update.Node) + nodeWs := make([]memdb.WatchSet, len(updates)) + for i, update := range updates { + nodeWs[i] = memdb.NewWatchSet() + idx, coords, err := s.Coordinate(update.Node, nodeWs[i]) if err != nil { t.Fatalf("err: %s", err) } + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } expected := lib.CoordinateSet{ "": update.Coord, } @@ -120,6 +135,11 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { if !watchFired(ws) { t.Fatalf("bad") } + for _, ws := range nodeWs { + if !watchFired(ws) { + t.Fatalf("bad") + } + } // Verify it got applied. idx, all, err = s.Coordinates(nil) @@ -133,10 +153,13 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { // And check the per-node coordinate version of the same thing. for _, update := range updates { - coords, err := s.Coordinate(update.Node) + idx, coords, err := s.Coordinate(update.Node, nil) if err != nil { t.Fatalf("err: %s", err) } + if idx != 4 { + t.Fatalf("bad index: %d", idx) + } expected := lib.CoordinateSet{ "": update.Coord, } @@ -188,7 +211,7 @@ func TestStateStore_Coordinate_Cleanup(t *testing.T) { } // Make sure it's in there. - coords, err := s.Coordinate("node1") + _, coords, err := s.Coordinate("node1", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -204,7 +227,7 @@ func TestStateStore_Coordinate_Cleanup(t *testing.T) { } // Make sure the coordinate is gone. - coords, err = s.Coordinate("node1") + _, coords, err = s.Coordinate("node1", nil) if err != nil { t.Fatalf("err: %s", err) } diff --git a/agent/coordinate_endpoint.go b/agent/coordinate_endpoint.go index e9ac566d95..4e4ea12960 100644 --- a/agent/coordinate_endpoint.go +++ b/agent/coordinate_endpoint.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "sort" + "strings" "github.com/hashicorp/consul/agent/structs" ) @@ -85,22 +86,55 @@ func (s *HTTPServer) CoordinateNodes(resp http.ResponseWriter, req *http.Request return nil, err } - // Use empty list instead of nil. - if out.Coordinates == nil { - out.Coordinates = make(structs.Coordinates, 0) - } - - // Filter by segment if applicable - if v, ok := req.URL.Query()["segment"]; ok && len(v) > 0 { - segment := v[0] - filtered := make(structs.Coordinates, 0) - for _, coord := range out.Coordinates { - if coord.Segment == segment { - filtered = append(filtered, coord) - } - } - out.Coordinates = filtered - } - - return out.Coordinates, nil + return filterCoordinates(req, out.Coordinates), nil +} + +// CoordinateNode returns the LAN node in the given datacenter, along with +// raw network coordinates. +func (s *HTTPServer) CoordinateNode(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "GET" { + return nil, MethodNotAllowedError{req.Method, []string{"GET"}} + } + + node := strings.TrimPrefix(req.URL.Path, "/v1/coordinate/node/") + args := structs.NodeSpecificRequest{Node: node} + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var out structs.IndexedCoordinates + defer setMeta(resp, &out.QueryMeta) + if err := s.agent.RPC("Coordinate.Node", &args, &out); err != nil { + return nil, err + } + + result := filterCoordinates(req, out.Coordinates) + if len(result) == 0 { + resp.WriteHeader(http.StatusNotFound) + return nil, nil + } + + return result, nil +} + +func filterCoordinates(req *http.Request, in structs.Coordinates) structs.Coordinates { + out := structs.Coordinates{} + + if in == nil { + return out + } + + segment := "" + v, filterBySegment := req.URL.Query()["segment"] + if filterBySegment && len(v) > 0 { + segment = v[0] + } + + for _, c := range in { + if filterBySegment && c.Segment != segment { + continue + } + out = append(out, c) + } + return out } diff --git a/agent/coordinate_endpoint_test.go b/agent/coordinate_endpoint_test.go index 455611ea63..b34197c4a9 100644 --- a/agent/coordinate_endpoint_test.go +++ b/agent/coordinate_endpoint_test.go @@ -140,3 +140,106 @@ func TestCoordinate_Nodes(t *testing.T) { t.Fatalf("bad: %v", coordinates) } } + +func TestCoordinate_Node(t *testing.T) { + t.Parallel() + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Make sure we get a 404 with no coordinates. + req, _ := http.NewRequest("GET", "/v1/coordinate/node/foo?dc=dc1", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.CoordinateNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) + } + + // Register the nodes. + nodes := []string{"foo", "bar"} + for _, node := range nodes { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: node, + Address: "127.0.0.1", + } + var reply struct{} + if err := a.RPC("Catalog.Register", &req, &reply); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Send some coordinates for a few nodes, waiting a little while for the + // batch update to run. + arg1 := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "foo", + Segment: "alpha", + Coord: coordinate.NewCoordinate(coordinate.DefaultConfig()), + } + var out struct{} + if err := a.RPC("Coordinate.Update", &arg1, &out); err != nil { + t.Fatalf("err: %v", err) + } + + arg2 := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "bar", + Coord: coordinate.NewCoordinate(coordinate.DefaultConfig()), + } + if err := a.RPC("Coordinate.Update", &arg2, &out); err != nil { + t.Fatalf("err: %v", err) + } + time.Sleep(300 * time.Millisecond) + + // Query back and check the nodes are present. + req, _ = http.NewRequest("GET", "/v1/coordinate/node/foo?dc=dc1", nil) + resp = httptest.NewRecorder() + obj, err = a.srv.CoordinateNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + coordinates := obj.(structs.Coordinates) + if len(coordinates) != 1 || + coordinates[0].Node != "foo" { + t.Fatalf("bad: %v", coordinates) + } + + // Filter on a nonexistant node segment + req, _ = http.NewRequest("GET", "/v1/coordinate/node/foo?segment=nope", nil) + resp = httptest.NewRecorder() + obj, err = a.srv.CoordinateNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) + } + + // Filter on a real node segment + req, _ = http.NewRequest("GET", "/v1/coordinate/node/foo?segment=alpha", nil) + resp = httptest.NewRecorder() + obj, err = a.srv.CoordinateNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + coordinates = obj.(structs.Coordinates) + if len(coordinates) != 1 || coordinates[0].Node != "foo" { + t.Fatalf("bad: %v", coordinates) + } + + // Make sure the empty filter works + req, _ = http.NewRequest("GET", "/v1/coordinate/node/foo?segment=", nil) + resp = httptest.NewRecorder() + obj, err = a.srv.CoordinateNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) + } +} diff --git a/agent/http.go b/agent/http.go index 10189517d1..0abaf42346 100644 --- a/agent/http.go +++ b/agent/http.go @@ -139,9 +139,11 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { if !s.agent.config.DisableCoordinates { handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(s.CoordinateDatacenters)) handleFuncMetrics("/v1/coordinate/nodes", s.wrap(s.CoordinateNodes)) + handleFuncMetrics("/v1/coordinate/node/", s.wrap(s.CoordinateNode)) } else { handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(coordinateDisabled)) handleFuncMetrics("/v1/coordinate/nodes", s.wrap(coordinateDisabled)) + handleFuncMetrics("/v1/coordinate/node/", s.wrap(coordinateDisabled)) } handleFuncMetrics("/v1/event/fire/", s.wrap(s.EventFire)) handleFuncMetrics("/v1/event/list", s.wrap(s.EventList)) diff --git a/agent/http_test.go b/agent/http_test.go index 3f1c70df73..517b90807c 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -350,6 +350,7 @@ func TestHTTPAPI_MethodNotAllowed(t *testing.T) { {"GET", "/v1/catalog/services"}, {"GET", "/v1/coordinate/datacenters"}, {"GET", "/v1/coordinate/nodes"}, + {"GET", "/v1/coordinate/node/"}, {"PUT", "/v1/event/fire/"}, {"GET", "/v1/event/list"}, {"GET", "/v1/health/checks/"}, diff --git a/api/coordinate.go b/api/coordinate.go index 90214e392c..42df0decaf 100644 --- a/api/coordinate.go +++ b/api/coordinate.go @@ -66,3 +66,24 @@ func (c *Coordinate) Nodes(q *QueryOptions) ([]*CoordinateEntry, *QueryMeta, err } return out, qm, nil } + +// Node is used to return the coordinates of a single in the LAN pool. +func (c *Coordinate) Node(node string, q *QueryOptions) ([]*CoordinateEntry, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/coordinate/node/"+node) + r.setQueryOptions(q) + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out []*CoordinateEntry + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} diff --git a/api/coordinate_test.go b/api/coordinate_test.go index f41756b57a..cf0f82c331 100644 --- a/api/coordinate_test.go +++ b/api/coordinate_test.go @@ -1,6 +1,7 @@ package api import ( + "strings" "testing" "github.com/hashicorp/consul/testutil/retry" @@ -42,3 +43,22 @@ func TestAPI_CoordinateNodes(t *testing.T) { // get an error. }) } + +func TestAPI_CoordinateNode(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + coordinate := c.Coordinate() + retry.Run(t, func(r *retry.R) { + _, _, err := coordinate.Node(s.Config.NodeName, nil) + if err != nil && !strings.Contains(err.Error(), "Unexpected response code: 404") { + r.Fatal(err) + } + + // There's not a good way to populate coordinates without + // waiting for them to calculate and update, so the best + // we can do is call the endpoint and make sure we don't + // get an error. + }) +} diff --git a/website/source/api/coordinate.html.md b/website/source/api/coordinate.html.md index f92b8601d7..a1bfecb606 100644 --- a/website/source/api/coordinate.html.md +++ b/website/source/api/coordinate.html.md @@ -71,7 +71,7 @@ In **Consul Enterprise**, this will include coordinates for user-added network areas as well, as indicated by the `AreaID`. Coordinates are only compatible within the same area. -## Read LAN Coordinates +## Read LAN Coordinates for all nodes This endpoint returns the LAN network coordinates for all nodes in a given datacenter. @@ -94,6 +94,11 @@ The table below shows this endpoint's support for - `dc` `(string: "")` - Specifies the datacenter to query. This will default to the datacenter of the agent being queried. This is specified as part of the URL as a query parameter. +- `segment` `(string: "")` - (Enterprise-only) Specifies the segment to list members for. + If left blank, this will query for the default segment when connecting to a server and + the agent's own segment when connecting to a client (clients can only be part of one + network segment). When querying a server, setting this to the special string `_all` + will show members in all segments. ### Sample Request @@ -122,3 +127,59 @@ $ curl \ In **Consul Enterprise**, this may include multiple coordinates for the same node, each marked with a different `Segment`. Coordinates are only compatible within the same segment. + +## Read LAN Coordinates for a node + +This endpoint returns the LAN network coordinates for the given node. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/coordinate/node/:node` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), +[consistency modes](/api/index.html#consistency-modes), and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------ | +| `YES` | `all` | `node:read` | + +### Parameters + +- `dc` `(string: "")` - Specifies the datacenter to query. This will default to + the datacenter of the agent being queried. This is specified as part of the + URL as a query parameter. +- `segment` `(string: "")` - (Enterprise-only) Specifies the segment to list members for. + If left blank, this will query for the default segment when connecting to a server and + the agent's own segment when connecting to a client (clients can only be part of one + network segment). When querying a server, setting this to the special string `_all` + will show members in all segments. + +### Sample Request + +```text +$ curl \ + https://consul.rocks/v1/coordinate/node/agent-one +``` + +### Sample Response + +```json +[ + { + "Node": "agent-one", + "Segment": "", + "Coord": { + "Adjustment": 0, + "Error": 1.5, + "Height": 0, + "Vec": [0, 0, 0, 0, 0, 0, 0, 0] + } + } +] +``` + +In **Consul Enterprise**, this may include multiple coordinates for the same node, +each marked with a different `Segment`. Coordinates are only compatible within the same +segment.