mirror of https://github.com/status-im/consul.git
Adds a prepared query debug endpoint.
This commit is contained in:
parent
c7ee82c67f
commit
d7288e3a5e
|
@ -13,6 +13,7 @@ import (
|
||||||
const (
|
const (
|
||||||
preparedQueryEndpoint = "PreparedQuery"
|
preparedQueryEndpoint = "PreparedQuery"
|
||||||
preparedQueryExecuteSuffix = "/execute"
|
preparedQueryExecuteSuffix = "/execute"
|
||||||
|
preparedQueryDebugSuffix = "/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
// preparedQueryCreateResponse is used to wrap the query ID.
|
// preparedQueryCreateResponse is used to wrap the query ID.
|
||||||
|
@ -124,6 +125,31 @@ func (s *HTTPServer) preparedQueryExecute(id string, resp http.ResponseWriter, r
|
||||||
return reply, nil
|
return reply, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preparedQueryDebug shows what a given name resolves to, which is useful for
|
||||||
|
// operators in a world with templates.
|
||||||
|
func (s *HTTPServer) preparedQueryDebug(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
args := structs.PreparedQueryExecuteRequest{
|
||||||
|
QueryIDOrName: id,
|
||||||
|
}
|
||||||
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply structs.PreparedQueryDebugResponse
|
||||||
|
endpoint := s.agent.getEndpoint(preparedQueryEndpoint)
|
||||||
|
if err := s.agent.RPC(endpoint+".Debug", &args, &reply); err != nil {
|
||||||
|
// We have to check the string since the RPC sheds
|
||||||
|
// the specific error type.
|
||||||
|
if err.Error() == consul.ErrQueryNotFound.Error() {
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
resp.Write([]byte(err.Error()))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
// preparedQueryGet returns a single prepared query.
|
// preparedQueryGet returns a single prepared query.
|
||||||
func (s *HTTPServer) preparedQueryGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (s *HTTPServer) preparedQueryGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
args := structs.PreparedQuerySpecificRequest{
|
args := structs.PreparedQuerySpecificRequest{
|
||||||
|
@ -197,16 +223,22 @@ func (s *HTTPServer) preparedQueryDelete(id string, resp http.ResponseWriter, re
|
||||||
// particular query.
|
// particular query.
|
||||||
func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
id := strings.TrimPrefix(req.URL.Path, "/v1/query/")
|
id := strings.TrimPrefix(req.URL.Path, "/v1/query/")
|
||||||
execute := false
|
|
||||||
|
execute, debug := false, false
|
||||||
if strings.HasSuffix(id, preparedQueryExecuteSuffix) {
|
if strings.HasSuffix(id, preparedQueryExecuteSuffix) {
|
||||||
execute = true
|
execute = true
|
||||||
id = strings.TrimSuffix(id, preparedQueryExecuteSuffix)
|
id = strings.TrimSuffix(id, preparedQueryExecuteSuffix)
|
||||||
|
} else if strings.HasSuffix(id, preparedQueryDebugSuffix) {
|
||||||
|
debug = true
|
||||||
|
id = strings.TrimSuffix(id, preparedQueryDebugSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
if execute {
|
if execute {
|
||||||
return s.preparedQueryExecute(id, resp, req)
|
return s.preparedQueryExecute(id, resp, req)
|
||||||
|
} else if debug {
|
||||||
|
return s.preparedQueryDebug(id, resp, req)
|
||||||
} else {
|
} else {
|
||||||
return s.preparedQueryGet(id, resp, req)
|
return s.preparedQueryGet(id, resp, req)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ type MockPreparedQuery struct {
|
||||||
getFn func(*structs.PreparedQuerySpecificRequest, *structs.IndexedPreparedQueries) error
|
getFn func(*structs.PreparedQuerySpecificRequest, *structs.IndexedPreparedQueries) error
|
||||||
listFn func(*structs.DCSpecificRequest, *structs.IndexedPreparedQueries) error
|
listFn func(*structs.DCSpecificRequest, *structs.IndexedPreparedQueries) error
|
||||||
executeFn func(*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryExecuteResponse) error
|
executeFn func(*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryExecuteResponse) error
|
||||||
|
debugFn func(*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryDebugResponse) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockPreparedQuery) Apply(args *structs.PreparedQueryRequest,
|
func (m *MockPreparedQuery) Apply(args *structs.PreparedQueryRequest,
|
||||||
|
@ -59,6 +60,14 @@ func (m *MockPreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest,
|
||||||
return fmt.Errorf("should not have called Execute")
|
return fmt.Errorf("should not have called Execute")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockPreparedQuery) Debug(args *structs.PreparedQueryExecuteRequest,
|
||||||
|
reply *structs.PreparedQueryDebugResponse) error {
|
||||||
|
if m.debugFn != nil {
|
||||||
|
return m.debugFn(args, reply)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("should not have called Debug")
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreparedQuery_Create(t *testing.T) {
|
func TestPreparedQuery_Create(t *testing.T) {
|
||||||
httpTest(t, func(srv *HTTPServer) {
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
m := MockPreparedQuery{}
|
m := MockPreparedQuery{}
|
||||||
|
@ -332,6 +341,72 @@ func TestPreparedQuery_Execute(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreparedQuery_Debug(t *testing.T) {
|
||||||
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
|
m := MockPreparedQuery{}
|
||||||
|
if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.debugFn = func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryDebugResponse) error {
|
||||||
|
expected := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "my-id",
|
||||||
|
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.Query.Name = "hello"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body := bytes.NewBuffer(nil)
|
||||||
|
req, err := http.NewRequest("GET", "/v1/query/my-id/debug?token=my-token&consistent=true", 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.PreparedQueryDebugResponse)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unexpected: %T", obj)
|
||||||
|
}
|
||||||
|
if r.Query.Name != "hello" {
|
||||||
|
t.Fatalf("bad: %v", r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
|
body := bytes.NewBuffer(nil)
|
||||||
|
req, err := http.NewRequest("GET", "/v1/query/not-there/debug", 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 != 404 {
|
||||||
|
t.Fatalf("bad code: %d", resp.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreparedQuery_Get(t *testing.T) {
|
func TestPreparedQuery_Get(t *testing.T) {
|
||||||
httpTest(t, func(srv *HTTPServer) {
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
m := MockPreparedQuery{}
|
m := MockPreparedQuery{}
|
||||||
|
|
|
@ -269,6 +269,53 @@ func (p *PreparedQuery) List(args *structs.DCSpecificRequest, reply *structs.Ind
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug resolves a prepared query and returns the (possibly rendered template)
|
||||||
|
// to the caller. This is useful for letting operators figure out which query is
|
||||||
|
// picking up a given name.
|
||||||
|
func (p *PreparedQuery) Debug(args *structs.PreparedQueryExecuteRequest,
|
||||||
|
reply *structs.PreparedQueryDebugResponse) error {
|
||||||
|
if done, err := p.srv.forward("PreparedQuery.Debug", args, args, reply); done {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer metrics.MeasureSince([]string{"consul", "prepared-query", "debug"}, time.Now())
|
||||||
|
|
||||||
|
// We have to do this ourselves since we are not doing a blocking RPC.
|
||||||
|
p.srv.setQueryMeta(&reply.QueryMeta)
|
||||||
|
if args.RequireConsistent {
|
||||||
|
if err := p.srv.consistentRead(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to locate the query.
|
||||||
|
state := p.srv.fsm.State()
|
||||||
|
_, query, err := state.PreparedQueryResolve(args.QueryIDOrName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if query == nil {
|
||||||
|
return ErrQueryNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the query into a list so we can run the standard ACL filter on
|
||||||
|
// it.
|
||||||
|
queries := &structs.IndexedPreparedQueries{
|
||||||
|
Queries: structs.PreparedQueries{query},
|
||||||
|
}
|
||||||
|
if err := p.srv.filterACL(args.Token, queries); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the query was filtered out, return an error.
|
||||||
|
if len(queries.Queries) == 0 {
|
||||||
|
p.srv.logger.Printf("[WARN] consul.prepared_query: Debug on prepared query '%s' denied due to ACLs", query.ID)
|
||||||
|
return permissionDeniedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Query = *(queries.Queries[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Execute runs a prepared query and returns the results. This will perform the
|
// 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
|
// 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.
|
// part of a DNS lookup, or when executing prepared queries from the HTTP API.
|
||||||
|
|
|
@ -579,7 +579,7 @@ func TestPreparedQuery_parseQuery(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreparedQuery_ACLDeny_Template(t *testing.T) {
|
func TestPreparedQuery_ACLDeny_Catchall_Template(t *testing.T) {
|
||||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
c.ACLDatacenter = "dc1"
|
c.ACLDatacenter = "dc1"
|
||||||
c.ACLMasterToken = "root"
|
c.ACLMasterToken = "root"
|
||||||
|
@ -681,7 +681,6 @@ func TestPreparedQuery_ACLDeny_Template(t *testing.T) {
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
QueryID: query.Query.ID,
|
QueryID: query.Query.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp structs.IndexedPreparedQueries
|
var resp structs.IndexedPreparedQueries
|
||||||
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp)
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp)
|
||||||
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||||
|
@ -734,6 +733,63 @@ func TestPreparedQuery_ACLDeny_Template(t *testing.T) {
|
||||||
t.Fatalf("bad: %v", actual)
|
t.Fatalf("bad: %v", actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debugging should also be denied without a token.
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "anything",
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||||
|
t.Fatalf("bad: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user can debug and see the redacted token.
|
||||||
|
query.Query.Token = redactedToken
|
||||||
|
query.Query.Service.Service = "anything"
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "anything",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: token},
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := &resp.Query
|
||||||
|
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||||
|
if !reflect.DeepEqual(actual, query.Query) {
|
||||||
|
t.Fatalf("bad: %v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the management token can also debug and see the token.
|
||||||
|
query.Query.Token = "5e1e24e5-1329-f86f-18c6-3d3734edb2cd"
|
||||||
|
query.Query.Service.Service = "anything"
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "anything",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: "root"},
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := &resp.Query
|
||||||
|
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||||
|
if !reflect.DeepEqual(actual, query.Query) {
|
||||||
|
t.Fatalf("bad: %v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreparedQuery_Get(t *testing.T) {
|
func TestPreparedQuery_Get(t *testing.T) {
|
||||||
|
@ -1161,6 +1217,138 @@ func TestPreparedQuery_List(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreparedQuery_Debug(t *testing.T) {
|
||||||
|
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||||
|
c.ACLDatacenter = "dc1"
|
||||||
|
c.ACLMasterToken = "root"
|
||||||
|
c.ACLDefaultPolicy = "deny"
|
||||||
|
})
|
||||||
|
defer os.RemoveAll(dir1)
|
||||||
|
defer s1.Shutdown()
|
||||||
|
codec := rpcClient(t, s1)
|
||||||
|
defer codec.Close()
|
||||||
|
|
||||||
|
testutil.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
|
||||||
|
// Create an ACL with write permissions for prod- queries.
|
||||||
|
var token string
|
||||||
|
{
|
||||||
|
var rules = `
|
||||||
|
query "prod-" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
req := structs.ACLRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.ACLSet,
|
||||||
|
ACL: structs.ACL{
|
||||||
|
Name: "User token",
|
||||||
|
Type: structs.ACLTypeClient,
|
||||||
|
Rules: rules,
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||||
|
}
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a template.
|
||||||
|
query := structs.PreparedQueryRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Op: structs.PreparedQueryCreate,
|
||||||
|
Query: &structs.PreparedQuery{
|
||||||
|
Name: "prod-",
|
||||||
|
Token: "5e1e24e5-1329-f86f-18c6-3d3734edb2cd",
|
||||||
|
Template: structs.QueryTemplateOptions{
|
||||||
|
Type: structs.QueryTemplateTypeNamePrefixMatch,
|
||||||
|
},
|
||||||
|
Service: structs.ServiceQuery{
|
||||||
|
Service: "${name.full}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WriteRequest: structs.WriteRequest{Token: token},
|
||||||
|
}
|
||||||
|
var reply string
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug via the management token.
|
||||||
|
query.Query.ID = reply
|
||||||
|
query.Query.Service.Service = "prod-redis"
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "prod-redis",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: "root"},
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := &resp.Query
|
||||||
|
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||||
|
if !reflect.DeepEqual(actual, query.Query) {
|
||||||
|
t.Fatalf("bad: %v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug via the user token, which will redact the captured token.
|
||||||
|
query.Query.Token = redactedToken
|
||||||
|
query.Query.Service.Service = "prod-redis"
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "prod-redis",
|
||||||
|
QueryOptions: structs.QueryOptions{Token: token},
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := &resp.Query
|
||||||
|
actual.CreateIndex, actual.ModifyIndex = 0, 0
|
||||||
|
if !reflect.DeepEqual(actual, query.Query) {
|
||||||
|
t.Fatalf("bad: %v", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging should be denied without a token, since the user isn't
|
||||||
|
// allowed to see the query.
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: "prod-redis",
|
||||||
|
}
|
||||||
|
var resp structs.PreparedQueryDebugResponse
|
||||||
|
err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
|
||||||
|
t.Fatalf("bad: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to debug a bogus ID.
|
||||||
|
{
|
||||||
|
req := &structs.PreparedQueryExecuteRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryIDOrName: generateUUID(),
|
||||||
|
QueryOptions: structs.QueryOptions{Token: "root"},
|
||||||
|
}
|
||||||
|
var resp structs.IndexedPreparedQueries
|
||||||
|
if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Debug", req, &resp); err != nil {
|
||||||
|
if err.Error() != ErrQueryNotFound.Error() {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is a beast of a test, but the setup is so extensive it makes sense to
|
// This is a beast of a test, but the setup is so extensive it makes sense to
|
||||||
// walk through the different cases once we have it up. This is broken into
|
// walk through the different cases once we have it up. This is broken into
|
||||||
// sections so it's still pretty easy to read.
|
// sections so it's still pretty easy to read.
|
||||||
|
|
|
@ -231,3 +231,12 @@ type PreparedQueryExecuteResponse struct {
|
||||||
// QueryMeta has freshness information about the query.
|
// QueryMeta has freshness information about the query.
|
||||||
QueryMeta
|
QueryMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreparedQueryDebugResponse has the results when debugging a query.
|
||||||
|
type PreparedQueryDebugResponse struct {
|
||||||
|
// Query has the fully-rendered query.
|
||||||
|
Query PreparedQuery
|
||||||
|
|
||||||
|
// QueryMeta has freshness information about the query.
|
||||||
|
QueryMeta
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ The following endpoints are supported:
|
||||||
a prepared query
|
a prepared query
|
||||||
* [`/v1/query/<query or name>/execute`](#execute): Executes a
|
* [`/v1/query/<query or name>/execute`](#execute): Executes a
|
||||||
prepared query by its ID or optional name
|
prepared query by its ID or optional name
|
||||||
|
* [`/v1/query/<query or name>/debug`](#debug): Debugs a
|
||||||
|
prepared query by its ID or optional name
|
||||||
|
|
||||||
Not all endpoints support blocking queries and all consistency modes,
|
Not all endpoints support blocking queries and all consistency modes,
|
||||||
see details in the sections below.
|
see details in the sections below.
|
||||||
|
@ -229,6 +231,9 @@ above with a `Regexp` field set to `^geo-db-(.*?)-([^\-]+?)$` would return
|
||||||
"master" for `${match(2)}`. If the regular expression doesn't match, or an invalid
|
"master" for `${match(2)}`. If the regular expression doesn't match, or an invalid
|
||||||
index is given, then `${match(N)}` will return an empty string.
|
index is given, then `${match(N)}` will return an empty string.
|
||||||
|
|
||||||
|
See the [query debug](#debug) endpoint which is useful for testing interpolations
|
||||||
|
and determining which query is handling a given name.
|
||||||
|
|
||||||
Using templates it's possible to apply prepared query behaviors to many services
|
Using templates it's possible to apply prepared query behaviors to many services
|
||||||
with a single template. Here's an example template that matches any query and
|
with a single template. Here's an example template that matches any query and
|
||||||
applies a failover policy to it:
|
applies a failover policy to it:
|
||||||
|
@ -433,3 +438,49 @@ and `Failovers` has the number of remote datacenters that were queried
|
||||||
while executing the query. This provides some insight into where the data
|
while executing the query. This provides some insight into where the data
|
||||||
came from. This will be zero during non-failover operations where there
|
came from. This will be zero during non-failover operations where there
|
||||||
were healthy nodes found in the local datacenter.
|
were healthy nodes found in the local datacenter.
|
||||||
|
|
||||||
|
### <a name="debug"></a> /v1/query/\<query or name\>/debug
|
||||||
|
|
||||||
|
The query debug endpoint supports only the `GET` method and is used to see
|
||||||
|
a fully-rendered query for a given name. This is especially useful for finding
|
||||||
|
which [prepared query template](#templates) matches a given name, and what the
|
||||||
|
final query looks like after interpolation.
|
||||||
|
|
||||||
|
By default, the datacenter of the agent is queried; however, the `dc` can be
|
||||||
|
provided using the "?dc=" query parameter. This endpoint does not support
|
||||||
|
blocking queries, but it does support all consistency modes.
|
||||||
|
|
||||||
|
If ACLs are enabled, then the client will only see prepared queries for which their
|
||||||
|
token has `query` read privileges. A management token will be able to see all
|
||||||
|
prepared queries. Tokens will be redacted and displayed as `<hidden>` unless a
|
||||||
|
management token is used.
|
||||||
|
|
||||||
|
If the query does not exist then a 404 status code will be returned. Otherwise,
|
||||||
|
a JSON body will be returned like this:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"Query": {
|
||||||
|
"ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05",
|
||||||
|
"Name": "my-query",
|
||||||
|
"Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
|
||||||
|
"Token": "<hidden>",
|
||||||
|
"Name": "geo-db",
|
||||||
|
"Template" {
|
||||||
|
"Type": "name_prefix_match",
|
||||||
|
"Regexp": "^geo-db-(.*?)-([^\-]+?)$"
|
||||||
|
},
|
||||||
|
"Service": {
|
||||||
|
"Service": "mysql-customer",
|
||||||
|
"Failover": {
|
||||||
|
"NearestN": 3,
|
||||||
|
"Datacenters": ["dc1", "dc2"]
|
||||||
|
},
|
||||||
|
"OnlyPassing": true,
|
||||||
|
"Tags": ["master"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that even though this query is a template, it is shown with its `Service`
|
||||||
|
fields interpolated based on the example query name "geo-db-customer-master".
|
||||||
|
|
Loading…
Reference in New Issue