From 712a3dba2fe6fd1ee9b9bd2bd65254dced47d5a1 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 18:07:35 -0800 Subject: [PATCH] Adds API client support for prepared queries. --- api/prepared_query.go | 219 +++++++++++++++++++++++++++++++++++++ api/prepared_query_test.go | 123 +++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 api/prepared_query.go create mode 100644 api/prepared_query_test.go diff --git a/api/prepared_query.go b/api/prepared_query.go new file mode 100644 index 0000000000..2ab1ccceb5 --- /dev/null +++ b/api/prepared_query.go @@ -0,0 +1,219 @@ +package api + +import () + +// QueryDatacenterOptions sets options about how we fail over if there are no +// healthy nodes in the local datacenter. +type QueryDatacenterOptions struct { + // NearestN is set to the number of remote datacenters to try, based on + // network coordinates. + NearestN int + + // Datacenters is a fixed list of datacenters to try after NearestN. We + // never try a datacenter multiple times, so those are subtracted from + // this list before proceeding. + Datacenters []string +} + +// QueryDNSOptions controls settings when query results are served over DNS. +type QueryDNSOptions struct { + // TTL is the time to live for the served DNS results. + TTL string +} + +// ServiceQuery is used to query for a set of healthy nodes offering a specific +// service. +type ServiceQuery struct { + // Service is the service to query. + Service string + + // Failover controls what we do if there are no healthy nodes in the + // local datacenter. + Failover QueryDatacenterOptions + + // If OnlyPassing is true then we will only include nodes with passing + // health checks (critical AND warning checks will cause a node to be + // discarded) + OnlyPassing bool + + // Tags are a set of required and/or disallowed tags. If a tag is in + // this list it must be present. If the tag is preceded with "~" then + // it is disallowed. + Tags []string +} + +// PrepatedQueryDefinition defines a complete prepared query. +type PreparedQueryDefinition struct { + // ID is this UUID-based ID for the query, always generated by Consul. + ID string + + // Name is an optional friendly name for the query supplied by the + // user. NOTE - if this feature is used then it will reduce the security + // of any read ACL associated with this query/service since this name + // can be used to locate nodes with supplying any ACL. + Name string + + // Session is an optional session to tie this query's lifetime to. If + // this is omitted then the query will not expire. + Session string + + // Token is the ACL token used when the query was created, and it is + // used when a query is subsequently executed. This token, or a token + // with management privileges, must be used to change the query later. + Token string + + // Service defines a service query (leaving things open for other types + // later). + Service ServiceQuery + + // DNS has options that control how the results of this query are + // served over DNS. + DNS QueryDNSOptions +} + +// PreparedQueryExecuteResponse has the results of executing a query. +type PreparedQueryExecuteResponse struct { + // Service is the service that was queried. + Service string + + // Nodes has the nodes that were output by the query. + Nodes []ServiceEntry + + // DNS has the options for serving these results over DNS. + DNS QueryDNSOptions + + // Datacenter is the datacenter that these results came from. + Datacenter string + + // Failovers is a count of how many times we had to query a remote + // datacenter. + Failovers int +} + +// PreparedQuery can be used to query the prepared query endpoints. +type PreparedQuery struct { + c *Client +} + +// PreparedQuery returns a handle to the prepared query endpoints. +func (c *Client) PreparedQuery() *PreparedQuery { + return &PreparedQuery{c} +} + +// Create makes a new prepared query. The ID of the new query is returned. +func (c *PreparedQuery) Create(query *PreparedQueryDefinition, q *WriteOptions) (string, *WriteMeta, error) { + r := c.c.newRequest("POST", "/v1/query") + r.setWriteOptions(q) + r.obj = query + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{} + wm.RequestTime = rtt + + var out struct{ ID string } + if err := decodeBody(resp, &out); err != nil { + return "", nil, err + } + return out.ID, wm, nil +} + +// Update makes updates to an existing prepared query. +func (c *PreparedQuery) Update(query *PreparedQueryDefinition, q *WriteOptions) (*WriteMeta, error) { + r := c.c.newRequest("PUT", "/v1/query/"+query.ID) + r.setWriteOptions(q) + r.obj = query + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, err + } + resp.Body.Close() + + wm := &WriteMeta{} + wm.RequestTime = rtt + return wm, nil +} + +// List is used to fetch all the prepared queries (always requires a management +// token). +func (c *PreparedQuery) List(q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/query") + 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 []*PreparedQueryDefinition + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + +// Get is used to fetch a specific prepared query. +func (c *PreparedQuery) Get(queryID string, q *QueryOptions) ([]*PreparedQueryDefinition, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/query/"+queryID) + 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 []*PreparedQueryDefinition + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + +// Delete is used to delete a specific prepared query. +func (c *PreparedQuery) Delete(queryID string, q *QueryOptions) (*QueryMeta, error) { + r := c.c.newRequest("DELETE", "/v1/query/"+queryID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + return qm, nil +} + +// Execute is used to execute a specific prepared query. You can execute using +// a query ID or name. +func (c *PreparedQuery) Execute(queryIDOrName string, q *QueryOptions) (*PreparedQueryExecuteResponse, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/query/"+queryIDOrName+"/execute") + 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 *PreparedQueryExecuteResponse + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} diff --git a/api/prepared_query_test.go b/api/prepared_query_test.go new file mode 100644 index 0000000000..011bfb195d --- /dev/null +++ b/api/prepared_query_test.go @@ -0,0 +1,123 @@ +package api + +import ( + "reflect" + "testing" + + "github.com/hashicorp/consul/testutil" +) + +func TestPreparedQuery(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + // Set up a node and a service. + reg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + Service: &AgentService{ + ID: "redis1", + Service: "redis", + Tags: []string{"master", "v1"}, + Port: 8000, + }, + } + + catalog := c.Catalog() + testutil.WaitForResult(func() (bool, error) { + if _, err := catalog.Register(reg, nil); err != nil { + return false, err + } + + if _, _, err := catalog.Node("foobar", nil); err != nil { + return false, err + } + + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Create a simple prepared query. + def := &PreparedQueryDefinition{ + Service: ServiceQuery{ + Service: "redis", + }, + } + + query := c.PreparedQuery() + var err error + def.ID, _, err = query.Create(def, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Read it back. + defs, _, err := query.Get(def.ID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(defs) != 1 || !reflect.DeepEqual(defs[0], def) { + t.Fatalf("bad: %v", defs) + } + + // List them all. + defs, _, err = query.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(defs) != 1 || !reflect.DeepEqual(defs[0], def) { + t.Fatalf("bad: %v", defs) + } + + // Make an update. + def.Name = "my-query" + _, err = query.Update(def, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Read it back again to verify the update worked. + defs, _, err = query.Get(def.ID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(defs) != 1 || !reflect.DeepEqual(defs[0], def) { + t.Fatalf("bad: %v", defs) + } + + // Execute by ID. + results, _, err := query.Execute(def.ID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(results.Nodes) != 1 || results.Nodes[0].Node.Node != "foobar" { + t.Fatalf("bad: %v", results) + } + + // Execute by name. + results, _, err = query.Execute("my-query", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(results.Nodes) != 1 || results.Nodes[0].Node.Node != "foobar" { + t.Fatalf("bad: %v", results) + } + + // Delete it. + _, err = query.Delete(def.ID, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure there are no longer any queries. + defs, _, err = query.List(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(defs) != 0 { + t.Fatalf("bad: %v", defs) + } +}