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.
pull/3622/head
Frank Schroeder 2017-10-26 14:24:42 +02:00
parent b31cfaaf2a
commit ca9aac746f
No known key found for this signature in database
GPG Key ID: 4D65C6EAEC87DECD
5 changed files with 215 additions and 19 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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/"},

View File

@ -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.