diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 3840d069ca..40e371b8e1 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -181,6 +181,76 @@ func parseDNS(dns *structs.QueryDNSOptions) error { return nil } +// Lookup returns a single prepared query by ID or name. +func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply *structs.IndexedPreparedQuery) error { + if done, err := p.srv.forward("PreparedQuery.Lookup", args, args, reply); done { + return err + } + + // We will use this in the loop to see if the caller is allowed to see + // the query. + acl, err := p.srv.resolveToken(args.Token) + if err != nil { + return err + } + + // Get the requested query. + state := p.srv.fsm.State() + return p.srv.blockingRPC( + &args.QueryOptions, + &reply.QueryMeta, + state.GetQueryWatch("PreparedQueryLookup"), + func() error { + index, query, err := state.PreparedQueryLookup(args.QueryIDOrName) + if err != nil { + return err + } + + if (query.Token != args.Token) && (acl != nil && !acl.QueryModify()) { + p.srv.logger.Printf("[WARN] consul.prepared_query: Request to lookup prepared query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.QueryIDOrName) + return permissionDeniedErr + } + + reply.Index, reply.Query = index, query + return nil + }) + + return nil +} + +// List returns all the prepared queries. +func (p *PreparedQuery) List(args *structs.DCSpecificRequest, reply *structs.IndexedPreparedQueries) error { + if done, err := p.srv.forward("PreparedQuery.List", args, args, reply); done { + return err + } + + // This always requires a management token. + acl, err := p.srv.resolveToken(args.Token) + if err != nil { + return err + } + if acl != nil && !acl.QueryList() { + p.srv.logger.Printf("[WARN] consul.prepared_query: Request to list prepared queries denied due to ACLs") + return permissionDeniedErr + } + + // Get the list of queries. + state := p.srv.fsm.State() + return p.srv.blockingRPC( + &args.QueryOptions, + &reply.QueryMeta, + state.GetQueryWatch("PreparedQueryList"), + func() error { + index, queries, err := state.PreparedQueryList() + if err != nil { + return err + } + + reply.Index, reply.Queries = index, queries + return nil + }) +} + // Execute runs a prepared query and returns the results. This will perform the // failover logic if no local results are available. This is typically called as // part of a DNS lookup, or when executing prepared queries from the HTTP API. diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index c9a4a736a1..cca1e666e8 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -2,6 +2,7 @@ package consul import ( "os" + "strings" "testing" "github.com/hashicorp/consul/consul/structs" @@ -18,6 +19,27 @@ func TestPreparedQuery_Apply(t *testing.T) { testutil.WaitForLeader(t, s1.RPC, "dc1") + // Set up a node and service in the catalog. + { + arg := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "redis", + Tags: []string{"master"}, + Port: 8000, + }, + } + var reply struct{} + + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &reply) + if err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up a bare bones query. arg := structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, @@ -28,7 +50,92 @@ func TestPreparedQuery_Apply(t *testing.T) { }, } var reply string - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { + + // Set an ID which should fail the create. + arg.Query.ID = "nope" + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "ID must be empty") { + t.Fatalf("bad: %v", err) + } + + // Change it to a bogus modify which should also fail. + arg.Op = structs.PreparedQueryUpdate + arg.Query.ID = generateUUID() + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "Cannot modify non-existent prepared query") { + t.Fatalf("bad: %v", err) + } + + // Fix up the ID but invalidate the query itself. This proves we call + // parseQuery for a create, but that function is checked in detail as + // part of another test. + arg.Op = structs.PreparedQueryCreate + arg.Query.ID = "" + arg.Query.Service.Failover.NearestN = -1 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "Bad NearestN") { + t.Fatalf("bad: %v", err) + } + + // Fix that and make sure it propagates an error from the Raft apply. + arg.Query.Service.Failover.NearestN = 0 + arg.Query.Service.Service = "nope" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "invalid service") { + t.Fatalf("bad: %v", err) + } + + // Fix that and make sure the apply goes through. + arg.Query.Service.Service = "redis" + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { t.Fatalf("err: %v", err) } + + // Capture the new ID and make the op an update. This should go through. + arg.Op = structs.PreparedQueryUpdate + arg.Query.ID = reply + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Give a bogus op and make sure it fails. + arg.Op = "nope" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "Unknown prepared query operation:") { + t.Fatalf("bad: %v", err) + } + + // Prove that an update also goes through the parseQuery validation. + arg.Op = structs.PreparedQueryUpdate + arg.Query.Service.Failover.NearestN = -1 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + if err == nil || !strings.Contains(err.Error(), "Bad NearestN") { + t.Fatalf("bad: %v", err) + } + + // Sanity check - make sure there's one PQ in there. + var queries structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", + &structs.DCSpecificRequest{Datacenter: "dc1"}, &queries); err != nil { + t.Fatalf("err: %v", err) + } + if len(queries.Queries) != 1 { + t.Fatalf("bad: %v", queries) + } + + // Now change the op to delete; the bad query field should be ignored + // because all we care about for a delete op is the ID. + arg.Op = structs.PreparedQueryDelete + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Make sure there are no longer any queries. + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", + &structs.DCSpecificRequest{Datacenter: "dc1"}, &queries); err != nil { + t.Fatalf("err: %v", err) + } + if len(queries.Queries) != 0 { + t.Fatalf("bad: %v", queries) + } } diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index 4b6cde3b9e..c21cd7e6de 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -83,6 +83,16 @@ type PreparedQuery struct { type PreparedQueries []*PreparedQuery +type IndexedPreparedQuery struct { + Query *PreparedQuery + QueryMeta +} + +type IndexedPreparedQueries struct { + Queries PreparedQueries + QueryMeta +} + type PreparedQueryOp string const ( @@ -104,6 +114,26 @@ func (q *PreparedQueryRequest) RequestDatacenter() string { return q.Datacenter } +// PreparedQuerySpecificRequest is used to get information about a prepared +// query. +type PreparedQuerySpecificRequest struct { + // Datacenter is the target this request is intended for. + Datacenter string + + // QueryIDOrName is the ID of a query _or_ the name of one, either can + // be provided. + QueryIDOrName string + + // QueryOptions (unfortunately named here) controls the consistency + // settings for the query lookup itself, as well as the service lookups. + QueryOptions +} + +// RequestDatacenter returns the datacenter for a given request. +func (q *PreparedQuerySpecificRequest) RequestDatacenter() string { + return q.Datacenter +} + // PreparedQueryExecuteRequest is used to execute a prepared query. type PreparedQueryExecuteRequest struct { // Datacenter is the target this request is intended for.