From ca9aac746f23e77797492b5946845cb5b6bdca38 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 26 Oct 2017 14:24:42 +0200 Subject: [PATCH 1/5] agent: add /v1/coordianate/node/:node endpoint This patch adds a /v1/coordinate/node/:node endpoint to get the network coordinates for a single node in the network. Since Consul Enterprise supports network segments it is still possible to receive mutiple entries for a single node - one per segment. --- agent/coordinate_endpoint.go | 68 +++++++++++----- agent/coordinate_endpoint_test.go | 109 ++++++++++++++++++++++++++ agent/http.go | 2 + agent/http_test.go | 1 + website/source/api/coordinate.html.md | 54 ++++++++++++- 5 files changed, 215 insertions(+), 19 deletions(-) diff --git a/agent/coordinate_endpoint.go b/agent/coordinate_endpoint.go index e9ac566d95..8dd944393c 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,53 @@ 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"}} + } + + args := structs.DCSpecificRequest{} + 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.ListNodes", &args, &out); err != nil { + sort.Sort(&sorter{out.Coordinates}) + return nil, err + } + + node := strings.TrimPrefix(req.URL.Path, "/v1/coordinate/node/") + return filterCoordinates(req, node, out.Coordinates), nil +} + +func filterCoordinates(req *http.Request, node string, 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 node != "" && c.Node != node { + continue + } + 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..74da244c08 100644 --- a/agent/coordinate_endpoint_test.go +++ b/agent/coordinate_endpoint_test.go @@ -140,3 +140,112 @@ 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 an empty list is non-nil. + 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 coordinates == nil || len(coordinates) != 0 { + t.Fatalf("bad: %v", coordinates) + } + + // 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 and sorted correctly. + 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) + } + + coordinates = obj.(structs.Coordinates) + if len(coordinates) != 0 { + t.Fatalf("bad: %v", coordinates) + } + + // 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) + } + + coordinates = obj.(structs.Coordinates) + if len(coordinates) != 0 { + t.Fatalf("bad: %v", coordinates) + } +} 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/website/source/api/coordinate.html.md b/website/source/api/coordinate.html.md index f92b8601d7..e3d87fcab5 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. @@ -122,3 +122,55 @@ $ 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 all nodes in a given +datacenter. + +| 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. + +### 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. From 5589eadcf52eaa0a70f1babb7c5dfa159edd05ab Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Thu, 26 Oct 2017 19:16:40 -0700 Subject: [PATCH 2/5] Added Coordinate.Node rpc endpoint and client api method --- agent/consul/coordinate_endpoint.go | 42 +++++++ agent/consul/coordinate_endpoint_test.go | 154 ++++++++++++++++++++++- agent/consul/rtt.go | 10 +- agent/consul/state/coordinate.go | 10 +- agent/consul/state/coordinate_test.go | 10 +- agent/coordinate_endpoint.go | 6 +- api/coordinate.go | 21 ++++ api/coordinate_test.go | 19 +++ 8 files changed, 251 insertions(+), 21 deletions(-) diff --git a/agent/consul/coordinate_endpoint.go b/agent/consul/coordinate_endpoint.go index a001fad7b0..abf255fbcc 100644 --- a/agent/consul/coordinate_endpoint.go +++ b/agent/consul/coordinate_endpoint.go @@ -200,3 +200,45 @@ func (c *Coordinate) ListNodes(args *structs.DCSpecificRequest, reply *structs.I return nil }) } + +// ListNodes returns the list of nodes with their raw network coordinates (if no +// coordinates are available for a node it won't appear in this list). +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 { + // 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 + } + } + + 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..5f23aeb2cb 100644 --- a/agent/consul/coordinate_endpoint_test.go +++ b/agent/consul/coordinate_endpoint_test.go @@ -86,13 +86,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 +107,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 +116,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 +155,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) } @@ -502,3 +502,147 @@ 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"} + 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) + } + } + + // 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"} + 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) + } + } + + // Send an update for the first node. This should go through since we + // don't have version 8 ACLs enforced yet. + req := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "node1", + Coord: generateRandomCoordinate(), + } + var out struct{} + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Now turn on version 8 enforcement and try again. + s1.config.ACLEnforceVersion8 = true + err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + + // Create an ACL that can write to the node. + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: ` +node "node1" { + policy = "write" +} +`, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var id string + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &arg, &id); err != nil { + t.Fatalf("err: %v", err) + } + + // With the token, it should now go through. + req.Token = id + if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // But it should be blocked for the other node. + req.Node = "node2" + err = msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("err: %v", err) + } +} 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..68087b1cb1 100644 --- a/agent/consul/state/coordinate.go +++ b/agent/consul/state/coordinate.go @@ -42,21 +42,25 @@ 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() + // Get the table index. + idx := 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 idx, 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..dcc59c864b 100644 --- a/agent/consul/state/coordinate_test.go +++ b/agent/consul/state/coordinate_test.go @@ -42,7 +42,7 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { } verify.Values(t, "", all, structs.Coordinates{}) - coords, err := s.Coordinate("nope") + _, coords, err := s.Coordinate("nope", nil) if err != nil { t.Fatalf("err: %s", err) } @@ -102,7 +102,7 @@ func TestStateStore_Coordinate_Updates(t *testing.T) { // Also verify the per-node coordinate interface. for _, update := range updates { - coords, err := s.Coordinate(update.Node) + _, coords, err := s.Coordinate(update.Node, nil) if err != nil { t.Fatalf("err: %s", err) } @@ -133,7 +133,7 @@ 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) + _, coords, err := s.Coordinate(update.Node, nil) if err != nil { t.Fatalf("err: %s", err) } @@ -188,7 +188,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 +204,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 8dd944393c..ade8b582a9 100644 --- a/agent/coordinate_endpoint.go +++ b/agent/coordinate_endpoint.go @@ -96,19 +96,19 @@ func (s *HTTPServer) CoordinateNode(resp http.ResponseWriter, req *http.Request) return nil, MethodNotAllowedError{req.Method, []string{"GET"}} } - args := structs.DCSpecificRequest{} + 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.ListNodes", &args, &out); err != nil { + if err := s.agent.RPC("Coordinate.Node", &args, &out); err != nil { sort.Sort(&sorter{out.Coordinates}) return nil, err } - node := strings.TrimPrefix(req.URL.Path, "/v1/coordinate/node/") return filterCoordinates(req, node, out.Coordinates), nil } 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..a91d1869b1 100644 --- a/api/coordinate_test.go +++ b/api/coordinate_test.go @@ -42,3 +42,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 { + 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. + }) +} From 1e3b0d441bd047f68641053160a3552f77b58b8d Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 31 Oct 2017 13:34:49 -0700 Subject: [PATCH 3/5] Factor out registerNodes function --- agent/consul/coordinate_endpoint.go | 3 +- agent/consul/coordinate_endpoint_test.go | 77 +++++++++--------------- agent/consul/state/coordinate.go | 5 +- 3 files changed, 30 insertions(+), 55 deletions(-) diff --git a/agent/consul/coordinate_endpoint.go b/agent/consul/coordinate_endpoint.go index abf255fbcc..5591122217 100644 --- a/agent/consul/coordinate_endpoint.go +++ b/agent/consul/coordinate_endpoint.go @@ -201,8 +201,7 @@ func (c *Coordinate) ListNodes(args *structs.DCSpecificRequest, reply *structs.I }) } -// ListNodes returns the list of nodes with their raw network coordinates (if no -// coordinates are available for a node it won't appear in this list). +// 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 diff --git a/agent/consul/coordinate_endpoint_test.go b/agent/consul/coordinate_endpoint_test.go index 5f23aeb2cb..d697f92348 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. @@ -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. @@ -515,16 +492,8 @@ func TestCoordinate_Node(t *testing.T) { // Register some 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 := 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 each node. @@ -582,16 +551,8 @@ func TestCoordinate_Node_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 @@ -646,3 +607,19 @@ node "node1" { 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/state/coordinate.go b/agent/consul/state/coordinate.go index 68087b1cb1..c810a8151f 100644 --- a/agent/consul/state/coordinate.go +++ b/agent/consul/state/coordinate.go @@ -46,8 +46,7 @@ func (s *Store) Coordinate(node string, ws memdb.WatchSet) (uint64, lib.Coordina tx := s.db.Txn(false) defer tx.Abort() - // Get the table index. - idx := maxIndexTxn(tx, "coordinates") + tableIdx := maxIndexTxn(tx, "coordinates") iter, err := tx.Get("coordinates", "node", node) if err != nil { @@ -60,7 +59,7 @@ func (s *Store) Coordinate(node string, ws memdb.WatchSet) (uint64, lib.Coordina coord := raw.(*structs.Coordinate) results[coord.Segment] = coord.Coord } - return idx, results, nil + return tableIdx, results, nil } // Coordinates queries for all nodes with coordinates. From b0536a96cc74d73ddbea8abdec95345efae2b173 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 31 Oct 2017 15:08:14 -0700 Subject: [PATCH 4/5] Fill out the tests around coordinate/node functionality --- agent/consul/coordinate_endpoint.go | 8 +---- agent/consul/coordinate_endpoint_test.go | 41 +++++++++++++++++------- agent/consul/state/coordinate_test.go | 35 ++++++++++++++++---- agent/coordinate_endpoint.go | 15 +++++---- agent/coordinate_endpoint_test.go | 22 +++++-------- api/coordinate_test.go | 4 ++- website/source/api/coordinate.html.md | 13 ++++++-- 7 files changed, 90 insertions(+), 48 deletions(-) diff --git a/agent/consul/coordinate_endpoint.go b/agent/consul/coordinate_endpoint.go index 5591122217..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 } @@ -213,10 +210,7 @@ func (c *Coordinate) Node(args *structs.NodeSpecificRequest, reply *structs.Inde 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) { + if !rule.NodeRead(args.Node) { return acl.ErrPermissionDenied } } diff --git a/agent/consul/coordinate_endpoint_test.go b/agent/consul/coordinate_endpoint_test.go index d697f92348..58eb13fabe 100644 --- a/agent/consul/coordinate_endpoint_test.go +++ b/agent/consul/coordinate_endpoint_test.go @@ -555,27 +555,44 @@ func TestCoordinate_Node_ACLDeny(t *testing.T) { t.Fatal(err) } - // Send an update for the first node. This should go through since we - // don't have version 8 ACLs enforced yet. + coord := generateRandomCoordinate() req := structs.CoordinateUpdateRequest{ Datacenter: "dc1", Node: "node1", - Coord: generateRandomCoordinate(), + 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.Update", &req, &out) + err := msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp) if !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } - // Create an ACL that can write to the node. - arg := structs.ACLRequest{ + // Create an ACL that can read from the node. + aclReq := structs.ACLRequest{ Datacenter: "dc1", Op: structs.ACLSet, ACL: structs.ACL{ @@ -583,26 +600,26 @@ func TestCoordinate_Node_ACLDeny(t *testing.T) { Type: structs.ACLTypeClient, Rules: ` node "node1" { - policy = "write" + policy = "read" } `, }, WriteRequest: structs.WriteRequest{Token: "root"}, } var id string - if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &arg, &id); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &aclReq, &id); err != nil { t.Fatalf("err: %v", err) } // With the token, it should now go through. - req.Token = id - if err := msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out); err != nil { + 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. - req.Node = "node2" - err = msgpackrpc.CallWithCodec(codec, "Coordinate.Update", &req, &out) + arg.Node = "node2" + err = msgpackrpc.CallWithCodec(codec, "Coordinate.Node", &arg, &resp) if !acl.IsErrPermissionDenied(err) { t.Fatalf("err: %v", err) } diff --git a/agent/consul/state/coordinate_test.go b/agent/consul/state/coordinate_test.go index dcc59c864b..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", nil) + 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, nil) + 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, nil) + 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, } diff --git a/agent/coordinate_endpoint.go b/agent/coordinate_endpoint.go index ade8b582a9..deaad933e4 100644 --- a/agent/coordinate_endpoint.go +++ b/agent/coordinate_endpoint.go @@ -86,7 +86,7 @@ func (s *HTTPServer) CoordinateNodes(resp http.ResponseWriter, req *http.Request return nil, err } - return filterCoordinates(req, "", out.Coordinates), nil + return filterCoordinates(req, out.Coordinates), nil } // CoordinateNode returns the LAN node in the given datacenter, along with @@ -109,10 +109,16 @@ func (s *HTTPServer) CoordinateNode(resp http.ResponseWriter, req *http.Request) return nil, err } - return filterCoordinates(req, node, out.Coordinates), nil + result := filterCoordinates(req, out.Coordinates) + if len(result) == 0 { + resp.WriteHeader(http.StatusNotFound) + return nil, nil + } + + return result, nil } -func filterCoordinates(req *http.Request, node string, in structs.Coordinates) structs.Coordinates { +func filterCoordinates(req *http.Request, in structs.Coordinates) structs.Coordinates { out := structs.Coordinates{} if in == nil { @@ -126,9 +132,6 @@ func filterCoordinates(req *http.Request, node string, in structs.Coordinates) s } for _, c := range in { - if node != "" && c.Node != node { - continue - } if filterBySegment && c.Segment != segment { continue } diff --git a/agent/coordinate_endpoint_test.go b/agent/coordinate_endpoint_test.go index 74da244c08..c7005e926d 100644 --- a/agent/coordinate_endpoint_test.go +++ b/agent/coordinate_endpoint_test.go @@ -146,17 +146,15 @@ func TestCoordinate_Node(t *testing.T) { a := NewTestAgent(t.Name(), "") defer a.Shutdown() - // Make sure an empty list is non-nil. + // 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) } - - coordinates := obj.(structs.Coordinates) - if coordinates == nil || len(coordinates) != 0 { - t.Fatalf("bad: %v", coordinates) + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) } // Register the nodes. @@ -204,7 +202,7 @@ func TestCoordinate_Node(t *testing.T) { t.Fatalf("err: %v", err) } - coordinates = obj.(structs.Coordinates) + coordinates := obj.(structs.Coordinates) if len(coordinates) != 1 || coordinates[0].Node != "foo" { t.Fatalf("bad: %v", coordinates) @@ -217,10 +215,8 @@ func TestCoordinate_Node(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - - coordinates = obj.(structs.Coordinates) - if len(coordinates) != 0 { - t.Fatalf("bad: %v", coordinates) + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) } // Filter on a real node segment @@ -243,9 +239,7 @@ func TestCoordinate_Node(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - - coordinates = obj.(structs.Coordinates) - if len(coordinates) != 0 { - t.Fatalf("bad: %v", coordinates) + if resp.Code != http.StatusNotFound { + t.Fatalf("bad: %v", resp.Code) } } diff --git a/api/coordinate_test.go b/api/coordinate_test.go index a91d1869b1..aae1ddf1aa 100644 --- a/api/coordinate_test.go +++ b/api/coordinate_test.go @@ -3,6 +3,8 @@ package api import ( "testing" + "strings" + "github.com/hashicorp/consul/testutil/retry" ) @@ -51,7 +53,7 @@ func TestAPI_CoordinateNode(t *testing.T) { coordinate := c.Coordinate() retry.Run(t, func(r *retry.R) { _, _, err := coordinate.Node(s.Config.NodeName, nil) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "Unexpected response code: 404") { r.Fatal(err) } diff --git a/website/source/api/coordinate.html.md b/website/source/api/coordinate.html.md index e3d87fcab5..a1bfecb606 100644 --- a/website/source/api/coordinate.html.md +++ b/website/source/api/coordinate.html.md @@ -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 @@ -125,8 +130,7 @@ segment. ## Read LAN Coordinates for a node -This endpoint returns the LAN network coordinates for all nodes in a given -datacenter. +This endpoint returns the LAN network coordinates for the given node. | Method | Path | Produces | | ------ | ---------------------------- | -------------------------- | @@ -146,6 +150,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 From 021b2849c52192dd8564233f74735238b45800e1 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Wed, 1 Nov 2017 11:25:33 -0700 Subject: [PATCH 5/5] Remove redundant lines from coordinate test --- agent/coordinate_endpoint.go | 1 - agent/coordinate_endpoint_test.go | 2 +- api/coordinate_test.go | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/agent/coordinate_endpoint.go b/agent/coordinate_endpoint.go index deaad933e4..4e4ea12960 100644 --- a/agent/coordinate_endpoint.go +++ b/agent/coordinate_endpoint.go @@ -105,7 +105,6 @@ func (s *HTTPServer) CoordinateNode(resp http.ResponseWriter, req *http.Request) var out structs.IndexedCoordinates defer setMeta(resp, &out.QueryMeta) if err := s.agent.RPC("Coordinate.Node", &args, &out); err != nil { - sort.Sort(&sorter{out.Coordinates}) return nil, err } diff --git a/agent/coordinate_endpoint_test.go b/agent/coordinate_endpoint_test.go index c7005e926d..b34197c4a9 100644 --- a/agent/coordinate_endpoint_test.go +++ b/agent/coordinate_endpoint_test.go @@ -194,7 +194,7 @@ func TestCoordinate_Node(t *testing.T) { } time.Sleep(300 * time.Millisecond) - // Query back and check the nodes are present and sorted correctly. + // 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) diff --git a/api/coordinate_test.go b/api/coordinate_test.go index aae1ddf1aa..cf0f82c331 100644 --- a/api/coordinate_test.go +++ b/api/coordinate_test.go @@ -1,9 +1,8 @@ package api import ( - "testing" - "strings" + "testing" "github.com/hashicorp/consul/testutil/retry" )