From 787f946f8d43994150cc91b4181f0b2eb01cd53d Mon Sep 17 00:00:00 2001 From: James Phillips Date: Thu, 15 Oct 2015 21:42:09 -0700 Subject: [PATCH] Adds support for coordinates to client API. --- api/api.go | 9 ++++++ api/api_test.go | 4 +++ api/coordinate.go | 66 ++++++++++++++++++++++++++++++++++++++++++ api/coordinate_test.go | 54 ++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 api/coordinate.go create mode 100644 api/coordinate_test.go diff --git a/api/api.go b/api/api.go index 7123c8a687..6736aecd2e 100644 --- a/api/api.go +++ b/api/api.go @@ -44,6 +44,12 @@ type QueryOptions struct { // Token is used to provide a per-request ACL token // which overrides the agent's default token. Token string + + // Near is used to provide a node name that will sort the results + // in ascending order based on the estimated round trip time from + // that node. Setting this to "_agent" will use the agent's node + // for the sort. + Near string } // WriteOptions are used to parameterize a write @@ -250,6 +256,9 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.Token != "" { r.params.Set("token", q.Token) } + if q.Near != "" { + r.params.Set("near", q.Near) + } } // durToMsec converts a duration to a millisecond specified string diff --git a/api/api_test.go b/api/api_test.go index 56f9494f89..314a89b146 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -127,6 +127,7 @@ func TestSetQueryOptions(t *testing.T) { WaitIndex: 1000, WaitTime: 100 * time.Second, Token: "12345", + Near: "nodex", } r.setQueryOptions(q) @@ -148,6 +149,9 @@ func TestSetQueryOptions(t *testing.T) { if r.params.Get("token") != "12345" { t.Fatalf("bad: %v", r.params) } + if r.params.Get("near") != "nodex" { + t.Fatalf("bad: %v", r.params) + } } func TestSetWriteOptions(t *testing.T) { diff --git a/api/coordinate.go b/api/coordinate.go new file mode 100644 index 0000000000..fdff2075cd --- /dev/null +++ b/api/coordinate.go @@ -0,0 +1,66 @@ +package api + +import ( + "github.com/hashicorp/serf/coordinate" +) + +// CoordinateEntry represents a node and its associated network coordinate. +type CoordinateEntry struct { + Node string + Coord *coordinate.Coordinate +} + +// CoordinateDatacenterMap represents a datacenter and its associated WAN +// nodes and their associates coordinates. +type CoordinateDatacenterMap struct { + Datacenter string + Coordinates []CoordinateEntry +} + +// Coordinate can be used to query the coordinate endpoints +type Coordinate struct { + c *Client +} + +// Coordinate returns a handle to the coordinate endpoints +func (c *Client) Coordinate() *Coordinate { + return &Coordinate{c} +} + +// Datacenters is used to return the coordinates of all the servers in the WAN +// pool. +func (c *Coordinate) Datacenters() ([]*CoordinateDatacenterMap, error) { + r := c.c.newRequest("GET", "/v1/coordinate/datacenters") + _, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out []*CoordinateDatacenterMap + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + +// Nodes is used to return the coordinates of all the nodes in the LAN pool. +func (c *Coordinate) Nodes(q *QueryOptions) ([]*CoordinateEntry, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/coordinate/nodes") + 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 new file mode 100644 index 0000000000..9d13d1c39d --- /dev/null +++ b/api/coordinate_test.go @@ -0,0 +1,54 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/hashicorp/consul/testutil" +) + +func TestCoordinate_Datacenters(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + coordinate := c.Coordinate() + + testutil.WaitForResult(func() (bool, error) { + datacenters, err := coordinate.Datacenters() + if err != nil { + return false, err + } + + if len(datacenters) == 0 { + return false, fmt.Errorf("Bad: %v", datacenters) + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +} + +func TestCoordinate_Nodes(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + coordinate := c.Coordinate() + + testutil.WaitForResult(func() (bool, error) { + _, _, err := coordinate.Nodes(nil) + if err != nil { + return false, 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. + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) +}