From 57be55103c7a2c1e0b1b90b527049ca94c1febea Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 17:27:51 -0800 Subject: [PATCH] Adds an HTTP endpoint for prepared queries. --- command/agent/http.go | 3 + command/agent/prepared_query_endpoint.go | 175 +++++ command/agent/prepared_query_endpoint_test.go | 681 ++++++++++++++++++ consul/prepared_query_endpoint.go | 3 +- consul/structs/prepared_query.go | 11 +- 5 files changed, 870 insertions(+), 3 deletions(-) create mode 100644 command/agent/prepared_query_endpoint.go create mode 100644 command/agent/prepared_query_endpoint_test.go diff --git a/command/agent/http.go b/command/agent/http.go index 6174ba2cd8..04d0e76192 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -265,6 +265,9 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/acl/list", s.wrap(aclDisabled)) } + s.mux.HandleFunc("/v1/query", s.wrap(s.PreparedQueryGeneral)) + s.mux.HandleFunc("/v1/query/", s.wrap(s.PreparedQuerySpecific)) + if enableDebug { s.mux.HandleFunc("/debug/pprof/", pprof.Index) s.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) diff --git a/command/agent/prepared_query_endpoint.go b/command/agent/prepared_query_endpoint.go new file mode 100644 index 0000000000..a27978c89a --- /dev/null +++ b/command/agent/prepared_query_endpoint.go @@ -0,0 +1,175 @@ +package agent + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/hashicorp/consul/consul/structs" +) + +const ( + preparedQueryEndpoint = "PreparedQuery" + preparedQueryExecuteSuffix = "/execute" +) + +// preparedQueryCreateResponse is used to wrap the query ID. +type preparedQueryCreateResponse struct { + ID string +} + +// PreparedQueryGeneral handles all the general prepared query requests. +func (s *HTTPServer) PreparedQueryGeneral(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.preparedQueryGeneral(preparedQueryEndpoint, resp, req) +} + +// preparedQueryGeneral is the internal method that does the work on behalf of +// PreparedQueryGeneral. The RPC endpoint is parameterized to ease testing. +func (s *HTTPServer) preparedQueryGeneral(endpoint string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + switch req.Method { + case "POST": // Create a new prepared query. + args := structs.PreparedQueryRequest{ + Op: structs.PreparedQueryCreate, + } + s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) + if req.ContentLength > 0 { + if err := decodeBody(req, &args.Query, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + } + + var reply string + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return preparedQueryCreateResponse{reply}, nil + + case "GET": // List all the prepared queries. + var args structs.DCSpecificRequest + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var reply structs.IndexedPreparedQueries + if err := s.agent.RPC(endpoint+".List", &args, &reply); err != nil { + return nil, err + } + return reply.Queries, nil + + default: + resp.WriteHeader(405) + return nil, nil + } +} + +// parseLimit parses the optional limit parameter for a prepared query execution. +func parseLimit(req *http.Request, limit *int) error { + *limit = 0 + if arg := req.URL.Query().Get("limit"); arg != "" { + if i, err := strconv.Atoi(arg); err != nil { + return err + } else { + *limit = i + } + } + return nil +} + +// PreparedQuerySpecifc handles all the prepared query requests specific to a +// particular query. +func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return s.preparedQuerySpecific(preparedQueryEndpoint, resp, req) +} + +// preparedQuerySpecific is the internal method that does the work on behalf of +// PreparedQuerySpecific. The RPC endpoint is parameterized to ease testing. +func (s *HTTPServer) preparedQuerySpecific(endpoint string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + id := strings.TrimPrefix(req.URL.Path, "/v1/query/") + execute := false + if strings.HasSuffix(id, preparedQueryExecuteSuffix) { + execute = true + id = strings.TrimSuffix(id, preparedQueryExecuteSuffix) + } + + switch req.Method { + case "GET": // Execute or retrieve a prepared query. + if execute { + args := structs.PreparedQueryExecuteRequest{ + QueryIDOrName: id, + } + s.parseSource(req, &args.Source) + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + if err := parseLimit(req, &args.Limit); err != nil { + return nil, fmt.Errorf("Bad limit: %s", err) + } + + var reply structs.PreparedQueryExecuteResponse + if err := s.agent.RPC(endpoint+".Execute", &args, &reply); err != nil { + return nil, err + } + return reply, nil + } else { + args := structs.PreparedQuerySpecificRequest{ + QueryID: id, + } + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var reply structs.IndexedPreparedQueries + if err := s.agent.RPC(endpoint+".Get", &args, &reply); err != nil { + return nil, err + } + return reply.Queries, nil + } + + case "PUT": // Update an existing prepared query. + args := structs.PreparedQueryRequest{ + Op: structs.PreparedQueryUpdate, + } + s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) + if req.ContentLength > 0 { + if err := decodeBody(req, &args.Query, nil); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err))) + return nil, nil + } + } + + // Take the ID from the URL, not the embedded one. + args.Query.ID = id + + var reply string + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return nil, nil + + case "DELETE": // Delete a prepared query. + args := structs.PreparedQueryRequest{ + Op: structs.PreparedQueryDelete, + Query: &structs.PreparedQuery{ + ID: id, + }, + } + s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) + + var reply string + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return nil, nil + + default: + resp.WriteHeader(405) + return nil, nil + } +} diff --git a/command/agent/prepared_query_endpoint_test.go b/command/agent/prepared_query_endpoint_test.go new file mode 100644 index 0000000000..01384d0284 --- /dev/null +++ b/command/agent/prepared_query_endpoint_test.go @@ -0,0 +1,681 @@ +package agent + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/hashicorp/consul/consul/structs" +) + +// MockPreparedQuery is a fake endpoint that we inject into the Consul server +// in order to observe the RPC calls made by these HTTP endpoints. This lets +// us make sure that the request is being formed properly without having to +// set up a realistic environment for prepared queries, which is a huge task and +// already done in detail inside the prepared query endpoint's unit tests. If we +// can prove this formats proper requests into that then we should be good to +// go. We will do a single set of end-to-end tests in here to make sure that the +// server is wired up to the right endpoint when not "injected". +type MockPreparedQuery struct { + applyFn func(*structs.PreparedQueryRequest, *string) error + getFn func(*structs.PreparedQuerySpecificRequest, *structs.IndexedPreparedQueries) error + listFn func(*structs.DCSpecificRequest, *structs.IndexedPreparedQueries) error + executeFn func(*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryExecuteResponse) error +} + +func (m *MockPreparedQuery) Apply(args *structs.PreparedQueryRequest, + reply *string) (err error) { + if m.applyFn != nil { + return m.applyFn(args, reply) + } + return fmt.Errorf("should not have called Apply") +} + +func (m *MockPreparedQuery) Get(args *structs.PreparedQuerySpecificRequest, + reply *structs.IndexedPreparedQueries) error { + if m.getFn != nil { + return m.getFn(args, reply) + } + return fmt.Errorf("should not have called Get") +} + +func (m *MockPreparedQuery) List(args *structs.DCSpecificRequest, + reply *structs.IndexedPreparedQueries) error { + if m.listFn != nil { + return m.listFn(args, reply) + } + return fmt.Errorf("should not have called List") +} + +func (m *MockPreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, + reply *structs.PreparedQueryExecuteResponse) error { + if m.executeFn != nil { + return m.executeFn(args, reply) + } + return fmt.Errorf("should not have called Execute") +} + +func TestPreparedQuery_Create(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.applyFn = func(args *structs.PreparedQueryRequest, reply *string) error { + expected := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "my-query", + Session: "my-session", + Service: structs.ServiceQuery{ + Service: "my-service", + Failover: structs.QueryDatacenterOptions{ + NearestN: 4, + Datacenters: []string{"dc1", "dc2"}, + }, + OnlyPassing: true, + Tags: []string{"foo", "bar"}, + }, + DNS: structs.QueryDNSOptions{ + TTL: "10s", + }, + }, + WriteRequest: structs.WriteRequest{ + Token: "my-token", + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + *reply = "my-id" + return nil + } + + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Name": "my-query", + "Session": "my-session", + "Service": map[string]interface{}{ + "Service": "my-service", + "Failover": map[string]interface{}{ + "NearestN": 4, + "Datacenters": []string{"dc1", "dc2"}, + }, + "OnlyPassing": true, + "Tags": []string{"foo", "bar"}, + }, + "DNS": map[string]interface{}{ + "TTL": "10s", + }, + } + if err := enc.Encode(raw); err != nil { + t.Fatalf("err: %v", err) + } + + req, err := http.NewRequest("POST", "/v1/query?token=my-token", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.preparedQueryGeneral("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(preparedQueryCreateResponse) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if r.ID != "my-id" { + t.Fatalf("bad ID: %s", r.ID) + } + }) +} + +func TestPreparedQuery_List(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.listFn = func(args *structs.DCSpecificRequest, reply *structs.IndexedPreparedQueries) error { + expected := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{ + Token: "my-token", + RequireConsistent: true, + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + query := &structs.PreparedQuery{ + ID: "my-id", + } + reply.Queries = append(reply.Queries, query) + return nil + } + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query?token=my-token&consistent=true", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.preparedQueryGeneral("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueries) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if len(r) != 1 || r[0].ID != "my-id" { + t.Fatalf("bad: %v", r) + } + }) +} + +func TestPreparedQuery_Execute(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.executeFn = func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { + expected := &structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: "my-id", + Limit: 5, + Source: structs.QuerySource{ + Datacenter: "dc1", + Node: "my-node", + }, + QueryOptions: structs.QueryOptions{ + Token: "my-token", + RequireConsistent: true, + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + // Just set something so we can tell this is returned. + reply.Failovers = 99 + return nil + } + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/my-id/execute?token=my-token&consistent=true&near=my-node&limit=5", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueryExecuteResponse) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if r.Failovers != 99 { + t.Fatalf("bad: %v", r) + } + }) +} + +func TestPreparedQuery_Get(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.getFn = func(args *structs.PreparedQuerySpecificRequest, reply *structs.IndexedPreparedQueries) error { + expected := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryID: "my-id", + QueryOptions: structs.QueryOptions{ + Token: "my-token", + RequireConsistent: true, + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + query := &structs.PreparedQuery{ + ID: "my-id", + } + reply.Queries = append(reply.Queries, query) + return nil + } + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/my-id?token=my-token&consistent=true", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueries) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if len(r) != 1 || r[0].ID != "my-id" { + t.Fatalf("bad: %v", r) + } + }) +} + +func TestPreparedQuery_Update(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.applyFn = func(args *structs.PreparedQueryRequest, reply *string) error { + expected := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryUpdate, + Query: &structs.PreparedQuery{ + ID: "my-id", + Name: "my-query", + Session: "my-session", + Service: structs.ServiceQuery{ + Service: "my-service", + Failover: structs.QueryDatacenterOptions{ + NearestN: 4, + Datacenters: []string{"dc1", "dc2"}, + }, + OnlyPassing: true, + Tags: []string{"foo", "bar"}, + }, + DNS: structs.QueryDNSOptions{ + TTL: "10s", + }, + }, + WriteRequest: structs.WriteRequest{ + Token: "my-token", + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + *reply = "don't care" + return nil + } + + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "ID": "this should get ignored", + "Name": "my-query", + "Session": "my-session", + "Service": map[string]interface{}{ + "Service": "my-service", + "Failover": map[string]interface{}{ + "NearestN": 4, + "Datacenters": []string{"dc1", "dc2"}, + }, + "OnlyPassing": true, + "Tags": []string{"foo", "bar"}, + }, + "DNS": map[string]interface{}{ + "TTL": "10s", + }, + } + if err := enc.Encode(raw); err != nil { + t.Fatalf("err: %v", err) + } + + req, err := http.NewRequest("PUT", "/v1/query/my-id?token=my-token", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + }) +} + +func TestPreparedQuery_Delete(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + m := MockPreparedQuery{} + if err := srv.agent.server.InjectEndpoint(&m); err != nil { + t.Fatalf("err: %v", err) + } + + m.applyFn = func(args *structs.PreparedQueryRequest, reply *string) error { + expected := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryDelete, + Query: &structs.PreparedQuery{ + ID: "my-id", + }, + WriteRequest: structs.WriteRequest{ + Token: "my-token", + }, + } + if !reflect.DeepEqual(args, expected) { + t.Fatalf("bad: %v", args) + } + + *reply = "don't care" + return nil + } + + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "ID": "this should get ignored", + } + if err := enc.Encode(raw); err != nil { + t.Fatalf("err: %v", err) + } + + req, err := http.NewRequest("DELETE", "/v1/query/my-id?token=my-token", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + }) +} + +func TestPreparedQuery_BadMethods(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("DELETE", "/v1/query", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.PreparedQueryGeneral(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 405 { + t.Fatalf("bad code: %d", resp.Code) + } + }) + + httpTest(t, func(srv *HTTPServer) { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("POST", "/v1/query/my-id", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.PreparedQuerySpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 405 { + t.Fatalf("bad code: %d", resp.Code) + } + }) +} + +func TestPreparedQuery_parseLimit(t *testing.T) { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + limit := 99 + if err := parseLimit(req, &limit); err != nil { + t.Fatalf("err: %v", err) + } + if limit != 0 { + t.Fatalf("bad limit: %d", limit) + } + + req, err = http.NewRequest("GET", "/v1/query?limit=11", body) + if err != nil { + t.Fatalf("err: %v", err) + } + if err := parseLimit(req, &limit); err != nil { + t.Fatalf("err: %v", err) + } + if limit != 11 { + t.Fatalf("bad limit: %d", limit) + } + + req, err = http.NewRequest("GET", "/v1/query?limit=bob", body) + if err != nil { + t.Fatalf("err: %v", err) + } + if err := parseLimit(req, &limit); err == nil { + t.Fatalf("bad: %v", err) + } +} + +// Since we've done exhaustive testing of the calls into the endpoints above +// this is just a basic end-to-end sanity check to make sure things are wired +// correctly when calling through to the real endpoints. +func TestPreparedQuery_Integration(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + // Register a node and a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: srv.agent.config.NodeName, + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "my-service", + }, + } + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Create a query. + var id string + { + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Name": "my-query", + "Service": map[string]interface{}{ + "Service": "my-service", + }, + } + if err := enc.Encode(raw); err != nil { + t.Fatalf("err: %v", err) + } + + req, err := http.NewRequest("POST", "/v1/query", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.PreparedQueryGeneral(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(preparedQueryCreateResponse) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + id = r.ID + } + + // List them all. + { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query?token=root", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.PreparedQueryGeneral(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueries) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if len(r) != 1 { + t.Fatalf("bad: %v", r) + } + } + + // Execute it. + { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/"+id+"/execute", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.PreparedQuerySpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueryExecuteResponse) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if len(r.Nodes) != 1 { + t.Fatalf("bad: %v", r) + } + } + + // Read it back. + { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/"+id, body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.PreparedQuerySpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + r, ok := obj.(structs.PreparedQueries) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if len(r) != 1 { + t.Fatalf("bad: %v", r) + } + } + + // Make an update to it. + { + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Name": "my-query", + "Service": map[string]interface{}{ + "Service": "my-service", + "OnlyPassing": true, + }, + } + if err := enc.Encode(raw); err != nil { + t.Fatalf("err: %v", err) + } + + req, err := http.NewRequest("PUT", "/v1/query/"+id, body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.PreparedQuerySpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + } + + // Delete it. + { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("DELETE", "/v1/query/"+id, body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + _, err = srv.PreparedQuerySpecific(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 200 { + t.Fatalf("bad code: %d", resp.Code) + } + } + }) +} diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 741feee200..a84032142c 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -182,7 +182,8 @@ func parseDNS(dns *structs.QueryDNSOptions) error { } // Get returns a single prepared query by ID. -func (p *PreparedQuery) Get(args *structs.PreparedQuerySpecificRequest, reply *structs.IndexedPreparedQueries) error { +func (p *PreparedQuery) Get(args *structs.PreparedQuerySpecificRequest, + reply *structs.IndexedPreparedQueries) error { if done, err := p.srv.forward("PreparedQuery.Get", args, args, reply); done { return err } diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index bb49d1f264..9f9e2fb96b 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -98,9 +98,16 @@ const ( // QueryRequest is used to create or change prepared queries. type PreparedQueryRequest struct { + // Datacenter is the target this request is intended for. Datacenter string - Op PreparedQueryOp - Query *PreparedQuery + + // Op is the operation to apply. + Op PreparedQueryOp + + // Query is the query itself. + Query *PreparedQuery + + // WriteRequest holds the ACL token to go along with this request. WriteRequest }