From 081b9165cac9529a3ec03f793cd8ca438ee3b8b5 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 07:01:54 -0800 Subject: [PATCH 001/123] Adds a note about the new acquire behavior for lock holders. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2616a83334..ac746f167e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ IMPROVEMENTS: * Enables the `/v1/internal/ui/*` endpoints, even if `-ui-dir` isn't set [GH-1215] * Added HTTP method to Consul's log output for better debugging [GH-1270] +* Lock holders can "?acquire" a key again with the same session to update + its contents without releasing the lock [GH-1291] * Improved an O(n^2) algorithm in the agent's catalog sync code [GH-1296] * Switched to net-rpc-msgpackrpc to reduce RPC overhead [GH-1307] * Removes all uses of the http package's default client and transport in From bc12c5e7119649aed2ad3dfc264485f78fb4576d Mon Sep 17 00:00:00 2001 From: talwai Date: Tue, 16 Jun 2015 18:05:55 -0400 Subject: [PATCH 002/123] Add DogStatsd configuration --- command/agent/command.go | 19 +++++++++++++++++++ command/agent/config.go | 14 ++++++++++++++ command/agent/config_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/command/agent/command.go b/command/agent/command.go index 2d642b20e3..490ef69581 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -15,6 +15,8 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/armon/go-metrics/datadog" + "github.com/hashicorp/consul-migrate/migrator" "github.com/hashicorp/consul/watch" "github.com/hashicorp/go-checkpoint" "github.com/hashicorp/go-syslog" @@ -604,6 +606,23 @@ func (c *Command) Run(args []string) int { fanout = append(fanout, sink) } + // Configure the DogStatsd sink + if config.DogStatsdAddr != "" { + var tags []string + + if config.DogStatsdTags != nil { + tags = config.DogStatsdTags + } + + sink, err := datadog.NewDogStatsdSink(config.DogStatsdAddr, metricsConf.HostName) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to start DogStatsd sink. Got: %s", err)) + return 1 + } + sink.SetTags(tags) + fanout = append(fanout, sink) + } + // Initialize the global sink if len(fanout) > 0 { fanout = append(fanout, inm) diff --git a/command/agent/config.go b/command/agent/config.go index 6240b33f22..3577a18c30 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -184,6 +184,14 @@ type Config struct { // metrics will be sent to that instance. StatsdAddr string `mapstructure:"statsd_addr"` + // DogStatsdAddr is the address of a dogstatsd instance. If provided, + // metrics will be sent to that instance + DogStatsdAddr string `mapstructure:"dogstatsd_addr"` + + // DogStatsdTags are the global tags that should be sent with each packet to dogstatsd + // It is a list of strings, where each string looks like "my_tag_name:my_tag_value" + DogStatsdTags []string `mapstructure:"dogstatsd_tags"` + // Protocol is the Consul protocol version to use. Protocol int `mapstructure:"protocol"` @@ -916,6 +924,12 @@ func MergeConfig(a, b *Config) *Config { if b.StatsdAddr != "" { result.StatsdAddr = b.StatsdAddr } + if b.DogStatsdAddr != "" { + result.DogStatsdAddr = b.DogStatsdAddr + } + if b.DogStatsdTags != nil { + result.DogStatsdTags = b.DogStatsdTags + } if b.EnableDebug { result.EnableDebug = true } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index dc0517f349..8fb1ae370e 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -629,6 +629,28 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + // dogstatsd + input = `{"dogstatsd_addr": "127.0.0.1:7254", "dogstatsd_tags":["tag_1:val_1", "tag_2:val_2"]}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + if config.DogStatsdAddr != "127.0.0.1:7254" { + t.Fatalf("bad: %#v", config) + } + + if len(config.DogStatsdTags) != 2 { + t.Fatalf("bad: %#v", config) + } + + if config.DogStatsdTags[0] != "tag_1:val_1" { + t.Fatalf("bad: %#v", config) + } + + if config.DogStatsdTags[1] != "tag_2:val_2" { + t.Fatalf("bad: %#v", config) + } + // Statsite prefix input = `{"statsite_prefix": "my_prefix"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) @@ -1216,6 +1238,8 @@ func TestMergeConfig(t *testing.T) { StatsiteAddr: "127.0.0.1:7250", StatsitePrefix: "stats_prefix", StatsdAddr: "127.0.0.1:7251", + DogStatsdAddr: "127.0.0.1:7254", + DogStatsdTags: []string{"tag_1:val_1", "tag_2:val_2"}, DisableUpdateCheck: true, DisableAnonymousSignature: true, HTTPAPIResponseHeaders: map[string]string{ From 0a5434f2a099b9733b15c11c8f30fae53ef74e18 Mon Sep 17 00:00:00 2001 From: talwai Date: Mon, 12 Oct 2015 13:53:33 -0400 Subject: [PATCH 003/123] Add DogStatsD configuration options to documentation source --- website/source/docs/agent/options.html.markdown | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 1851f87b36..043e537453 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -562,6 +562,15 @@ definitions support being updated during a reload. This can be used to capture runtime information. This sends UDP packets only and can be used with statsd or statsite. +* `dogstatsd_addr` This provides the + address of a DogStatsD instance. DogStatsD is a protocol-compatible flavor of statsd, with the added ability + to decorate metrics with tags and event information. If provided, Consul will send various telemetry information + to that instance for aggregation. This can be used to capture runtime information. + +* `dogstatsd_tags` This provides a list of global tags + that will be added to all telemetry packets sent to DogStatsD. It is a list of strings, where each string + looks like "my_tag_name:my_tag_value". + * `statsite_addr` This provides the address of a statsite instance. If provided, Consul will stream various telemetry information to that instance for aggregation. This can be used to capture runtime information. This streams via From 98731f727237395120cdd04453ccef546814b92f Mon Sep 17 00:00:00 2001 From: James Phillips Date: Thu, 12 Nov 2015 18:16:44 -0800 Subject: [PATCH 004/123] Fixes unit test fail due to expected truncated message. --- command/agent/dns_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index aafc7c3902..f71703ebc1 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -1406,7 +1406,7 @@ func TestDNS_ServiceLookup_Truncate(t *testing.T) { addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) c := new(dns.Client) in, _, err := c.Exchange(m, addr.String()) - if err != nil { + if err != nil && err != dns.ErrTruncated { t.Fatalf("err: %v", err) } From f6f2e19c6c738b6f24d64d9851a074306e901860 Mon Sep 17 00:00:00 2001 From: talwai Date: Fri, 13 Nov 2015 11:14:15 -0500 Subject: [PATCH 005/123] Kill unused import in command.go --- command/agent/command.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/agent/command.go b/command/agent/command.go index 490ef69581..165a73994a 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -16,7 +16,6 @@ import ( "github.com/armon/go-metrics" "github.com/armon/go-metrics/datadog" - "github.com/hashicorp/consul-migrate/migrator" "github.com/hashicorp/consul/watch" "github.com/hashicorp/go-checkpoint" "github.com/hashicorp/go-syslog" From 692adad55c4ad3748d24aff462ad018c311d1566 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 09:58:30 -0800 Subject: [PATCH 006/123] Updates the changelog. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac746f167e..f4a51a777e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ FEATURES: * Ported in-memory database from LMDB to an immutable radix tree to improve read throughput, reduce garbage collection pressure, and make Consul 100% pure Go [GH-1291] -* Added new network tomography sub system that estimates the network +* Added support for sending telemetry to DogStatsD [GH-1293] +* Added new network tomography subsystem that estimates the network round trip times between nodes and exposes that in raw APIs, as well as in existing APIs (find the service node nearest node X); also includes a new `consul rtt` command to query interactively [GH-1331] From 4a775004b80d4f0af05577dbbca2b37351ceaa49 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 10:07:42 -0800 Subject: [PATCH 007/123] Adds a note about the new KV lock behavior. --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a51a777e..4b81a58dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## 0.6.0 (Unreleased) +BACKWARDS INCOMPATIBILITIES: + +* A KV lock acquisition operation will now allow the lock holder to + update the key's value without giving up the lock by doing another + PUT with `?acquire=` and providing the same session that + is holding the lock. Previously, this operation would fail. + FEATURES: * Service ACLs now apply to service discovery [GH-1024] @@ -73,7 +80,7 @@ MISC: * Lots of docs fixes * Lots of Vagrantfile cleanup -* Data migrator utility removed to reduce cgo dependency. [GH-1309] +* Data migrator utility removed to reduce cgo dependency [GH-1309] UPGRADE NOTES: From 9ffafdf323f4da195af0f8bcd24a1da09bbf8030 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 10:08:57 -0800 Subject: [PATCH 008/123] Updates wording on new lock operation. --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b81a58dd1..d14a0088a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ BACKWARDS INCOMPATIBILITIES: * A KV lock acquisition operation will now allow the lock holder to - update the key's value without giving up the lock by doing another + update the key's contents without giving up the lock by doing another PUT with `?acquire=` and providing the same session that is holding the lock. Previously, this operation would fail. @@ -67,8 +67,9 @@ IMPROVEMENTS: * Enables the `/v1/internal/ui/*` endpoints, even if `-ui-dir` isn't set [GH-1215] * Added HTTP method to Consul's log output for better debugging [GH-1270] -* Lock holders can "?acquire" a key again with the same session to update - its contents without releasing the lock [GH-1291] +* Lock holders can `?acquire=` a key again with the same session + that holds the lock to update a key's contents without releasing the + lock [GH-1291] * Improved an O(n^2) algorithm in the agent's catalog sync code [GH-1296] * Switched to net-rpc-msgpackrpc to reduce RPC overhead [GH-1307] * Removes all uses of the http package's default client and transport in From 86f56ee4a1545d648e6c033e9c68bd4d28c6df39 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 10:28:14 -0800 Subject: [PATCH 009/123] Adds a note about the new acquire behavior into the sessions internals guide. --- website/source/docs/internals/sessions.html.markdown | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/source/docs/internals/sessions.html.markdown b/website/source/docs/internals/sessions.html.markdown index ef666148a8..d864da5ecc 100644 --- a/website/source/docs/internals/sessions.html.markdown +++ b/website/source/docs/internals/sessions.html.markdown @@ -109,9 +109,15 @@ and is then referred to by its ID. The Key/Value API is extended to support an `acquire` and `release` operation. The `acquire` operation acts like a Check-And-Set operation except it -can only succeed if there is no existing lock holder. On success, there -is a normal key update, but there is also an increment to the `LockIndex`, -and the `Session` value is updated to reflect the session holding the lock. +can only succeed if there is no existing lock holder (the current lock holder +can re-`acquire`, see below). On success, there is a normal key update, but +there is also an increment to the `LockIndex`, and the `Session` value is +updated to reflect the session holding the lock. + +If the lock is already held by the given session during an `acquire`, then +the `LockIndex` is not incremented but the key contents are updated. This +lets the current lock holder update the key contents without having to give +up the lock and reacquire it. Once held, the lock can be released using a corresponding `release` operation, providing the same session. Again, this acts like a Check-And-Set operations From c248b0017a22fb2fcb4f9cb566e32ad07fff2ec9 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 21:05:37 -0800 Subject: [PATCH 010/123] Fixes nil slices from HTTP endpoints. These would manifest in the HTTP output as Javascript nulls instead of empty lists, so we had unintentionally changed the interface while porting to the new state store. We added code to each HTTP endpoint to convert nil slices to empty ones so they JSON-ify properly, and we added tests to catch this in the future. --- command/agent/acl_endpoint.go | 10 ++ command/agent/acl_endpoint_test.go | 35 +++++- command/agent/catalog_endpoint.go | 10 ++ command/agent/catalog_endpoint_test.go | 21 ++++ command/agent/coordinate_endpoint.go | 17 +++ command/agent/coordinate_endpoint_test.go | 25 ++++- command/agent/health_endpoint.go | 28 +++++ command/agent/health_endpoint_test.go | 130 ++++++++++++++++++++-- command/agent/session_endpoint.go | 15 +++ command/agent/session_endpoint_test.go | 49 ++++++++ command/agent/ui_endpoint.go | 22 +++- command/agent/ui_endpoint_test.go | 53 ++++++++- 12 files changed, 398 insertions(+), 17 deletions(-) diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 72ae4c20af..3bc054611b 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -176,6 +176,11 @@ func (s *HTTPServer) ACLGet(resp http.ResponseWriter, req *http.Request) (interf if err := s.agent.RPC("ACL.Get", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.ACLs == nil { + out.ACLs = make(structs.ACLs, 0) + } return out.ACLs, nil } @@ -193,5 +198,10 @@ func (s *HTTPServer) ACLList(resp http.ResponseWriter, req *http.Request) (inter if err := s.agent.RPC("ACL.List", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.ACLs == nil { + out.ACLs = make(structs.ACLs, 0) + } return out.ACLs, nil } diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index b0dea0c9ba..361d661ac6 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -94,15 +94,30 @@ func TestACLUpdate_Upsert(t *testing.T) { func TestACLDestroy(t *testing.T) { httpTest(t, func(srv *HTTPServer) { id := makeTestACL(t, srv) - req, err := http.NewRequest("PUT", "/v1/session/destroy/"+id+"?token=root", nil) + req, err := http.NewRequest("PUT", "/v1/acl/destroy/"+id+"?token=root", nil) resp := httptest.NewRecorder() obj, err := srv.ACLDestroy(resp, req) if err != nil { t.Fatalf("err: %v", err) } - if resp := obj.(bool); !resp { + if resp, ok := obj.(bool); !ok || !resp { t.Fatalf("should work") } + + req, err = http.NewRequest("GET", + "/v1/acl/info/"+id, nil) + resp = httptest.NewRecorder() + obj, err = srv.ACLGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.ACLs) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 0 { + t.Fatalf("bad: %v", respObj) + } }) } @@ -143,6 +158,22 @@ func TestACLClone(t *testing.T) { } func TestACLGet(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", "/v1/acl/info/nope", nil) + resp := httptest.NewRecorder() + obj, err := srv.ACLGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.ACLs) + if !ok { + t.Fatalf("should work") + } + if respObj == nil || len(respObj) != 0 { + t.Fatalf("bad: %v", respObj) + } + }) + httpTest(t, func(srv *HTTPServer) { id := makeTestACL(t, srv) diff --git a/command/agent/catalog_endpoint.go b/command/agent/catalog_endpoint.go index 447822a875..3a5bd98134 100644 --- a/command/agent/catalog_endpoint.go +++ b/command/agent/catalog_endpoint.go @@ -70,6 +70,11 @@ func (s *HTTPServer) CatalogNodes(resp http.ResponseWriter, req *http.Request) ( if err := s.agent.RPC("Catalog.ListNodes", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.Nodes == nil { + out.Nodes = make(structs.Nodes, 0) + } return out.Nodes, nil } @@ -117,6 +122,11 @@ func (s *HTTPServer) CatalogServiceNodes(resp http.ResponseWriter, req *http.Req if err := s.agent.RPC("Catalog.ServiceNodes", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.ServiceNodes == nil { + out.ServiceNodes = make(structs.ServiceNodes, 0) + } return out.ServiceNodes, nil } diff --git a/command/agent/catalog_endpoint_test.go b/command/agent/catalog_endpoint_test.go index 5e4d75aab3..251d288df0 100644 --- a/command/agent/catalog_endpoint_test.go +++ b/command/agent/catalog_endpoint_test.go @@ -351,6 +351,27 @@ func TestCatalogServiceNodes(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + // Make sure an empty list is returned, not a nil + { + req, err := http.NewRequest("GET", "/v1/catalog/service/api?tag=a", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.CatalogServiceNodes(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + assertIndex(t, resp) + + nodes := obj.(structs.ServiceNodes) + if nodes == nil || len(nodes) != 0 { + t.Fatalf("bad: %v", obj) + } + } + // Register node args := &structs.RegisterRequest{ Datacenter: "dc1", diff --git a/command/agent/coordinate_endpoint.go b/command/agent/coordinate_endpoint.go index 0cc5824cec..92453225d7 100644 --- a/command/agent/coordinate_endpoint.go +++ b/command/agent/coordinate_endpoint.go @@ -45,6 +45,18 @@ func (s *HTTPServer) CoordinateDatacenters(resp http.ResponseWriter, req *http.R } return nil, err } + + // Use empty list instead of nil (these aren't really possible because + // Serf will give back a default coordinate and there's always one DC, + // but it's better to be explicit about what we want here). + for i, _ := range out { + if out[i].Coordinates == nil { + out[i].Coordinates = make(structs.Coordinates, 0) + } + } + if out == nil { + out = make([]structs.DatacenterMap, 0) + } return out, nil } @@ -62,5 +74,10 @@ func (s *HTTPServer) CoordinateNodes(resp http.ResponseWriter, req *http.Request sort.Sort(&sorter{out.Coordinates}) return nil, err } + + // Use empty list instead of nil. + if out.Coordinates == nil { + out.Coordinates = make(structs.Coordinates, 0) + } return out.Coordinates, nil } diff --git a/command/agent/coordinate_endpoint_test.go b/command/agent/coordinate_endpoint_test.go index aee7bc18cd..8da0a5b397 100644 --- a/command/agent/coordinate_endpoint_test.go +++ b/command/agent/coordinate_endpoint_test.go @@ -48,6 +48,23 @@ func TestCoordinate_Nodes(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + // Make sure an empty list is non-nil. + req, err := http.NewRequest("GET", "/v1/coordinate/nodes?dc=dc1", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.CoordinateNodes(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + coordinates := obj.(structs.Coordinates) + if coordinates == nil || len(coordinates) != 0 { + t.Fatalf("bad: %v", coordinates) + } + // Register the nodes. nodes := []string{"foo", "bar"} for _, node := range nodes { @@ -85,18 +102,18 @@ func TestCoordinate_Nodes(t *testing.T) { time.Sleep(200 * time.Millisecond) // Query back and check the nodes are present and sorted correctly. - req, err := http.NewRequest("GET", "/v1/coordinate/nodes?dc=dc1", nil) + req, err = http.NewRequest("GET", "/v1/coordinate/nodes?dc=dc1", nil) if err != nil { t.Fatalf("err: %v", err) } - resp := httptest.NewRecorder() - obj, err := srv.CoordinateNodes(resp, req) + resp = httptest.NewRecorder() + obj, err = srv.CoordinateNodes(resp, req) if err != nil { t.Fatalf("err: %v", err) } - coordinates := obj.(structs.Coordinates) + coordinates = obj.(structs.Coordinates) if len(coordinates) != 2 || coordinates[0].Node != "bar" || coordinates[1].Node != "foo" { diff --git a/command/agent/health_endpoint.go b/command/agent/health_endpoint.go index f95e4c5e72..e4a36f6a6a 100644 --- a/command/agent/health_endpoint.go +++ b/command/agent/health_endpoint.go @@ -28,6 +28,11 @@ func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Req if err := s.agent.RPC("Health.ChecksInState", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.HealthChecks == nil { + out.HealthChecks = make(structs.HealthChecks, 0) + } return out.HealthChecks, nil } @@ -52,6 +57,11 @@ func (s *HTTPServer) HealthNodeChecks(resp http.ResponseWriter, req *http.Reques if err := s.agent.RPC("Health.NodeChecks", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.HealthChecks == nil { + out.HealthChecks = make(structs.HealthChecks, 0) + } return out.HealthChecks, nil } @@ -77,6 +87,11 @@ func (s *HTTPServer) HealthServiceChecks(resp http.ResponseWriter, req *http.Req if err := s.agent.RPC("Health.ServiceChecks", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.HealthChecks == nil { + out.HealthChecks = make(structs.HealthChecks, 0) + } return out.HealthChecks, nil } @@ -114,6 +129,19 @@ func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Requ if _, ok := params["passing"]; ok { out.Nodes = filterNonPassing(out.Nodes) } + + // Use empty list instead of nil + for i, _ := range out.Nodes { + // TODO (slackpad) It's lame that this isn't a slice of pointers + // but it's not a well-scoped change to fix this. We should + // change this at the next opportunity. + if out.Nodes[i].Checks == nil { + out.Nodes[i].Checks = make(structs.HealthChecks, 0) + } + } + if out.Nodes == nil { + out.Nodes = make(structs.CheckServiceNodes, 0) + } return out.Nodes, nil } diff --git a/command/agent/health_endpoint_test.go b/command/agent/health_endpoint_test.go index 317203065b..02330a37c0 100644 --- a/command/agent/health_endpoint_test.go +++ b/command/agent/health_endpoint_test.go @@ -15,6 +15,31 @@ import ( ) func TestHealthChecksInState(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", "/v1/health/state/warning?dc=dc1", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + resp := httptest.NewRecorder() + obj, err := srv.HealthChecksInState(resp, req) + if err != nil { + return false, err + } + if err := checkIndex(resp); err != nil { + return false, err + } + + // Should be a non-nil empty list + nodes := obj.(structs.HealthChecks) + if nodes == nil || len(nodes) != 0 { + return false, fmt.Errorf("bad: %v", obj) + } + return true, nil + }, func(err error) { t.Fatalf("err: %v", err) }) + }) + httpTest(t, func(srv *HTTPServer) { req, err := http.NewRequest("GET", "/v1/health/state/passing?dc=dc1", nil) if err != nil { @@ -130,8 +155,7 @@ func TestHealthNodeChecks(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - req, err := http.NewRequest("GET", - fmt.Sprintf("/v1/health/node/%s?dc=dc1", srv.agent.config.NodeName), nil) + req, err := http.NewRequest("GET", "/v1/health/node/nope?dc=dc1", nil) if err != nil { t.Fatalf("err: %v", err) } @@ -143,8 +167,27 @@ func TestHealthNodeChecks(t *testing.T) { } assertIndex(t, resp) - // Should be 1 health check for the server + // Should be a non-nil empty list nodes := obj.(structs.HealthChecks) + if nodes == nil || len(nodes) != 0 { + t.Fatalf("bad: %v", obj) + } + + req, err = http.NewRequest("GET", + fmt.Sprintf("/v1/health/node/%s?dc=dc1", srv.agent.config.NodeName), nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp = httptest.NewRecorder() + obj, err = srv.HealthNodeChecks(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + assertIndex(t, resp) + + // Should be 1 health check for the server + nodes = obj.(structs.HealthChecks) if len(nodes) != 1 { t.Fatalf("bad: %v", obj) } @@ -158,6 +201,24 @@ func TestHealthServiceChecks(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + req, err := http.NewRequest("GET", "/v1/health/checks/consul?dc=dc1", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.HealthServiceChecks(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + assertIndex(t, resp) + + // Should be a non-nil empty list + nodes := obj.(structs.HealthChecks) + if nodes == nil || len(nodes) != 0 { + t.Fatalf("bad: %v", obj) + } + // Create a service check args := &structs.RegisterRequest{ Datacenter: "dc1", @@ -171,24 +232,24 @@ func TestHealthServiceChecks(t *testing.T) { } var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + if err = srv.agent.RPC("Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } - req, err := http.NewRequest("GET", "/v1/health/checks/consul?dc=dc1", nil) + req, err = http.NewRequest("GET", "/v1/health/checks/consul?dc=dc1", nil) if err != nil { t.Fatalf("err: %v", err) } - resp := httptest.NewRecorder() - obj, err := srv.HealthServiceChecks(resp, req) + resp = httptest.NewRecorder() + obj, err = srv.HealthServiceChecks(resp, req) if err != nil { t.Fatalf("err: %v", err) } assertIndex(t, resp) // Should be 1 health check for consul - nodes := obj.(structs.HealthChecks) + nodes = obj.(structs.HealthChecks) if len(nodes) != 1 { t.Fatalf("bad: %v", obj) } @@ -306,6 +367,59 @@ func TestHealthServiceNodes(t *testing.T) { if len(nodes) != 1 { t.Fatalf("bad: %v", obj) } + + req, err = http.NewRequest("GET", "/v1/health/service/nope?dc=dc1", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp = httptest.NewRecorder() + obj, err = srv.HealthServiceNodes(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + assertIndex(t, resp) + + // Should be a non-nil empty list + nodes = obj.(structs.CheckServiceNodes) + if nodes == nil || len(nodes) != 0 { + t.Fatalf("bad: %v", obj) + } + + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "test", + Service: "test", + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + req, err = http.NewRequest("GET", "/v1/health/service/test?dc=dc1", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp = httptest.NewRecorder() + obj, err = srv.HealthServiceNodes(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + assertIndex(t, resp) + + // Should be a non-nil empty list for checks + nodes = obj.(structs.CheckServiceNodes) + if len(nodes) != 1 || nodes[0].Checks == nil || len(nodes[0].Checks) != 0 { + t.Fatalf("bad: %v", obj) + } } func TestHealthServiceNodes_DistanceSort(t *testing.T) { diff --git a/command/agent/session_endpoint.go b/command/agent/session_endpoint.go index f3d8db22bc..0f2685465f 100644 --- a/command/agent/session_endpoint.go +++ b/command/agent/session_endpoint.go @@ -185,6 +185,11 @@ func (s *HTTPServer) SessionGet(resp http.ResponseWriter, req *http.Request) (in if err := s.agent.RPC("Session.Get", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.Sessions == nil { + out.Sessions = make(structs.Sessions, 0) + } return out.Sessions, nil } @@ -200,6 +205,11 @@ func (s *HTTPServer) SessionList(resp http.ResponseWriter, req *http.Request) (i if err := s.agent.RPC("Session.List", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.Sessions == nil { + out.Sessions = make(structs.Sessions, 0) + } return out.Sessions, nil } @@ -223,5 +233,10 @@ func (s *HTTPServer) SessionsForNode(resp http.ResponseWriter, req *http.Request if err := s.agent.RPC("Session.NodeSessions", &args, &out); err != nil { return nil, err } + + // Use empty list instead of nil + if out.Sessions == nil { + out.Sessions = make(structs.Sessions, 0) + } return out.Sessions, nil } diff --git a/command/agent/session_endpoint_test.go b/command/agent/session_endpoint_test.go index 960fdeec18..c7f744c84a 100644 --- a/command/agent/session_endpoint_test.go +++ b/command/agent/session_endpoint_test.go @@ -349,6 +349,22 @@ func TestSessionTTLRenew(t *testing.T) { } func TestSessionGet(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", "/v1/session/info/adf4238a-882b-9ddc-4a9d-5b6758e4159e", nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if respObj == nil || len(respObj) != 0 { + t.Fatalf("bad: %v", respObj) + } + }) + httpTest(t, func(srv *HTTPServer) { id := makeTestSession(t, srv) @@ -370,6 +386,22 @@ func TestSessionGet(t *testing.T) { } func TestSessionList(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", "/v1/session/list", nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionList(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if respObj == nil || len(respObj) != 0 { + t.Fatalf("bad: %v", respObj) + } + }) + httpTest(t, func(srv *HTTPServer) { var ids []string for i := 0; i < 10; i++ { @@ -393,6 +425,23 @@ func TestSessionList(t *testing.T) { } func TestSessionsForNode(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + req, err := http.NewRequest("GET", + "/v1/session/node/"+srv.agent.config.NodeName, nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionsForNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if respObj == nil || len(respObj) != 0 { + t.Fatalf("bad: %v", respObj) + } + }) + httpTest(t, func(srv *HTTPServer) { var ids []string for i := 0; i < 10; i++ { diff --git a/command/agent/ui_endpoint.go b/command/agent/ui_endpoint.go index 425e392ce9..c240879876 100644 --- a/command/agent/ui_endpoint.go +++ b/command/agent/ui_endpoint.go @@ -38,6 +38,19 @@ RPC: } return nil, err } + + // Use empty list instead of nil + for _, info := range out.Dump { + if info.Services == nil { + info.Services = make([]*structs.NodeService, 0) + } + if info.Checks == nil { + info.Checks = make([]*structs.HealthCheck, 0) + } + } + if out.Dump == nil { + out.Dump = make(structs.NodeDump, 0) + } return out.Dump, nil } @@ -73,7 +86,14 @@ RPC: // Return only the first entry if len(out.Dump) > 0 { - return out.Dump[0], nil + info := out.Dump[0] + if info.Services == nil { + info.Services = make([]*structs.NodeService, 0) + } + if info.Checks == nil { + info.Checks = make([]*structs.HealthCheck, 0) + } + return info, nil } return nil, nil } diff --git a/command/agent/ui_endpoint_test.go b/command/agent/ui_endpoint_test.go index 76e3f379ba..c37d7c0214 100644 --- a/command/agent/ui_endpoint_test.go +++ b/command/agent/ui_endpoint_test.go @@ -65,6 +65,17 @@ func TestUiNodes(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "test", + Address: "127.0.0.1", + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + req, err := http.NewRequest("GET", "/v1/internal/ui/nodes/dc1", nil) if err != nil { t.Fatalf("err: %v", err) @@ -77,9 +88,15 @@ func TestUiNodes(t *testing.T) { } assertIndex(t, resp) - // Should be 1 node for the server + // Should be 2 nodes, and all the empty lists should be non-nil nodes := obj.(structs.NodeDump) - if len(nodes) != 1 { + if len(nodes) != 2 || + nodes[0].Node != srv.agent.config.NodeName || + nodes[0].Services == nil || len(nodes[0].Services) != 1 || + nodes[0].Checks == nil || len(nodes[0].Checks) != 1 || + nodes[1].Node != "test" || + nodes[1].Services == nil || len(nodes[1].Services) != 0 || + nodes[1].Checks == nil || len(nodes[1].Checks) != 0 { t.Fatalf("bad: %v", obj) } } @@ -111,6 +128,38 @@ func TestUiNodeInfo(t *testing.T) { if node.Node != srv.agent.config.NodeName { t.Fatalf("bad: %v", node) } + + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "test", + Address: "127.0.0.1", + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + req, err = http.NewRequest("GET", "/v1/internal/ui/node/test", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp = httptest.NewRecorder() + obj, err = srv.UINodeInfo(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + assertIndex(t, resp) + + // Should be non-nil empty lists for services and checks + node = obj.(*structs.NodeInfo) + if node.Node != "test" || + node.Services == nil || len(node.Services) != 0 || + node.Checks == nil || len(node.Checks) != 0 { + t.Fatalf("bad: %v", node) + } } func TestSummarizeServices(t *testing.T) { From 9972eb249884cb05727984d87beae832400a9b66 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 15:44:26 -0800 Subject: [PATCH 011/123] Adds a check for in-band error returns in the coordinate RaftApply. --- consul/coordinate_endpoint.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/consul/coordinate_endpoint.go b/consul/coordinate_endpoint.go index e5774764e6..5fc9c5f36e 100644 --- a/consul/coordinate_endpoint.go +++ b/consul/coordinate_endpoint.go @@ -92,9 +92,13 @@ func (c *Coordinate) batchApplyUpdates() error { t := structs.CoordinateBatchUpdateType | structs.IgnoreUnknownTypeFlag slice := updates[start:end] - if _, err := c.srv.raftApply(t, slice); err != nil { + resp, err := c.srv.raftApply(t, slice) + if err != nil { return err } + if respErr, ok := resp.(error); ok { + return respErr + } } return nil } From f1f9d30adecb7f5dd1b47689613990bf91b8da30 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 15:30:53 -0800 Subject: [PATCH 012/123] Extends the session TTL max to 24 hours, and adds a warning to the docs. --- consul/session_endpoint_test.go | 6 +++--- consul/structs/structs.go | 2 +- website/source/docs/agent/http/session.html.markdown | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/consul/session_endpoint_test.go b/consul/session_endpoint_test.go index db8035f3b0..59b6ce8995 100644 --- a/consul/session_endpoint_test.go +++ b/consul/session_endpoint_test.go @@ -522,18 +522,18 @@ func TestSessionEndpoint_Apply_BadTTL(t *testing.T) { if err == nil { t.Fatal("expected error") } - if err.Error() != "Invalid Session TTL '5000000000', must be between [10s=1h0m0s]" { + if err.Error() != "Invalid Session TTL '5000000000', must be between [10s=24h0m0s]" { t.Fatalf("incorrect error message: %s", err.Error()) } // more than SessionTTLMax - arg.Session.TTL = "4000s" + arg.Session.TTL = "100000s" err = msgpackrpc.CallWithCodec(codec, "Session.Apply", &arg, &out) if err == nil { t.Fatal("expected error") } - if err.Error() != "Invalid Session TTL '4000000000000', must be between [10s=1h0m0s]" { + if err.Error() != "Invalid Session TTL '100000000000000', must be between [10s=24h0m0s]" { t.Fatalf("incorrect error message: %s", err.Error()) } } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 72ecee00be..a4da32658c 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -547,7 +547,7 @@ const ( ) const ( - SessionTTLMax = 3600 * time.Second + SessionTTLMax = 24 * time.Hour SessionTTLMultiplier = 2 ) diff --git a/website/source/docs/agent/http/session.html.markdown b/website/source/docs/agent/http/session.html.markdown index 88a5a360f2..20d72fd4cb 100644 --- a/website/source/docs/agent/http/session.html.markdown +++ b/website/source/docs/agent/http/session.html.markdown @@ -65,10 +65,13 @@ causes any locks that are held to be deleted. `delete` is useful for creating ep key/value entries. The `TTL` field is a duration string, and like `LockDelay` it can use "s" as -a suffix for seconds. If specified, it must be between 10s and 3600s currently. +a suffix for seconds. If specified, it must be between 10s and 86400s currently. When provided, the session is invalidated if it is not renewed before the TTL -expires. See the [session internals page](/docs/internals/sessions.html) for more -documentation of this feature. +expires. The lowest practical TTL should be used to keep the number of managed +sessions low. When locks are forcibly expired, such as during a leader election, +sessions may not be reaped for up to double this TTL, so long TTL values (>1 hour) +should be avoided. See the [session internals page](/docs/internals/sessions.html) +for more documentation of this feature. The return code is 200 on success and returns the ID of the created session: From ce0881a99a71be94a97ae05bb6b1e744aaacbb87 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 4 Nov 2015 15:16:21 -0800 Subject: [PATCH 013/123] Adds a new management ACL for prepared queries. --- acl/acl.go | 24 ++++++++++++++++++++++++ acl/acl_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/acl/acl.go b/acl/acl.go index 8492177e30..344afc5c34 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -70,6 +70,12 @@ type ACL interface { // ACLModify checks for permission to manipulate ACLs ACLModify() bool + + // QueryList checks for permission to list all the prepared queries. + QueryList() bool + + // QueryModify checks for permission to modify any prepared query. + QueryModify() bool } // StaticACL is used to implement a base ACL policy. It either @@ -124,6 +130,14 @@ func (s *StaticACL) ACLModify() bool { return s.allowManage } +func (s *StaticACL) QueryList() bool { + return s.allowManage +} + +func (s *StaticACL) QueryModify() bool { + return s.allowManage +} + // AllowAll returns an ACL rule that allows all operations func AllowAll() ACL { return allowAll @@ -374,3 +388,13 @@ func (p *PolicyACL) ACLList() bool { func (p *PolicyACL) ACLModify() bool { return p.parent.ACLModify() } + +// QueryList checks if listing of all prepared queries is allowed. +func (p *PolicyACL) QueryList() bool { + return p.parent.QueryList() +} + +// QueryModify checks if modifying of any prepared query is allowed. +func (p *PolicyACL) QueryModify() bool { + return p.parent.QueryModify() +} diff --git a/acl/acl_test.go b/acl/acl_test.go index 1b83c81dec..06cdfb7557 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -65,6 +65,12 @@ func TestStaticACL(t *testing.T) { if all.ACLModify() { t.Fatalf("should not allow") } + if all.QueryList() { + t.Fatalf("should not allow") + } + if all.QueryModify() { + t.Fatalf("should not allow") + } if none.KeyRead("foobar") { t.Fatalf("should not allow") @@ -102,6 +108,12 @@ func TestStaticACL(t *testing.T) { if none.ACLModify() { t.Fatalf("should not allow") } + if none.QueryList() { + t.Fatalf("should not allow") + } + if none.QueryModify() { + t.Fatalf("should not allow") + } if !manage.KeyRead("foobar") { t.Fatalf("should allow") @@ -133,6 +145,12 @@ func TestStaticACL(t *testing.T) { if !manage.ACLModify() { t.Fatalf("should allow") } + if !manage.QueryList() { + t.Fatalf("should allow") + } + if !manage.QueryModify() { + t.Fatalf("should allow") + } } func TestPolicyACL(t *testing.T) { @@ -369,6 +387,20 @@ func TestPolicyACL_Parent(t *testing.T) { t.Fatalf("Write fail: %#v", c) } } + + // Check some management functions that chain up + if acl.ACLList() { + t.Fatalf("should not allow") + } + if acl.ACLModify() { + t.Fatalf("should not allow") + } + if acl.QueryList() { + t.Fatalf("should not allow") + } + if acl.QueryModify() { + t.Fatalf("should not allow") + } } func TestPolicyACL_Keyring(t *testing.T) { From 7babcefc59744b51df9226aab59b3aaf11ce5bd1 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 6 Nov 2015 16:59:32 -0800 Subject: [PATCH 014/123] Changes structs and state store for prepared queries. --- consul/state/query.go | 241 +++++++++++++ consul/state/query_test.go | 580 +++++++++++++++++++++++++++++++ consul/state/schema.go | 36 ++ consul/state/state_store.go | 46 ++- consul/state/state_store_test.go | 58 ++++ consul/structs/query.go | 151 ++++++++ consul/structs/structs.go | 31 ++ consul/structs/structs_test.go | 111 ++++++ 8 files changed, 1245 insertions(+), 9 deletions(-) create mode 100644 consul/state/query.go create mode 100644 consul/state/query_test.go create mode 100644 consul/structs/query.go diff --git a/consul/state/query.go b/consul/state/query.go new file mode 100644 index 0000000000..c7a9082dd4 --- /dev/null +++ b/consul/state/query.go @@ -0,0 +1,241 @@ +package state + +import ( + "fmt" + "strings" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/go-memdb" +) + +// Queries is used to pull all the prepared queries from the snapshot. +func (s *StateSnapshot) Queries() (memdb.ResultIterator, error) { + iter, err := s.tx.Get("queries", "id") + if err != nil { + return nil, err + } + return iter, nil +} + +// Query is used when restoring from a snapshot. For general inserts, use +// QuerySet. +func (s *StateRestore) Query(query *structs.PreparedQuery) error { + if err := s.tx.Insert("queries", query); err != nil { + return fmt.Errorf("failed restoring query: %s", err) + } + + if err := indexUpdateMaxTxn(s.tx, query.ModifyIndex, "queries"); err != nil { + return fmt.Errorf("failed updating index: %s", err) + } + + s.watches.Arm("queries") + return nil +} + +// QuerySet is used to create or update a prepared query. +func (s *StateStore) QuerySet(idx uint64, query *structs.PreparedQuery) error { + tx := s.db.Txn(true) + defer tx.Abort() + + // Call set on the Query. + if err := s.querySetTxn(tx, idx, query); err != nil { + return err + } + + tx.Commit() + return nil +} + +// querySetTxn is the inner method used to insert a prepared query with the +// proper indexes into the state store. +func (s *StateStore) querySetTxn(tx *memdb.Txn, idx uint64, query *structs.PreparedQuery) error { + // Check that the ID is set. + if query.ID == "" { + return ErrMissingQueryID + } + + // Check for an existing query. + existing, err := tx.First("queries", "id", query.ID) + if err != nil { + return fmt.Errorf("failed query lookup: %s", err) + } + + // Set the indexes. + if existing != nil { + query.CreateIndex = existing.(*structs.PreparedQuery).CreateIndex + query.ModifyIndex = idx + } else { + query.CreateIndex = idx + query.ModifyIndex = idx + } + + // Verify that the name doesn't alias any existing ID. If we didn't do + // this then a bad actor could steal traffic away from an existing DNS + // entry. + if query.Name != "" { + existing, err := tx.First("queries", "id", query.Name) + + // This is a little unfortunate but the UUID index will complain + // if the name isn't formatted like a UUID, so we can safely + // ignore any UUID format-related errors. + if err != nil && !strings.Contains(err.Error(), "UUID") { + return fmt.Errorf("failed query lookup: %s", err) + } + if existing != nil { + return fmt.Errorf("name '%s' aliases an existing query id", query.Name) + } + } + + // Verify that the session exists. + if query.Session != "" { + sess, err := tx.First("sessions", "id", query.Session) + if err != nil { + return fmt.Errorf("failed session lookup: %s", err) + } + if sess == nil { + return fmt.Errorf("invalid session %#v", query.Session) + } + } + + // Verify that the service exists. + service, err := tx.First("services", "service", query.Service.Service) + if err != nil { + return fmt.Errorf("failed service lookup: %s", err) + } + if service == nil { + return fmt.Errorf("invalid service %#v", query.Service.Service) + } + + // Insert the query. + if err := tx.Insert("queries", query); err != nil { + return fmt.Errorf("failed inserting query: %s", err) + } + if err := tx.Insert("index", &IndexEntry{"queries", idx}); err != nil { + return fmt.Errorf("failed updating index: %s", err) + } + + tx.Defer(func() { s.tableWatches["queries"].Notify() }) + return nil +} + +// QueryDelete deletes the given query by ID. +func (s *StateStore) QueryDelete(idx uint64, queryID string) error { + tx := s.db.Txn(true) + defer tx.Abort() + + watches := NewDumbWatchManager(s.tableWatches) + if err := s.queryDeleteTxn(tx, idx, watches, queryID); err != nil { + return fmt.Errorf("failed query delete: %s", err) + } + + tx.Defer(func() { watches.Notify() }) + tx.Commit() + return nil +} + +// queryDeleteTxn is the inner method used to delete a prepared query with the +// proper indexes into the state store. +func (s *StateStore) queryDeleteTxn(tx *memdb.Txn, idx uint64, watches *DumbWatchManager, + queryID string) error { + // Pull the query. + query, err := tx.First("queries", "id", queryID) + if err != nil { + return fmt.Errorf("failed query lookup: %s", err) + } + if query == nil { + return nil + } + + // Delete the query and update the index. + if err := tx.Delete("queries", query); err != nil { + return fmt.Errorf("failed query delete: %s", err) + } + if err := tx.Insert("index", &IndexEntry{"queries", idx}); err != nil { + return fmt.Errorf("failed updating index: %s", err) + } + + watches.Arm("queries") + return nil +} + +// QueryGet returns the given prepared query by ID. +func (s *StateStore) QueryGet(queryID string) (uint64, *structs.PreparedQuery, error) { + tx := s.db.Txn(false) + defer tx.Abort() + + // Get the table index. + idx := maxIndexTxn(tx, s.getWatchTables("QueryGet")...) + + // Look up the query by its ID. + query, err := tx.First("queries", "id", queryID) + if err != nil { + return 0, nil, fmt.Errorf("failed query lookup: %s", err) + } + if query != nil { + return idx, query.(*structs.PreparedQuery), nil + } + return idx, nil, nil +} + +// QueryLookup returns the given prepared query by looking up an ID or Name. +func (s *StateStore) QueryLookup(queryIDOrName string) (uint64, *structs.PreparedQuery, error) { + tx := s.db.Txn(false) + defer tx.Abort() + + // Get the table index. + idx := maxIndexTxn(tx, s.getWatchTables("QueryLookup")...) + + // Explicitly ban an empty query. This will never match an ID and the + // schema is set up so it will never match a query with an empty name, + // but we check it here to be explicit about it (we'd never want to + // return the results from the first query w/o a name). + if queryIDOrName == "" { + return idx, nil, ErrMissingQueryID + } + + // Try first by ID. + query, err := tx.First("queries", "id", queryIDOrName) + + // This is a little unfortunate but the UUID index will complain + // if the name isn't formatted like a UUID, so we can safely + // ignore any UUID format-related errors. + if err != nil && !strings.Contains(err.Error(), "UUID") { + return 0, nil, fmt.Errorf("failed query lookup: %s", err) + } + if query != nil { + return idx, query.(*structs.PreparedQuery), nil + } + + // Then try by name. + query, err = tx.First("queries", "name", queryIDOrName) + if err != nil { + return 0, nil, fmt.Errorf("failed query lookup: %s", err) + } + if query != nil { + return idx, query.(*structs.PreparedQuery), nil + } + + return idx, nil, nil +} + +// QueryList returns all the prepared queries. +func (s *StateStore) QueryList() (uint64, structs.PreparedQueries, error) { + tx := s.db.Txn(false) + defer tx.Abort() + + // Get the table index. + idx := maxIndexTxn(tx, s.getWatchTables("QueryList")...) + + // Query all of the prepared queries in the state store. + queries, err := tx.Get("queries", "id") + if err != nil { + return 0, nil, fmt.Errorf("failed query lookup: %s", err) + } + + // Go over all of the queries and build the response. + var result structs.PreparedQueries + for query := queries.Next(); query != nil; query = queries.Next() { + result = append(result, query.(*structs.PreparedQuery)) + } + return idx, result, nil +} diff --git a/consul/state/query_test.go b/consul/state/query_test.go new file mode 100644 index 0000000000..645c96fa36 --- /dev/null +++ b/consul/state/query_test.go @@ -0,0 +1,580 @@ +package state + +import ( + "reflect" + "strings" + "testing" + + "github.com/hashicorp/consul/consul/structs" +) + +func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { + s := testStateStore(t) + + // Querying with no results returns nil. + idx, res, err := s.QueryGet(testUUID()) + if idx != 0 || res != nil || err != nil { + t.Fatalf("expected (0, nil, nil), got: (%d, %#v, %#v)", idx, res, err) + } + + // Inserting a query with empty ID is disallowed. + if err := s.QuerySet(1, &structs.PreparedQuery{}); err == nil { + t.Fatalf("expected %#v, got: %#v", ErrMissingQueryID, err) + } + + // Index is not updated if nothing is saved. + if idx := s.maxIndex("queries"); idx != 0 { + t.Fatalf("bad index: %d", idx) + } + + // Build a legit-looking query with the most basic options. + query := &structs.PreparedQuery{ + ID: testUUID(), + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + + // The set will still fail because the service isn't registered yet. + err = s.QuerySet(1, query) + if err == nil || !strings.Contains(err.Error(), "invalid service") { + t.Fatalf("bad: %v", err) + } + + // Index is not updated if nothing is saved. + if idx := s.maxIndex("queries"); idx != 0 { + t.Fatalf("bad index: %d", idx) + } + + // Now register the service. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + + // This should go through. + if err := s.QuerySet(3, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 3 { + t.Fatalf("bad index: %d", idx) + } + + // Read it back out and verify it. + expected := &structs.PreparedQuery{ + ID: query.ID, + Service: structs.ServiceQuery{ + Service: "redis", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 3, + ModifyIndex: 3, + }, + } + idx, actual, err := s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Give it a name and set it again. + query.Name = "test-query" + if err := s.QuerySet(4, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 4 { + t.Fatalf("bad index: %d", idx) + } + + // Read it back and verify the data was updated as well as the index. + expected.Name = "test-query" + expected.ModifyIndex = 4 + idx, actual, err = s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 4 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Try to tie it to a bogus session. + query.Session = testUUID() + err = s.QuerySet(5, query) + if err == nil || !strings.Contains(err.Error(), "invalid session") { + t.Fatalf("bad: %v", err) + } + + // Index is not updated if nothing is saved. + if idx := s.maxIndex("queries"); idx != 4 { + t.Fatalf("bad index: %d", idx) + } + + // Now make a session and try again. + session := &structs.Session{ + ID: query.Session, + Node: "foo", + } + if err := s.SessionCreate(5, session); err != nil { + t.Fatalf("err: %s", err) + } + if err := s.QuerySet(6, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 6 { + t.Fatalf("bad index: %d", idx) + } + + // Read it back and verify the data was updated as well as the index. + expected.Session = query.Session + expected.ModifyIndex = 6 + idx, actual, err = s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 6 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Finally, try to abuse the system by trying to register a query whose + // name aliases a real query ID. + evil := &structs.PreparedQuery{ + ID: testUUID(), + Name: query.ID, + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + err = s.QuerySet(7, evil) + if err == nil || !strings.Contains(err.Error(), "aliases an existing query") { + t.Fatalf("bad: %v", err) + } + + // Index is not updated if nothing is saved. + if idx := s.maxIndex("queries"); idx != 6 { + t.Fatalf("bad index: %d", idx) + } + + // Sanity check to make sure it's not there. + idx, actual, err = s.QueryGet(evil.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 6 { + t.Fatalf("bad index: %d", idx) + } + if actual != nil { + t.Fatalf("bad: %v", actual) + } +} + +func TestStateStore_Query_QueryDelete(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + + // Create a new query. + query := &structs.PreparedQuery{ + ID: testUUID(), + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + + // Deleting a query that doesn't exist should be a no-op. + if err := s.QueryDelete(3, query.ID); err != nil { + t.Fatalf("err: %s", err) + } + + // Index is not updated if nothing is saved. + if idx := s.maxIndex("queries"); idx != 0 { + t.Fatalf("bad index: %d", idx) + } + + // Now add the query to the data store. + if err := s.QuerySet(3, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 3 { + t.Fatalf("bad index: %d", idx) + } + + // Read it back out and verify it. + expected := &structs.PreparedQuery{ + ID: query.ID, + Service: structs.ServiceQuery{ + Service: "redis", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 3, + ModifyIndex: 3, + }, + } + idx, actual, err := s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Now delete it. + if err := s.QueryDelete(4, query.ID); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 4 { + t.Fatalf("bad index: %d", idx) + } + + // Sanity check to make sure it's not there. + idx, actual, err = s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 4 { + t.Fatalf("bad index: %d", idx) + } + if actual != nil { + t.Fatalf("bad: %v", actual) + } +} + +func TestStateStore_Query_QueryLookup(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + + // Create a new query. + query := &structs.PreparedQuery{ + ID: testUUID(), + Name: "my-test-query", + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + + // Try to lookup a query that's not there using something that looks + // like a real ID. + idx, actual, err := s.QueryLookup(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 0 { + t.Fatalf("bad index: %d", idx) + } + if actual != nil { + t.Fatalf("bad: %v", actual) + } + + // Try to lookup a query that's not there using something that looks + // like a name + idx, actual, err = s.QueryLookup(query.Name) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 0 { + t.Fatalf("bad index: %d", idx) + } + if actual != nil { + t.Fatalf("bad: %v", actual) + } + + // Now actually insert the query. + if err := s.QuerySet(3, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Make sure the index got updated. + if idx := s.maxIndex("queries"); idx != 3 { + t.Fatalf("bad index: %d", idx) + } + + // Read it back out using the ID and verify it. + expected := &structs.PreparedQuery{ + ID: query.ID, + Name: "my-test-query", + Service: structs.ServiceQuery{ + Service: "redis", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 3, + ModifyIndex: 3, + }, + } + idx, actual, err = s.QueryLookup(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Read it back using the name and verify it again. + idx, actual, err = s.QueryLookup(query.Name) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 3 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + + // Make sure an empty lookup is well-behaved if there are actual queries + // in the state store. + if _, _, err = s.QueryLookup(""); err != ErrMissingQueryID { + t.Fatalf("bad: %v", err) + } +} + +func TestStateStore_Query_QueryList(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + testRegisterService(t, s, 3, "foo", "mongodb") + + // Create some queries. + queries := structs.PreparedQueries{ + &structs.PreparedQuery{ + ID: testUUID(), + Name: "alice", + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + &structs.PreparedQuery{ + ID: testUUID(), + Name: "bob", + Service: structs.ServiceQuery{ + Service: "mongodb", + }, + }, + } + + // Force the sort order of the UUIDs before we create them so the + // order is deterministic. + queries[0].ID = "a" + queries[0].ID[1:] + queries[1].ID = "b" + queries[1].ID[1:] + + // Now create the queries. + for i, query := range queries { + if err := s.QuerySet(uint64(4+i), query); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Read it back and verify. + expected := structs.PreparedQueries{ + &structs.PreparedQuery{ + ID: queries[0].ID, + Name: "alice", + Service: structs.ServiceQuery{ + Service: "redis", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 4, + ModifyIndex: 4, + }, + }, + &structs.PreparedQuery{ + ID: queries[1].ID, + Name: "bob", + Service: structs.ServiceQuery{ + Service: "mongodb", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 5, + ModifyIndex: 5, + }, + }, + } + idx, actual, err := s.QueryList() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 5 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } +} + +func TestStateStore_Query_Snapshot_Restore(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + testRegisterService(t, s, 3, "foo", "mongodb") + + // Create some queries. + queries := structs.PreparedQueries{ + &structs.PreparedQuery{ + ID: testUUID(), + Name: "alice", + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + &structs.PreparedQuery{ + ID: testUUID(), + Name: "bob", + Service: structs.ServiceQuery{ + Service: "mongodb", + }, + }, + } + + // Force the sort order of the UUIDs before we create them so the + // order is deterministic. + queries[0].ID = "a" + queries[0].ID[1:] + queries[1].ID = "b" + queries[1].ID[1:] + + // Now create the queries. + for i, query := range queries { + if err := s.QuerySet(uint64(4+i), query); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Snapshot the queries. + snap := s.Snapshot() + defer snap.Close() + + // Alter the real state store. + if err := s.QueryDelete(6, queries[0].ID); err != nil { + t.Fatalf("err: %s", err) + } + + // Verify the snapshot. + if idx := snap.LastIndex(); idx != 5 { + t.Fatalf("bad index: %d", idx) + } + expected := structs.PreparedQueries{ + &structs.PreparedQuery{ + ID: queries[0].ID, + Name: "alice", + Service: structs.ServiceQuery{ + Service: "redis", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 4, + ModifyIndex: 4, + }, + }, + &structs.PreparedQuery{ + ID: queries[1].ID, + Name: "bob", + Service: structs.ServiceQuery{ + Service: "mongodb", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 5, + ModifyIndex: 5, + }, + }, + } + iter, err := snap.Queries() + if err != nil { + t.Fatalf("err: %s", err) + } + var dump structs.PreparedQueries + for query := iter.Next(); query != nil; query = iter.Next() { + dump = append(dump, query.(*structs.PreparedQuery)) + } + if !reflect.DeepEqual(dump, expected) { + t.Fatalf("bad: %v", dump) + } + + // Restore the values into a new state store. + func() { + s := testStateStore(t) + restore := s.Restore() + for _, query := range dump { + if err := restore.Query(query); err != nil { + t.Fatalf("err: %s", err) + } + } + restore.Commit() + + // Read the restored queries back out and verify that they + // match. + idx, actual, err := s.QueryList() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 5 { + t.Fatalf("bad index: %d", idx) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %v", actual) + } + }() +} + +func TestStateStore_Query_Watches(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + + query := &structs.PreparedQuery{ + ID: testUUID(), + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + + // Call functions that update the queries table and make sure a watch + // fires each time. + verifyWatch(t, s.getTableWatch("queries"), func() { + if err := s.QuerySet(3, query); err != nil { + t.Fatalf("err: %s", err) + } + }) + verifyWatch(t, s.getTableWatch("queries"), func() { + if err := s.QueryDelete(4, query.ID); err != nil { + t.Fatalf("err: %s", err) + } + }) + verifyWatch(t, s.getTableWatch("queries"), func() { + restore := s.Restore() + if err := restore.Query(query); err != nil { + t.Fatalf("err: %s", err) + } + restore.Commit() + }) +} diff --git a/consul/state/schema.go b/consul/state/schema.go index c80012f05a..4bb354ad64 100644 --- a/consul/state/schema.go +++ b/consul/state/schema.go @@ -30,6 +30,7 @@ func stateStoreSchema() *memdb.DBSchema { sessionChecksTableSchema, aclsTableSchema, coordinatesTableSchema, + queriesTableSchema, } // Add the tables to the root schema @@ -365,3 +366,38 @@ func coordinatesTableSchema() *memdb.TableSchema { }, } } + +// queriesTableSchema returns a new table schema used for storing +// prepared queries. +func queriesTableSchema() *memdb.TableSchema { + return &memdb.TableSchema{ + Name: "queries", + Indexes: map[string]*memdb.IndexSchema{ + "id": &memdb.IndexSchema{ + Name: "id", + AllowMissing: false, + Unique: true, + Indexer: &memdb.UUIDFieldIndex{ + Field: "ID", + }, + }, + "name": &memdb.IndexSchema{ + Name: "name", + AllowMissing: true, + Unique: true, + Indexer: &memdb.StringFieldIndex{ + Field: "Name", + Lowercase: true, + }, + }, + "session": &memdb.IndexSchema{ + Name: "session", + AllowMissing: true, + Unique: false, + Indexer: &memdb.UUIDFieldIndex{ + Field: "Session", + }, + }, + }, + } +} diff --git a/consul/state/state_store.go b/consul/state/state_store.go index c3393c68a0..286700a67d 100644 --- a/consul/state/state_store.go +++ b/consul/state/state_store.go @@ -24,9 +24,13 @@ var ( // is attempted with an empty session ID. ErrMissingSessionID = errors.New("Missing session ID") - // ErrMissingACLID is returned when a session set is called on - // a session with an empty ID. + // ErrMissingACLID is returned when an ACL set is called on + // an ACL with an empty ID. ErrMissingACLID = errors.New("Missing ACL ID") + + // ErrMissingQueryID is returned when a Query set is called on + // a Query with an empty ID. + ErrMissingQueryID = errors.New("Missing Query ID") ) // StateStore is where we store all of Consul's state, including @@ -409,6 +413,8 @@ func (s *StateStore) getWatchTables(method string) []string { return []string{"acls"} case "Coordinates": return []string{"coordinates"} + case "QueryGet", "QueryLookup", "QueryList": + return []string{"queries"} } panic(fmt.Sprintf("Unknown method %s", method)) @@ -2120,15 +2126,37 @@ func (s *StateStore) deleteSessionTxn(tx *memdb.Txn, idx uint64, watches *DumbWa if err != nil { return fmt.Errorf("failed session checks lookup: %s", err) } - var objs []interface{} - for mapping := mappings.Next(); mapping != nil; mapping = mappings.Next() { - objs = append(objs, mapping) + { + var objs []interface{} + for mapping := mappings.Next(); mapping != nil; mapping = mappings.Next() { + objs = append(objs, mapping) + } + + // Do the delete in a separate loop so we don't trash the iterator. + for _, obj := range objs { + if err := tx.Delete("session_checks", obj); err != nil { + return fmt.Errorf("failed deleting session check: %s", err) + } + } } - // Do the delete in a separate loop so we don't trash the iterator. - for _, obj := range objs { - if err := tx.Delete("session_checks", obj); err != nil { - return fmt.Errorf("failed deleting session check: %s", err) + // Delete any prepared queries. + queries, err := tx.Get("queries", "session", sessionID) + if err != nil { + return fmt.Errorf("failed query lookup: %s", err) + } + { + var objs []interface{} + for query := queries.Next(); query != nil; query = queries.Next() { + objs = append(objs, query) + } + + // Do the delete in a separate loop so we don't trash the iterator. + for _, obj := range objs { + q := obj.(*structs.PreparedQuery) + if err := s.queryDeleteTxn(tx, idx, watches, q.ID); err != nil { + return fmt.Errorf("failed query delete: %s", err) + } } } diff --git a/consul/state/state_store_test.go b/consul/state/state_store_test.go index 24b7664b55..c022a4c464 100644 --- a/consul/state/state_store_test.go +++ b/consul/state/state_store_test.go @@ -4445,6 +4445,64 @@ func TestStateStore_Session_Invalidate_Key_Delete_Behavior(t *testing.T) { } } +func TestStateStore_Session_Invalidate_Query_Delete(t *testing.T) { + s := testStateStore(t) + + // Set up our test environment. + testRegisterNode(t, s, 1, "foo") + testRegisterService(t, s, 2, "foo", "redis") + session := &structs.Session{ + ID: testUUID(), + Node: "foo", + } + if err := s.SessionCreate(3, session); err != nil { + t.Fatalf("err: %v", err) + } + query := &structs.PreparedQuery{ + ID: testUUID(), + Session: session.ID, + Service: structs.ServiceQuery{ + Service: "redis", + }, + } + if err := s.QuerySet(4, query); err != nil { + t.Fatalf("err: %s", err) + } + + // Invalidate the session and make sure the watches fire. + verifyWatch(t, s.getTableWatch("sessions"), func() { + verifyWatch(t, s.getTableWatch("queries"), func() { + if err := s.SessionDestroy(5, session.ID); err != nil { + t.Fatalf("err: %v", err) + } + }) + }) + + // Make sure the session is gone. + idx, s2, err := s.SessionGet(session.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if s2 != nil { + t.Fatalf("session should be invalidated") + } + if idx != 5 { + t.Fatalf("bad index: %d", idx) + } + + // Make sure the query is gone and the index is updated. + idx, q2, err := s.QueryGet(query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 5 { + t.Fatalf("bad index: %d", idx) + } + if q2 != nil { + t.Fatalf("bad: %v", q2) + } +} + func TestStateStore_ACLSet_ACLGet(t *testing.T) { s := testStateStore(t) diff --git a/consul/structs/query.go b/consul/structs/query.go new file mode 100644 index 0000000000..0b534aca90 --- /dev/null +++ b/consul/structs/query.go @@ -0,0 +1,151 @@ +package structs + +import ( + "time" +) + +const ( + QueryOrderShuffle = "shuffle" + QueryOrderSort = "near_agent" +) + +const ( + QueryTTLMax = 24 * time.Hour + QueryTTLMin = 10 * time.Second +) + +// 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 + + // Sort has one of the QueryOrder* options which control how the output + // is sorted. If this is left blank we default to "shuffle". + Sort string +} + +// PreparedQuery defines a complete prepared query, and is the structure we +// maintain in the state store. +type PreparedQuery 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 + + // TTL is the time to live for the query itself. If this is omitted then + // the query will not expire (unless tied to a session). + TTL string + + // Session is an optional session to tie this query's lifetime to. If + // this is omitted then the query will not expire (unless given a TTL). + 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 + + RaftIndex +} + +type PreparedQueries []*PreparedQuery + +type QueryOp string + +const ( + QueryCreate QueryOp = "create" + QueryUpdate = "update" + QueryDelete = "delete" +) + +// QueryRequest is used to create or change prepared queries. +type QueryRequest struct { + Datacenter string + Op QueryOp + Query PreparedQuery + WriteRequest +} + +// RequestDatacenter returns the datacenter for a given request. +func (q *QueryRequest) RequestDatacenter() string { + return q.Datacenter +} + +// QuerySpecificRequest is used to execute a prepared query. +type QuerySpecificRequest struct { + Datacenter string + QueryIDOrName string + Source QuerySource + QueryOptions +} + +// RequestDatacenter returns the datacenter for a given request. +func (q *QuerySpecificRequest) RequestDatacenter() string { + return q.Datacenter +} + +// QueryRemoteRequest is used when running a local query in a remote +// datacenter. We have to ship the entire query over since it won't be +// present in the remote state store. +type QueryRemoteRequest struct { + Datacenter string + Query PreparedQuery + QueryOptions +} + +// RequestDatacenter returns the datacenter for a given request. +func (q *QueryRemoteRequest) RequestDatacenter() string { + return q.Datacenter +} + +// QueryExecutionResponse has the results of executing a query. +type QueryExecutionResponse struct { + Nodes CheckServiceNodes + DNS QueryDNSOptions +} diff --git a/consul/structs/structs.go b/consul/structs/structs.go index a4da32658c..e3cf706d57 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -3,6 +3,7 @@ package structs import ( "bytes" "fmt" + "math/rand" "reflect" "time" @@ -34,6 +35,7 @@ const ( ACLRequestType TombstoneRequestType CoordinateBatchUpdateType + QueryRequestType ) const ( @@ -403,6 +405,35 @@ type CheckServiceNode struct { } type CheckServiceNodes []CheckServiceNode +// Shuffle does an in-place random shuffle using the Fisher-Yates algorithm. +func (nodes CheckServiceNodes) Shuffle() { + for i := len(nodes) - 1; i > 0; i-- { + j := rand.Int31() % int32(i+1) + nodes[i], nodes[j] = nodes[j], nodes[i] + } +} + +// Filter removes nodes that are failing health checks (and any non-passing +// check if that option is selected). Note that this returns the filtered +// results AND modifies the receiver for performance. +func (nodes CheckServiceNodes) Filter(onlyPassing bool) CheckServiceNodes { + n := len(nodes) +OUTER: + for i := 0; i < n; i++ { + node := nodes[i] + for _, check := range node.Checks { + if check.Status == HealthCritical || + (onlyPassing && check.Status != HealthPassing) { + nodes[i], nodes[n-1] = nodes[n-1], CheckServiceNode{} + n-- + i-- + continue OUTER + } + } + } + return nodes[:n] +} + // NodeInfo is used to dump all associated information about // a node. This is currently used for the UI only, as it is // rather expensive to generate. diff --git a/consul/structs/structs_test.go b/consul/structs/structs_test.go index 30c1e8983b..4972e191af 100644 --- a/consul/structs/structs_test.go +++ b/consul/structs/structs_test.go @@ -209,6 +209,117 @@ func TestStructs_HealthCheck_IsSame(t *testing.T) { check(&other.ServiceName) } +func TestStructs_CheckServiceNodes_Shuffle(t *testing.T) { + nodes := CheckServiceNodes{ + CheckServiceNode{ + Node: &Node{ + Node: "node1", + Address: "127.0.0.1", + }, + }, + CheckServiceNode{ + Node: &Node{ + Node: "node2", + Address: "127.0.0.2", + }, + }, + CheckServiceNode{ + Node: &Node{ + Node: "node3", + Address: "127.0.0.3", + }, + }, + } + + // Make a copy to shuffle and make sure it matches initially. + twiddle := make(CheckServiceNodes, len(nodes)) + if n := copy(twiddle, nodes); n != len(nodes) { + t.Fatalf("bad: %d", n) + } + if !reflect.DeepEqual(twiddle, nodes) { + t.Fatalf("bad: %v", twiddle) + } + + // Give this lots of tries to randomize. If we find a case that's + // not equal we can end the test, otherwise we will call shenanigans. + for i := 0; i < 100; i++ { + twiddle.Shuffle() + if !reflect.DeepEqual(twiddle, nodes) { + return + } + } + t.Fatalf("shuffle is not working") +} + +func TestStructs_CheckServiceNodes_Filter(t *testing.T) { + nodes := CheckServiceNodes{ + CheckServiceNode{ + Node: &Node{ + Node: "node1", + Address: "127.0.0.1", + }, + Checks: HealthChecks{ + &HealthCheck{ + Status: HealthWarning, + }, + }, + }, + CheckServiceNode{ + Node: &Node{ + Node: "node2", + Address: "127.0.0.2", + }, + Checks: HealthChecks{ + &HealthCheck{ + Status: HealthPassing, + }, + }, + }, + CheckServiceNode{ + Node: &Node{ + Node: "node3", + Address: "127.0.0.3", + }, + Checks: HealthChecks{ + &HealthCheck{ + Status: HealthCritical, + }, + }, + }, + } + + // Test the case where warnings are allowed. + { + twiddle := make(CheckServiceNodes, len(nodes)) + if n := copy(twiddle, nodes); n != len(nodes) { + t.Fatalf("bad: %d", n) + } + filtered := twiddle.Filter(false) + expected := CheckServiceNodes{ + nodes[0], + nodes[1], + } + if !reflect.DeepEqual(filtered, expected) { + t.Fatalf("bad: %v", filtered) + } + } + + // Limit to only passing checks. + { + twiddle := make(CheckServiceNodes, len(nodes)) + if n := copy(twiddle, nodes); n != len(nodes) { + t.Fatalf("bad: %d", n) + } + filtered := twiddle.Filter(true) + expected := CheckServiceNodes{ + nodes[1], + } + if !reflect.DeepEqual(filtered, expected) { + t.Fatalf("bad: %v", filtered) + } + } +} + func TestStructs_DirEntry_Clone(t *testing.T) { e := &DirEntry{ LockIndex: 5, From 989619cb6b82e5eca1310850452224c6972ad9fe Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 6 Nov 2015 17:02:05 -0800 Subject: [PATCH 015/123] Moves DNS over to new shuffle and filter functions. --- command/agent/dns.go | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index 8778dedcb5..d2152f8161 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "log" - "math/rand" "net" "strings" "sync" @@ -499,7 +498,7 @@ RPC: } // Filter out any service nodes due to health checks - out.Nodes = d.filterServiceNodes(out.Nodes) + out.Nodes = out.Nodes.Filter(d.config.OnlyPassing) // If we have no nodes, return not found! if len(out.Nodes) == 0 { @@ -509,7 +508,7 @@ RPC: } // Perform a random shuffle - shuffleServiceNodes(out.Nodes) + out.Nodes.Shuffle() // Add various responses depending on the request qType := req.Question[0].Qtype @@ -536,36 +535,6 @@ RPC: } } -// filterServiceNodes is used to filter out nodes that are failing -// health checks to prevent routing to unhealthy nodes -func (d *DNSServer) filterServiceNodes(nodes structs.CheckServiceNodes) structs.CheckServiceNodes { - n := len(nodes) -OUTER: - for i := 0; i < n; i++ { - node := nodes[i] - for _, check := range node.Checks { - if check.Status == structs.HealthCritical || - (d.config.OnlyPassing && check.Status != structs.HealthPassing) { - d.logger.Printf("[WARN] dns: node '%s' failing health check '%s: %s', dropping from service '%s'", - node.Node.Node, check.CheckID, check.Name, node.Service.Service) - nodes[i], nodes[n-1] = nodes[n-1], structs.CheckServiceNode{} - n-- - i-- - continue OUTER - } - } - } - return nodes[:n] -} - -// shuffleServiceNodes does an in-place random shuffle using the Fisher-Yates algorithm -func shuffleServiceNodes(nodes structs.CheckServiceNodes) { - for i := len(nodes) - 1; i > 0; i-- { - j := rand.Int31() % int32(i+1) - nodes[i], nodes[j] = nodes[j], nodes[i] - } -} - // serviceNodeRecords is used to add the node records for a service lookup func (d *DNSServer) serviceNodeRecords(nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration) { qName := req.Question[0].Name From 1f87480e548a47ac399f969565c1eeb9c27d8844 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 6 Nov 2015 18:13:20 -0800 Subject: [PATCH 016/123] Adds a better shuffle test (similar to DNS). --- consul/structs/structs_test.go | 67 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/consul/structs/structs_test.go b/consul/structs/structs_test.go index 4972e191af..6a71116888 100644 --- a/consul/structs/structs_test.go +++ b/consul/structs/structs_test.go @@ -1,7 +1,9 @@ package structs import ( + "fmt" "reflect" + "strings" "testing" ) @@ -210,45 +212,36 @@ func TestStructs_HealthCheck_IsSame(t *testing.T) { } func TestStructs_CheckServiceNodes_Shuffle(t *testing.T) { - nodes := CheckServiceNodes{ - CheckServiceNode{ - Node: &Node{ - Node: "node1", - Address: "127.0.0.1", - }, - }, - CheckServiceNode{ - Node: &Node{ - Node: "node2", - Address: "127.0.0.2", - }, - }, - CheckServiceNode{ - Node: &Node{ - Node: "node3", - Address: "127.0.0.3", - }, - }, - } - - // Make a copy to shuffle and make sure it matches initially. - twiddle := make(CheckServiceNodes, len(nodes)) - if n := copy(twiddle, nodes); n != len(nodes) { - t.Fatalf("bad: %d", n) - } - if !reflect.DeepEqual(twiddle, nodes) { - t.Fatalf("bad: %v", twiddle) - } - - // Give this lots of tries to randomize. If we find a case that's - // not equal we can end the test, otherwise we will call shenanigans. + // Make a huge list of nodes. + var nodes CheckServiceNodes for i := 0; i < 100; i++ { - twiddle.Shuffle() - if !reflect.DeepEqual(twiddle, nodes) { - return - } + nodes = append(nodes, CheckServiceNode{ + Node: &Node{ + Node: fmt.Sprintf("node%d", i), + Address: fmt.Sprintf("127.0.0.%d", i+1), + }, + }) + } + + // Keep track of how many unique shuffles we get. + uniques := make(map[string]struct{}) + for i := 0; i < 100; i++ { + nodes.Shuffle() + + var names []string + for _, node := range nodes { + names = append(names, node.Node.Node) + } + key := strings.Join(names, "|") + uniques[key] = struct{}{} + } + + // We have to allow for the fact that there won't always be a unique + // shuffle each pass, so we just look for smell here without the test + // being flaky. + if len(uniques) < 50 { + t.Fatalf("unique shuffle ratio too low: %d/100", len(uniques)) } - t.Fatalf("shuffle is not working") } func TestStructs_CheckServiceNodes_Filter(t *testing.T) { From 1d1865ddffdcf2311da0449d8d1baac21da215ee Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 6 Nov 2015 22:14:45 -0800 Subject: [PATCH 017/123] Factors code for pulling the sorted list of DCs into a common place. --- consul/catalog_endpoint.go | 18 ++------------ consul/rtt.go | 50 +++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/consul/catalog_endpoint.go b/consul/catalog_endpoint.go index 4cb113d988..ec8389dc1b 100644 --- a/consul/catalog_endpoint.go +++ b/consul/catalog_endpoint.go @@ -2,7 +2,6 @@ package consul import ( "fmt" - "sort" "time" "github.com/armon/go-metrics" @@ -96,24 +95,11 @@ func (c *Catalog) Deregister(args *structs.DeregisterRequest, reply *struct{}) e // ListDatacenters is used to query for the list of known datacenters func (c *Catalog) ListDatacenters(args *struct{}, reply *[]string) error { - c.srv.remoteLock.RLock() - defer c.srv.remoteLock.RUnlock() - - // Read the known DCs - var dcs []string - for dc := range c.srv.remoteConsuls { - dcs = append(dcs, dc) - } - - // TODO - do we want to control the sort behavior with an argument? - - // Sort the DCs by name first, then apply a stable sort by distance. - sort.Strings(dcs) - if err := c.srv.sortDatacentersByDistance(dcs); err != nil { + dcs, err := c.srv.getDatacentersByDistance() + if err != nil { return err } - // Return *reply = dcs return nil } diff --git a/consul/rtt.go b/consul/rtt.go index 58f4aec3a9..a824d89d91 100644 --- a/consul/rtt.go +++ b/consul/rtt.go @@ -276,23 +276,6 @@ func (s *serverSerfer) GetNodesForDatacenter(dc string) []string { return nodes } -// sortDatacentersByDistance will sort the given list of DCs based on the -// median RTT to all nodes we know about from the WAN gossip pool). DCs with -// missing coordinates will be stable sorted to the end of the list. -// -// If coordinates are disabled this will be a no-op. -func (s *Server) sortDatacentersByDistance(dcs []string) error { - // Make it safe to call this without having to check if coordinates are - // disabled first. - if s.config.DisableCoordinates { - return nil - } - - // Do the sort! - serfer := serverSerfer{s} - return sortDatacentersByDistance(&serfer, dcs) -} - // getDatacenterDistance will return the median round trip time estimate for // the given DC from the given serfer, in seconds. This will return positive // infinity if no coordinates are available. @@ -396,3 +379,36 @@ func getDatacenterMaps(s serfer, dcs []string) []structs.DatacenterMap { } return maps } + +// TODO (slackpad) - Add a unit test for this! + +// getDatacentersByDistance will return the list of DCs, sorted in order +// of increasing distance based on the median distance to that DC from all +// servers we know about in the WAN gossip pool. This will sort by name all +// other things being equal (or if coordinates are disabled). +func (s *Server) getDatacentersByDistance() ([]string, error) { + s.remoteLock.RLock() + defer s.remoteLock.RUnlock() + + var dcs []string + for dc := range s.remoteConsuls { + dcs = append(dcs, dc) + } + + // Sort by name first, since the coordinate sort is stable. + sort.Strings(dcs) + + // Make it safe to call this without having to check if coordinates are + // disabled first. + if s.config.DisableCoordinates { + return dcs, nil + } + + // Do the sort! + serfer := serverSerfer{s} + if err := sortDatacentersByDistance(&serfer, dcs); err != nil { + return nil, err + } + + return dcs, nil +} From b736bc4e68dfb58ee12dda6569ef680af34f9a0b Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 6 Nov 2015 22:18:11 -0800 Subject: [PATCH 018/123] Adds basic structure for prepared queries (needs tests). --- consul/acl.go | 4 + consul/fsm.go | 19 ++ consul/query_endpoint.go | 446 ++++++++++++++++++++++++++++++++++ consul/query_endpoint_test.go | 34 +++ consul/server.go | 3 + consul/structs/query.go | 27 +- 6 files changed, 520 insertions(+), 13 deletions(-) create mode 100644 consul/query_endpoint.go create mode 100644 consul/query_endpoint_test.go diff --git a/consul/acl.go b/consul/acl.go index 9d2c1d94b0..479bc3c206 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -399,6 +399,10 @@ func (s *Server) filterACL(token string, subj interface{}) error { filt.filterNodeServices(v.NodeServices) } + case *structs.CheckServiceNodes: + // TODO (slackpad) - Add a test for this! + filt.filterCheckServiceNodes(v) + case *structs.IndexedCheckServiceNodes: filt.filterCheckServiceNodes(&v.Nodes) diff --git a/consul/fsm.go b/consul/fsm.go index 10e4c1b6fb..6b9ddcc57a 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -91,6 +91,8 @@ func (c *consulFSM) Apply(log *raft.Log) interface{} { return c.applyTombstoneOperation(buf[1:], log.Index) case structs.CoordinateBatchUpdateType: return c.applyCoordinateBatchUpdate(buf[1:], log.Index) + case structs.QueryRequestType: + return c.applyQueryOperation(buf[1:], log.Index) default: if ignoreUnknown { c.logger.Printf("[WARN] consul.fsm: ignoring unknown message type (%d), upgrade to newer version", msgType) @@ -264,6 +266,23 @@ func (c *consulFSM) applyCoordinateBatchUpdate(buf []byte, index uint64) interfa return nil } +func (c *consulFSM) applyQueryOperation(buf []byte, index uint64) interface{} { + var req structs.QueryRequest + if err := structs.Decode(buf, &req); err != nil { + panic(fmt.Errorf("failed to decode request: %v", err)) + } + defer metrics.MeasureSince([]string{"consul", "fsm", "query", string(req.Op)}, time.Now()) + switch req.Op { + case structs.QueryCreate, structs.QueryUpdate: + return c.state.QuerySet(index, &req.Query) + case structs.QueryDelete: + return c.state.QueryDelete(index, req.Query.ID) + default: + c.logger.Printf("[WARN] consul.fsm: Invalid Query operation '%s'", req.Op) + return fmt.Errorf("Invalid Query operation '%s'", req.Op) + } +} + func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) { defer func(start time.Time) { c.logger.Printf("[INFO] consul.fsm: snapshot created in %v", time.Now().Sub(start)) diff --git a/consul/query_endpoint.go b/consul/query_endpoint.go new file mode 100644 index 0000000000..54017b7a6b --- /dev/null +++ b/consul/query_endpoint.go @@ -0,0 +1,446 @@ +package consul + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/consul/structs" +) + +var ( + // ErrQueryNotFound is returned if the query lookup failed. + ErrQueryNotFound = errors.New("Query not found") +) + +// Query manages the prepared query endpoint. +type Query struct { + srv *Server +} + +// Apply is used to apply a modifying request to the data store. This should +// only be used for operations that modify the data. The ID of the session is +// returned in the reply. +func (q *Query) Apply(args *structs.QueryRequest, reply *string) (err error) { + if done, err := q.srv.forward("Query.Apply", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"consul", "query", "apply"}, time.Now()) + + // Validate the ID. We must create new IDs before applying to the Raft + // log since it's not deterministic. + if args.Op == structs.QueryCreate { + if args.Query.ID != "" { + return fmt.Errorf("ID must be empty when creating a new query") + } + + // We are relying on the fact that UUIDs are random and unlikely + // to collide since this isn't inside a write transaction. + state := q.srv.fsm.State() + for { + args.Query.ID = generateUUID() + _, query, err := state.QueryGet(args.Query.ID) + if err != nil { + return fmt.Errorf("Query lookup failed: %v", err) + } + if query == nil { + break + } + } + } + *reply = args.Query.ID + + // Grab the ACL because we need it in several places below. + acl, err := q.srv.resolveToken(args.Token) + if err != nil { + return err + } + + // Enforce that any modify operation has the same token used when the + // query was created, or a management token with sufficient rights. + if args.Op != structs.QueryCreate { + state := q.srv.fsm.State() + _, query, err := state.QueryGet(args.Query.ID) + if err != nil { + return fmt.Errorf("Query lookup failed: %v", err) + } + if query == nil { + return fmt.Errorf("Cannot modify non-existent query: '%s'", args.Query.ID) + } + if (query.Token != args.Token) && (acl != nil && !acl.QueryModify()) { + q.srv.logger.Printf("[WARN] consul.query: Operation on query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.Query.ID) + return permissionDeniedErr + } + } + + // Parse the query and prep it for the state store. + switch args.Op { + case structs.QueryCreate, structs.QueryUpdate: + if err := parseQuery(&args.Query); err != nil { + return fmt.Errorf("Invalid query: %v", err) + } + + if acl != nil && !acl.ServiceRead(args.Query.Service.Service) { + q.srv.logger.Printf("[WARN] consul.query: Operation on query for service '%s' denied due to ACLs", args.Query.Service.Service) + return permissionDeniedErr + } + + case structs.QueryDelete: + // Nothing else to verify here, just do the delete (we only look + // at the ID field for this op). + + default: + return fmt.Errorf("Unknown query operation: %s", args.Op) + } + + // At this point the token has been vetted, so make sure the token that + // is stored in the state store matches what was supplied. + args.Query.Token = args.Token + + resp, err := q.srv.raftApply(structs.QueryRequestType, args) + if err != nil { + q.srv.logger.Printf("[ERR] consul.query: Apply failed %v", err) + return err + } + if respErr, ok := resp.(error); ok { + return respErr + } + + return nil +} + +// parseQuery makes sure the entries of a query are valid for a create or +// update operation. Some of the fields are not checked or are partially +// checked, as noted in the comments below. This also updates all the parsed +// fields of the query. +func parseQuery(query *structs.PreparedQuery) error { + // We skip a few fields: + // - ID is checked outside this fn. + // - Name is optional with no restrictions, except for uniqueness which + // is checked for integrity during the transaction. We also make sure + // names do not overlap with IDs, which is also checked during the + // transaction. Otherwise, people could "steal" queries that they don't + // have proper ACL rights to change. + // - Session is optional and checked for integrity during the transaction. + // - Token is checked outside this fn. + + // Parse the service query sub-structure. + if err := parseService(&query.Service); err != nil { + return err + } + + // Parse the DNS options sub-structure. + if err := parseDNS(&query.DNS); err != nil { + return err + } + + return nil +} + +// parseService makes sure the entries of a query are valid for a create or +// update operation. Some of the fields are not checked or are partially +// checked, as noted in the comments below. This also updates all the parsed +// fields of the query. +func parseService(svc *structs.ServiceQuery) error { + // Service is required. We check integrity during the transaction. + if svc.Service == "" { + return fmt.Errorf("Must provide a service name to query") + } + + // NearestN can be 0 which means "don't fail over by RTT". + if svc.Failover.NearestN < 0 { + return fmt.Errorf("Bad NearestN '%d', must be >= 0", svc.Failover.NearestN) + } + + // We skip a few fields: + // - There's no validation for Datacenters; we skip any unknown entries + // at execution time. + // - OnlyPassing is just a boolean so doesn't need further validation. + // - Tags is a free-form list of tags and doesn't need further validation. + + // Sort order must be one of the allowed values, or if not given we + // default to "shuffle" so there's load balancing. + switch svc.Sort { + case structs.QueryOrderShuffle: + case structs.QueryOrderSort: + case "": + svc.Sort = structs.QueryOrderShuffle + default: + return fmt.Errorf("Bad Sort '%s'", svc.Sort) + } + + return nil +} + +// parseDNS makes sure the entries of a query are valid for a create or +// update operation. This also updates all the parsed fields of the query. +func parseDNS(dns *structs.QueryDNSOptions) error { + if dns.TTL != "" { + ttl, err := time.ParseDuration(dns.TTL) + if err != nil { + return fmt.Errorf("Bad DNS TTL '%s': %v", dns.TTL, err) + } + + if ttl < 0 { + return fmt.Errorf("DNS TTL '%d', must be >=0", ttl) + } + } + + 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. +func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryExecuteResponse) error { + if done, err := q.srv.forward("Query.Execute", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"consul", "query", "execute"}, time.Now()) + + // We have to do this ourselves since we are not doing a blocking RPC. + if args.RequireConsistent { + if err := q.srv.consistentRead(); err != nil { + return err + } + } + + // Try to locate the query. + state := q.srv.fsm.State() + _, query, err := state.QueryLookup(args.QueryIDOrName) + if err != nil { + return err + } + if query == nil { + return ErrQueryNotFound + } + + // Execute the query for the local DC. + if err := q.execute(query, reply); err != nil { + return err + } + + // Shuffle the results in case coordinates are not available if they + // requested an RTT sort. + reply.Nodes.Shuffle() + if query.Service.Sort == structs.QueryOrderSort { + if err := q.srv.sortNodesByDistanceFrom(args.Source, reply.Nodes); err != nil { + return err + } + } + + // In the happy path where we found some healthy nodes we go with that + // and bail out. Otherwise, we fail over and try remote DCs, as allowed + // by the query setup. + if len(reply.Nodes) == 0 { + wrapper := &queryServerWrapper{q.srv} + if err := queryFailover(wrapper, query, args, reply); err != nil { + return err + } + } + + return nil +} + +// ExecuteRemote is used when a local node doesn't have any instances of a +// service available and needs to probe remote DCs. This sends the full query +// over since the remote side won't have it in its state store, and this doesn't +// do the failover logic since that's already being run on the originating DC. +// We don't want things to fan out further than one level. +func (q *Query) ExecuteRemote(args *structs.QueryExecuteRemoteRequest, + reply *structs.QueryExecuteResponse) error { + if done, err := q.srv.forward("Query.ExecuteRemote", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"consul", "query", "execute_remote"}, time.Now()) + + // We have to do this ourselves since we are not doing a blocking RPC. + if args.RequireConsistent { + if err := q.srv.consistentRead(); err != nil { + return err + } + } + + // Run the query locally to see what we can find. + if err := q.execute(&args.Query, reply); err != nil { + return err + } + + // We don't bother trying to do an RTT sort here since we are by + // definition in another DC. We just shuffle to make sure that we + // balance the load across the results. + reply.Nodes.Shuffle() + + return nil +} + +// execute runs a prepared query in the local DC without any failover. We don't +// apply any sorting options at this level - it should be done up above. +func (q *Query) execute(query *structs.PreparedQuery, reply *structs.QueryExecuteResponse) error { + state := q.srv.fsm.State() + _, nodes, err := state.CheckServiceNodes(query.Service.Service) + if err != nil { + return err + } + + // This is kind of a paranoia ACL check, in case something changed with + // the token from the time the query was registered. Note that we use + // the token stored with the query, NOT the passed-in one, which is + // critical to how queries work (the query becomes a proxy for a lookup + // using the ACL it was created with). + if err := q.srv.filterACL(query.Token, nodes); err != nil { + return err + } + + // Filter out any unhealthy nodes. + nodes = nodes.Filter(query.Service.OnlyPassing) + + // Apply the tag filters, if any. + if len(query.Service.Tags) > 0 { + nodes = tagFilter(query.Service.Tags, nodes) + } + + // Capture the nodes and pass the DNS information through to the reply. + reply.Nodes = nodes + reply.DNS = query.DNS + + return nil +} + +// tagFilter returns a list of nodes who satisfy the given tags. Nodes must have +// ALL the given tags, and none of the forbidden tags (prefixed with ~). +func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes { + // Build up lists of required and disallowed tags. + must, not := make([]string, 0), make([]string, 0) + for _, tag := range tags { + tag = strings.ToLower(tag) + if strings.HasPrefix(tag, "~") { + tag = tag[1:] + not = append(not, tag) + } else { + must = append(must, tag) + } + } + + n := len(nodes) + for i := 0; i < n; i++ { + node := nodes[i] + + // Index the tags so lookups this way are cheaper. + index := make(map[string]struct{}) + for _, tag := range node.Service.Tags { + tag = strings.ToLower(tag) + index[tag] = struct{}{} + } + + // Bail if any of the required tags are missing. + for _, tag := range must { + if _, ok := index[tag]; !ok { + goto DELETE + } + } + + // Bail if any of the disallowed tags are present. + for _, tag := range not { + if _, ok := index[tag]; ok { + goto DELETE + } + } + + // At this point, the service is ok to leave in the list. + continue + + DELETE: + nodes[i], nodes[n-1] = nodes[n-1], structs.CheckServiceNode{} + n-- + i-- + } + return nodes[:n] +} + +// queryServer is a wrapper that makes it easier to test the failover logic. +type queryServer interface { + GetOtherDatacentersByDistance() ([]string, error) + ForwardDC(method, dc string, args interface{}, reply interface{}) error +} + +// queryServerWrapper applies the queryServer interface to a Server. +type queryServerWrapper struct { + srv *Server +} + +// ForwardDC calls into the server's RPC forwarder. +func (q *queryServerWrapper) ForwardDC(method, dc string, args interface{}, reply interface{}) error { + return q.srv.forwardDC(method, dc, args, reply) +} + +// GetOtherDatacentersByDistance calls into the server's fn and filters out the +// server's own DC. +func (q *queryServerWrapper) GetOtherDatacentersByDistance() ([]string, error) { + dcs, err := q.srv.getDatacentersByDistance() + if err != nil { + return nil, err + } + + var result []string + for _, dc := range dcs { + if dc != q.srv.config.Datacenter { + result = append(result, dc) + } + } + return result, nil +} + +// queryFailover runs an algorithm to determine which DCs to try and then calls +// them to try to locate alternative services. +func queryFailover(q queryServer, query *structs.PreparedQuery, + args *structs.QueryExecuteRequest, reply *structs.QueryExecuteResponse) error { + + // Build a candidate list of DCs, starting with the nearest N from RTTs. + var dcs []string + index := make(map[string]struct{}) + if query.Service.Failover.NearestN > 0 { + nearest, err := q.GetOtherDatacentersByDistance() + if err != nil { + return err + } + + for i, dc := range nearest { + if !(i < query.Service.Failover.NearestN) { + break + } + + dcs = append(dcs, dc) + index[dc] = struct{}{} + } + } + + // Then add any DCs explicitly listed that weren't selected above. + for _, dc := range query.Service.Failover.Datacenters { + _, ok := index[dc] + if !ok { + dcs = append(dcs, dc) + } + } + + // Now try the selected DCs in priority order. + for _, dc := range dcs { + remote := &structs.QueryExecuteRemoteRequest{ + Datacenter: dc, + Query: *query, + QueryOptions: args.QueryOptions, + } + if err := q.ForwardDC("Query.ExecuteRemote", dc, remote, reply); err != nil { + return err + } + + // We can stop if we found some nodes. + if len(reply.Nodes) > 0 { + break + } + } + + return nil +} diff --git a/consul/query_endpoint_test.go b/consul/query_endpoint_test.go new file mode 100644 index 0000000000..a00e0b938a --- /dev/null +++ b/consul/query_endpoint_test.go @@ -0,0 +1,34 @@ +package consul + +import ( + "os" + "testing" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" + "github.com/hashicorp/net-rpc-msgpackrpc" +) + +func TestQuery_Apply(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testutil.WaitForLeader(t, s1.RPC, "dc1") + + arg := structs.QueryRequest{ + Datacenter: "dc1", + Op: structs.QueryCreate, + Query: structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + } + var reply string + if err := msgpackrpc.CallWithCodec(codec, "Query.Apply", &arg, &reply); err != nil { + t.Fatalf("err: %v", err) + } +} diff --git a/consul/server.go b/consul/server.go index d90e163a74..ddb410bdd8 100644 --- a/consul/server.go +++ b/consul/server.go @@ -161,6 +161,7 @@ type endpoints struct { Internal *Internal ACL *ACL Coordinate *Coordinate + Query *Query } // NewServer is used to construct a new Consul server from the @@ -411,6 +412,7 @@ func (s *Server) setupRPC(tlsWrap tlsutil.DCWrapper) error { s.endpoints.Internal = &Internal{s} s.endpoints.ACL = &ACL{s} s.endpoints.Coordinate = NewCoordinate(s) + s.endpoints.Query = &Query{s} // Register the handlers s.rpcServer.Register(s.endpoints.Status) @@ -421,6 +423,7 @@ func (s *Server) setupRPC(tlsWrap tlsutil.DCWrapper) error { s.rpcServer.Register(s.endpoints.Internal) s.rpcServer.Register(s.endpoints.ACL) s.rpcServer.Register(s.endpoints.Coordinate) + s.rpcServer.Register(s.endpoints.Query) list, err := net.ListenTCP("tcp", s.config.RPCAddr) if err != nil { diff --git a/consul/structs/query.go b/consul/structs/query.go index 0b534aca90..500ba1753a 100644 --- a/consul/structs/query.go +++ b/consul/structs/query.go @@ -70,12 +70,8 @@ type PreparedQuery struct { // can be used to locate nodes with supplying any ACL. Name string - // TTL is the time to live for the query itself. If this is omitted then - // the query will not expire (unless tied to a session). - TTL string - // Session is an optional session to tie this query's lifetime to. If - // this is omitted then the query will not expire (unless given a TTL). + // 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 @@ -117,8 +113,13 @@ func (q *QueryRequest) RequestDatacenter() string { return q.Datacenter } -// QuerySpecificRequest is used to execute a prepared query. -type QuerySpecificRequest struct { +// QueryResponse is used to return the ID of an updated query. +type QueryResponse struct { + ID string +} + +// QueryExecuteRequest is used to execute a prepared query. +type QueryExecuteRequest struct { Datacenter string QueryIDOrName string Source QuerySource @@ -126,26 +127,26 @@ type QuerySpecificRequest struct { } // RequestDatacenter returns the datacenter for a given request. -func (q *QuerySpecificRequest) RequestDatacenter() string { +func (q *QueryExecuteRequest) RequestDatacenter() string { return q.Datacenter } -// QueryRemoteRequest is used when running a local query in a remote +// QueryExecuteRemoteRequest is used when running a local query in a remote // datacenter. We have to ship the entire query over since it won't be // present in the remote state store. -type QueryRemoteRequest struct { +type QueryExecuteRemoteRequest struct { Datacenter string Query PreparedQuery QueryOptions } // RequestDatacenter returns the datacenter for a given request. -func (q *QueryRemoteRequest) RequestDatacenter() string { +func (q *QueryExecuteRemoteRequest) RequestDatacenter() string { return q.Datacenter } -// QueryExecutionResponse has the results of executing a query. -type QueryExecutionResponse struct { +// QueryExecuteResponse has the results of executing a query. +type QueryExecuteResponse struct { Nodes CheckServiceNodes DNS QueryDNSOptions } From 81bb39751a8b2202a7ed050838fb0bad673512ab Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 20:37:41 -0800 Subject: [PATCH 019/123] Adds prefix "prepared" to everything prepared query-related. --- consul/fsm.go | 24 ++-- ...endpoint.go => prepared_query_endpoint.go} | 97 ++++++++-------- ...est.go => prepared_query_endpoint_test.go} | 8 +- consul/server.go | 22 ++-- consul/state/{query.go => prepared_query.go} | 108 +++++++++--------- .../{query_test.go => prepared_query_test.go} | 106 ++++++++--------- consul/state/schema.go | 8 +- consul/state/state_store.go | 12 +- consul/state/state_store_test.go | 8 +- .../structs/{query.go => prepared_query.go} | 37 +++--- consul/structs/structs.go | 2 +- 11 files changed, 216 insertions(+), 216 deletions(-) rename consul/{query_endpoint.go => prepared_query_endpoint.go} (76%) rename consul/{query_endpoint_test.go => prepared_query_endpoint_test.go} (70%) rename consul/state/{query.go => prepared_query.go} (51%) rename consul/state/{query_test.go => prepared_query_test.go} (78%) rename consul/structs/{query.go => prepared_query.go} (79%) diff --git a/consul/fsm.go b/consul/fsm.go index 6b9ddcc57a..7116a6255f 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -91,8 +91,8 @@ func (c *consulFSM) Apply(log *raft.Log) interface{} { return c.applyTombstoneOperation(buf[1:], log.Index) case structs.CoordinateBatchUpdateType: return c.applyCoordinateBatchUpdate(buf[1:], log.Index) - case structs.QueryRequestType: - return c.applyQueryOperation(buf[1:], log.Index) + case structs.PreparedQueryRequestType: + return c.applyPreparedQueryOperation(buf[1:], log.Index) default: if ignoreUnknown { c.logger.Printf("[WARN] consul.fsm: ignoring unknown message type (%d), upgrade to newer version", msgType) @@ -266,20 +266,22 @@ func (c *consulFSM) applyCoordinateBatchUpdate(buf []byte, index uint64) interfa return nil } -func (c *consulFSM) applyQueryOperation(buf []byte, index uint64) interface{} { - var req structs.QueryRequest +// applyPreparedQueryOperation applies the given prepared query operation to the +// state store. +func (c *consulFSM) applyPreparedQueryOperation(buf []byte, index uint64) interface{} { + var req structs.PreparedQueryRequest if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } - defer metrics.MeasureSince([]string{"consul", "fsm", "query", string(req.Op)}, time.Now()) + defer metrics.MeasureSince([]string{"consul", "fsm", "prepared-query", string(req.Op)}, time.Now()) switch req.Op { - case structs.QueryCreate, structs.QueryUpdate: - return c.state.QuerySet(index, &req.Query) - case structs.QueryDelete: - return c.state.QueryDelete(index, req.Query.ID) + case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: + return c.state.PreparedQuerySet(index, &req.Query) + case structs.PreparedQueryDelete: + return c.state.PreparedQueryDelete(index, req.Query.ID) default: - c.logger.Printf("[WARN] consul.fsm: Invalid Query operation '%s'", req.Op) - return fmt.Errorf("Invalid Query operation '%s'", req.Op) + c.logger.Printf("[WARN] consul.fsm: Invalid PreparedQuery operation '%s'", req.Op) + return fmt.Errorf("Invalid PreparedQuery operation '%s'", req.Op) } } diff --git a/consul/query_endpoint.go b/consul/prepared_query_endpoint.go similarity index 76% rename from consul/query_endpoint.go rename to consul/prepared_query_endpoint.go index 54017b7a6b..830989322b 100644 --- a/consul/query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -15,35 +15,35 @@ var ( ErrQueryNotFound = errors.New("Query not found") ) -// Query manages the prepared query endpoint. -type Query struct { +// PreparedQuery manages the prepared query endpoint. +type PreparedQuery struct { srv *Server } // Apply is used to apply a modifying request to the data store. This should // only be used for operations that modify the data. The ID of the session is // returned in the reply. -func (q *Query) Apply(args *structs.QueryRequest, reply *string) (err error) { - if done, err := q.srv.forward("Query.Apply", args, args, reply); done { +func (p *PreparedQuery) Apply(args *structs.PreparedQueryRequest, reply *string) (err error) { + if done, err := p.srv.forward("PreparedQuery.Apply", args, args, reply); done { return err } - defer metrics.MeasureSince([]string{"consul", "query", "apply"}, time.Now()) + defer metrics.MeasureSince([]string{"consul", "prepared-query", "apply"}, time.Now()) // Validate the ID. We must create new IDs before applying to the Raft // log since it's not deterministic. - if args.Op == structs.QueryCreate { + if args.Op == structs.PreparedQueryCreate { if args.Query.ID != "" { - return fmt.Errorf("ID must be empty when creating a new query") + return fmt.Errorf("ID must be empty when creating a new prepared query") } // We are relying on the fact that UUIDs are random and unlikely // to collide since this isn't inside a write transaction. - state := q.srv.fsm.State() + state := p.srv.fsm.State() for { args.Query.ID = generateUUID() - _, query, err := state.QueryGet(args.Query.ID) + _, query, err := state.PreparedQueryGet(args.Query.ID) if err != nil { - return fmt.Errorf("Query lookup failed: %v", err) + return fmt.Errorf("Prepared query lookup failed: %v", err) } if query == nil { break @@ -53,55 +53,55 @@ func (q *Query) Apply(args *structs.QueryRequest, reply *string) (err error) { *reply = args.Query.ID // Grab the ACL because we need it in several places below. - acl, err := q.srv.resolveToken(args.Token) + acl, err := p.srv.resolveToken(args.Token) if err != nil { return err } // Enforce that any modify operation has the same token used when the // query was created, or a management token with sufficient rights. - if args.Op != structs.QueryCreate { - state := q.srv.fsm.State() - _, query, err := state.QueryGet(args.Query.ID) + if args.Op != structs.PreparedQueryCreate { + state := p.srv.fsm.State() + _, query, err := state.PreparedQueryGet(args.Query.ID) if err != nil { - return fmt.Errorf("Query lookup failed: %v", err) + return fmt.Errorf("Prepared Query lookup failed: %v", err) } if query == nil { - return fmt.Errorf("Cannot modify non-existent query: '%s'", args.Query.ID) + return fmt.Errorf("Cannot modify non-existent prepared query: '%s'", args.Query.ID) } if (query.Token != args.Token) && (acl != nil && !acl.QueryModify()) { - q.srv.logger.Printf("[WARN] consul.query: Operation on query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.Query.ID) + p.srv.logger.Printf("[WARN] consul.prepared_query: Operation on prepared query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.Query.ID) return permissionDeniedErr } } // Parse the query and prep it for the state store. switch args.Op { - case structs.QueryCreate, structs.QueryUpdate: + case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: if err := parseQuery(&args.Query); err != nil { - return fmt.Errorf("Invalid query: %v", err) + return fmt.Errorf("Invalid prepared query: %v", err) } if acl != nil && !acl.ServiceRead(args.Query.Service.Service) { - q.srv.logger.Printf("[WARN] consul.query: Operation on query for service '%s' denied due to ACLs", args.Query.Service.Service) + p.srv.logger.Printf("[WARN] consul.prepared_query: Operation on prepared query for service '%s' denied due to ACLs", args.Query.Service.Service) return permissionDeniedErr } - case structs.QueryDelete: + case structs.PreparedQueryDelete: // Nothing else to verify here, just do the delete (we only look // at the ID field for this op). default: - return fmt.Errorf("Unknown query operation: %s", args.Op) + return fmt.Errorf("Unknown prepared query operation: %s", args.Op) } // At this point the token has been vetted, so make sure the token that // is stored in the state store matches what was supplied. args.Query.Token = args.Token - resp, err := q.srv.raftApply(structs.QueryRequestType, args) + resp, err := p.srv.raftApply(structs.PreparedQueryRequestType, args) if err != nil { - q.srv.logger.Printf("[ERR] consul.query: Apply failed %v", err) + p.srv.logger.Printf("[ERR] consul.prepared_query: Apply failed %v", err) return err } if respErr, ok := resp.(error); ok { @@ -194,22 +194,23 @@ func parseDNS(dns *structs.QueryDNSOptions) error { // 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. -func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryExecuteResponse) error { - if done, err := q.srv.forward("Query.Execute", args, args, reply); done { +func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, + reply *structs.PreparedQueryExecuteResponse) error { + if done, err := p.srv.forward("PreparedQuery.Execute", args, args, reply); done { return err } - defer metrics.MeasureSince([]string{"consul", "query", "execute"}, time.Now()) + defer metrics.MeasureSince([]string{"consul", "prepared-query", "execute"}, time.Now()) // We have to do this ourselves since we are not doing a blocking RPC. if args.RequireConsistent { - if err := q.srv.consistentRead(); err != nil { + if err := p.srv.consistentRead(); err != nil { return err } } // Try to locate the query. - state := q.srv.fsm.State() - _, query, err := state.QueryLookup(args.QueryIDOrName) + state := p.srv.fsm.State() + _, query, err := state.PreparedQueryLookup(args.QueryIDOrName) if err != nil { return err } @@ -218,7 +219,7 @@ func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryE } // Execute the query for the local DC. - if err := q.execute(query, reply); err != nil { + if err := p.execute(query, reply); err != nil { return err } @@ -226,7 +227,7 @@ func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryE // requested an RTT sort. reply.Nodes.Shuffle() if query.Service.Sort == structs.QueryOrderSort { - if err := q.srv.sortNodesByDistanceFrom(args.Source, reply.Nodes); err != nil { + if err := p.srv.sortNodesByDistanceFrom(args.Source, reply.Nodes); err != nil { return err } } @@ -235,8 +236,8 @@ func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryE // and bail out. Otherwise, we fail over and try remote DCs, as allowed // by the query setup. if len(reply.Nodes) == 0 { - wrapper := &queryServerWrapper{q.srv} - if err := queryFailover(wrapper, query, args, reply); err != nil { + wrapper := &queryServerWrapper{p.srv} + if err := queryFailover(wrapper, query, args.QueryOptions, reply); err != nil { return err } } @@ -249,22 +250,22 @@ func (q *Query) Execute(args *structs.QueryExecuteRequest, reply *structs.QueryE // over since the remote side won't have it in its state store, and this doesn't // do the failover logic since that's already being run on the originating DC. // We don't want things to fan out further than one level. -func (q *Query) ExecuteRemote(args *structs.QueryExecuteRemoteRequest, - reply *structs.QueryExecuteResponse) error { - if done, err := q.srv.forward("Query.ExecuteRemote", args, args, reply); done { +func (p *PreparedQuery) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRequest, + reply *structs.PreparedQueryExecuteResponse) error { + if done, err := p.srv.forward("PreparedQuery.ExecuteRemote", args, args, reply); done { return err } - defer metrics.MeasureSince([]string{"consul", "query", "execute_remote"}, time.Now()) + defer metrics.MeasureSince([]string{"consul", "prepared-query", "execute_remote"}, time.Now()) // We have to do this ourselves since we are not doing a blocking RPC. if args.RequireConsistent { - if err := q.srv.consistentRead(); err != nil { + if err := p.srv.consistentRead(); err != nil { return err } } // Run the query locally to see what we can find. - if err := q.execute(&args.Query, reply); err != nil { + if err := p.execute(&args.Query, reply); err != nil { return err } @@ -278,8 +279,9 @@ func (q *Query) ExecuteRemote(args *structs.QueryExecuteRemoteRequest, // execute runs a prepared query in the local DC without any failover. We don't // apply any sorting options at this level - it should be done up above. -func (q *Query) execute(query *structs.PreparedQuery, reply *structs.QueryExecuteResponse) error { - state := q.srv.fsm.State() +func (p *PreparedQuery) execute(query *structs.PreparedQuery, + reply *structs.PreparedQueryExecuteResponse) error { + state := p.srv.fsm.State() _, nodes, err := state.CheckServiceNodes(query.Service.Service) if err != nil { return err @@ -290,7 +292,7 @@ func (q *Query) execute(query *structs.PreparedQuery, reply *structs.QueryExecut // the token stored with the query, NOT the passed-in one, which is // critical to how queries work (the query becomes a proxy for a lookup // using the ACL it was created with). - if err := q.srv.filterACL(query.Token, nodes); err != nil { + if err := p.srv.filterACL(query.Token, nodes); err != nil { return err } @@ -396,7 +398,8 @@ func (q *queryServerWrapper) GetOtherDatacentersByDistance() ([]string, error) { // queryFailover runs an algorithm to determine which DCs to try and then calls // them to try to locate alternative services. func queryFailover(q queryServer, query *structs.PreparedQuery, - args *structs.QueryExecuteRequest, reply *structs.QueryExecuteResponse) error { + options structs.QueryOptions, + reply *structs.PreparedQueryExecuteResponse) error { // Build a candidate list of DCs, starting with the nearest N from RTTs. var dcs []string @@ -427,12 +430,12 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, // Now try the selected DCs in priority order. for _, dc := range dcs { - remote := &structs.QueryExecuteRemoteRequest{ + remote := &structs.PreparedQueryExecuteRemoteRequest{ Datacenter: dc, Query: *query, - QueryOptions: args.QueryOptions, + QueryOptions: options, } - if err := q.ForwardDC("Query.ExecuteRemote", dc, remote, reply); err != nil { + if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, remote, reply); err != nil { return err } diff --git a/consul/query_endpoint_test.go b/consul/prepared_query_endpoint_test.go similarity index 70% rename from consul/query_endpoint_test.go rename to consul/prepared_query_endpoint_test.go index a00e0b938a..c9a4a736a1 100644 --- a/consul/query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/net-rpc-msgpackrpc" ) -func TestQuery_Apply(t *testing.T) { +func TestPreparedQuery_Apply(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -18,9 +18,9 @@ func TestQuery_Apply(t *testing.T) { testutil.WaitForLeader(t, s1.RPC, "dc1") - arg := structs.QueryRequest{ + arg := structs.PreparedQueryRequest{ Datacenter: "dc1", - Op: structs.QueryCreate, + Op: structs.PreparedQueryCreate, Query: structs.PreparedQuery{ Service: structs.ServiceQuery{ Service: "redis", @@ -28,7 +28,7 @@ func TestQuery_Apply(t *testing.T) { }, } var reply string - if err := msgpackrpc.CallWithCodec(codec, "Query.Apply", &arg, &reply); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { t.Fatalf("err: %v", err) } } diff --git a/consul/server.go b/consul/server.go index ddb410bdd8..83602c4272 100644 --- a/consul/server.go +++ b/consul/server.go @@ -153,15 +153,15 @@ type Server struct { // Holds the RPC endpoints type endpoints struct { - Catalog *Catalog - Health *Health - Status *Status - KVS *KVS - Session *Session - Internal *Internal - ACL *ACL - Coordinate *Coordinate - Query *Query + Catalog *Catalog + Health *Health + Status *Status + KVS *KVS + Session *Session + Internal *Internal + ACL *ACL + Coordinate *Coordinate + PreparedQuery *PreparedQuery } // NewServer is used to construct a new Consul server from the @@ -412,7 +412,7 @@ func (s *Server) setupRPC(tlsWrap tlsutil.DCWrapper) error { s.endpoints.Internal = &Internal{s} s.endpoints.ACL = &ACL{s} s.endpoints.Coordinate = NewCoordinate(s) - s.endpoints.Query = &Query{s} + s.endpoints.PreparedQuery = &PreparedQuery{s} // Register the handlers s.rpcServer.Register(s.endpoints.Status) @@ -423,7 +423,7 @@ func (s *Server) setupRPC(tlsWrap tlsutil.DCWrapper) error { s.rpcServer.Register(s.endpoints.Internal) s.rpcServer.Register(s.endpoints.ACL) s.rpcServer.Register(s.endpoints.Coordinate) - s.rpcServer.Register(s.endpoints.Query) + s.rpcServer.Register(s.endpoints.PreparedQuery) list, err := net.ListenTCP("tcp", s.config.RPCAddr) if err != nil { diff --git a/consul/state/query.go b/consul/state/prepared_query.go similarity index 51% rename from consul/state/query.go rename to consul/state/prepared_query.go index c7a9082dd4..e8f2e48ab3 100644 --- a/consul/state/query.go +++ b/consul/state/prepared_query.go @@ -8,37 +8,36 @@ import ( "github.com/hashicorp/go-memdb" ) -// Queries is used to pull all the prepared queries from the snapshot. -func (s *StateSnapshot) Queries() (memdb.ResultIterator, error) { - iter, err := s.tx.Get("queries", "id") +// PreparedQueries is used to pull all the prepared queries from the snapshot. +func (s *StateSnapshot) PreparedQueries() (memdb.ResultIterator, error) { + iter, err := s.tx.Get("prepared-queries", "id") if err != nil { return nil, err } return iter, nil } -// Query is used when restoring from a snapshot. For general inserts, use -// QuerySet. -func (s *StateRestore) Query(query *structs.PreparedQuery) error { - if err := s.tx.Insert("queries", query); err != nil { - return fmt.Errorf("failed restoring query: %s", err) +// PrepparedQuery is used when restoring from a snapshot. For general inserts, +// use PreparedQuerySet. +func (s *StateRestore) PreparedQuery(query *structs.PreparedQuery) error { + if err := s.tx.Insert("prepared-queries", query); err != nil { + return fmt.Errorf("failed restoring prepared query: %s", err) } - if err := indexUpdateMaxTxn(s.tx, query.ModifyIndex, "queries"); err != nil { + if err := indexUpdateMaxTxn(s.tx, query.ModifyIndex, "prepared-queries"); err != nil { return fmt.Errorf("failed updating index: %s", err) } - s.watches.Arm("queries") + s.watches.Arm("prepared-queries") return nil } -// QuerySet is used to create or update a prepared query. -func (s *StateStore) QuerySet(idx uint64, query *structs.PreparedQuery) error { +// PreparedQuerySet is used to create or update a prepared query. +func (s *StateStore) PreparedQuerySet(idx uint64, query *structs.PreparedQuery) error { tx := s.db.Txn(true) defer tx.Abort() - // Call set on the Query. - if err := s.querySetTxn(tx, idx, query); err != nil { + if err := s.preparedQuerySetTxn(tx, idx, query); err != nil { return err } @@ -46,18 +45,18 @@ func (s *StateStore) QuerySet(idx uint64, query *structs.PreparedQuery) error { return nil } -// querySetTxn is the inner method used to insert a prepared query with the -// proper indexes into the state store. -func (s *StateStore) querySetTxn(tx *memdb.Txn, idx uint64, query *structs.PreparedQuery) error { +// preparedQuerySetTxn is the inner method used to insert a prepared query with +// the proper indexes into the state store. +func (s *StateStore) preparedQuerySetTxn(tx *memdb.Txn, idx uint64, query *structs.PreparedQuery) error { // Check that the ID is set. if query.ID == "" { return ErrMissingQueryID } // Check for an existing query. - existing, err := tx.First("queries", "id", query.ID) + existing, err := tx.First("prepared-queries", "id", query.ID) if err != nil { - return fmt.Errorf("failed query lookup: %s", err) + return fmt.Errorf("failed prepared query lookup: %s", err) } // Set the indexes. @@ -73,7 +72,7 @@ func (s *StateStore) querySetTxn(tx *memdb.Txn, idx uint64, query *structs.Prepa // this then a bad actor could steal traffic away from an existing DNS // entry. if query.Name != "" { - existing, err := tx.First("queries", "id", query.Name) + existing, err := tx.First("prepared-queries", "id", query.Name) // This is a little unfortunate but the UUID index will complain // if the name isn't formatted like a UUID, so we can safely @@ -107,25 +106,25 @@ func (s *StateStore) querySetTxn(tx *memdb.Txn, idx uint64, query *structs.Prepa } // Insert the query. - if err := tx.Insert("queries", query); err != nil { - return fmt.Errorf("failed inserting query: %s", err) + if err := tx.Insert("prepared-queries", query); err != nil { + return fmt.Errorf("failed inserting prepared query: %s", err) } - if err := tx.Insert("index", &IndexEntry{"queries", idx}); err != nil { + if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil { return fmt.Errorf("failed updating index: %s", err) } - tx.Defer(func() { s.tableWatches["queries"].Notify() }) + tx.Defer(func() { s.tableWatches["prepared-queries"].Notify() }) return nil } -// QueryDelete deletes the given query by ID. -func (s *StateStore) QueryDelete(idx uint64, queryID string) error { +// PreparedQueryDelete deletes the given query by ID. +func (s *StateStore) PreparedQueryDelete(idx uint64, queryID string) error { tx := s.db.Txn(true) defer tx.Abort() watches := NewDumbWatchManager(s.tableWatches) - if err := s.queryDeleteTxn(tx, idx, watches, queryID); err != nil { - return fmt.Errorf("failed query delete: %s", err) + if err := s.preparedQueryDeleteTxn(tx, idx, watches, queryID); err != nil { + return fmt.Errorf("failed prepared query delete: %s", err) } tx.Defer(func() { watches.Notify() }) @@ -133,43 +132,43 @@ func (s *StateStore) QueryDelete(idx uint64, queryID string) error { return nil } -// queryDeleteTxn is the inner method used to delete a prepared query with the -// proper indexes into the state store. -func (s *StateStore) queryDeleteTxn(tx *memdb.Txn, idx uint64, watches *DumbWatchManager, +// preparedQueryDeleteTxn is the inner method used to delete a prepared query +// with the proper indexes into the state store. +func (s *StateStore) preparedQueryDeleteTxn(tx *memdb.Txn, idx uint64, watches *DumbWatchManager, queryID string) error { // Pull the query. - query, err := tx.First("queries", "id", queryID) + query, err := tx.First("prepared-queries", "id", queryID) if err != nil { - return fmt.Errorf("failed query lookup: %s", err) + return fmt.Errorf("failed prepared query lookup: %s", err) } if query == nil { return nil } // Delete the query and update the index. - if err := tx.Delete("queries", query); err != nil { - return fmt.Errorf("failed query delete: %s", err) + if err := tx.Delete("prepared-queries", query); err != nil { + return fmt.Errorf("failed prepared query delete: %s", err) } - if err := tx.Insert("index", &IndexEntry{"queries", idx}); err != nil { + if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil { return fmt.Errorf("failed updating index: %s", err) } - watches.Arm("queries") + watches.Arm("prepared-queries") return nil } -// QueryGet returns the given prepared query by ID. -func (s *StateStore) QueryGet(queryID string) (uint64, *structs.PreparedQuery, error) { +// PreparedQueryGet returns the given prepared query by ID. +func (s *StateStore) PreparedQueryGet(queryID string) (uint64, *structs.PreparedQuery, error) { tx := s.db.Txn(false) defer tx.Abort() // Get the table index. - idx := maxIndexTxn(tx, s.getWatchTables("QueryGet")...) + idx := maxIndexTxn(tx, s.getWatchTables("PreparedQueryGet")...) // Look up the query by its ID. - query, err := tx.First("queries", "id", queryID) + query, err := tx.First("prepared-queries", "id", queryID) if err != nil { - return 0, nil, fmt.Errorf("failed query lookup: %s", err) + return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) } if query != nil { return idx, query.(*structs.PreparedQuery), nil @@ -177,13 +176,14 @@ func (s *StateStore) QueryGet(queryID string) (uint64, *structs.PreparedQuery, e return idx, nil, nil } -// QueryLookup returns the given prepared query by looking up an ID or Name. -func (s *StateStore) QueryLookup(queryIDOrName string) (uint64, *structs.PreparedQuery, error) { +// PreparedQueryLookup returns the given prepared query by looking up an ID or +// Name. +func (s *StateStore) PreparedQueryLookup(queryIDOrName string) (uint64, *structs.PreparedQuery, error) { tx := s.db.Txn(false) defer tx.Abort() // Get the table index. - idx := maxIndexTxn(tx, s.getWatchTables("QueryLookup")...) + idx := maxIndexTxn(tx, s.getWatchTables("PreparedQueryLookup")...) // Explicitly ban an empty query. This will never match an ID and the // schema is set up so it will never match a query with an empty name, @@ -194,22 +194,22 @@ func (s *StateStore) QueryLookup(queryIDOrName string) (uint64, *structs.Prepare } // Try first by ID. - query, err := tx.First("queries", "id", queryIDOrName) + query, err := tx.First("prepared-queries", "id", queryIDOrName) // This is a little unfortunate but the UUID index will complain // if the name isn't formatted like a UUID, so we can safely // ignore any UUID format-related errors. if err != nil && !strings.Contains(err.Error(), "UUID") { - return 0, nil, fmt.Errorf("failed query lookup: %s", err) + return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) } if query != nil { return idx, query.(*structs.PreparedQuery), nil } // Then try by name. - query, err = tx.First("queries", "name", queryIDOrName) + query, err = tx.First("prepared-queries", "name", queryIDOrName) if err != nil { - return 0, nil, fmt.Errorf("failed query lookup: %s", err) + return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) } if query != nil { return idx, query.(*structs.PreparedQuery), nil @@ -218,18 +218,18 @@ func (s *StateStore) QueryLookup(queryIDOrName string) (uint64, *structs.Prepare return idx, nil, nil } -// QueryList returns all the prepared queries. -func (s *StateStore) QueryList() (uint64, structs.PreparedQueries, error) { +// PreparedQueryList returns all the prepared queries. +func (s *StateStore) PreparedQueryList() (uint64, structs.PreparedQueries, error) { tx := s.db.Txn(false) defer tx.Abort() // Get the table index. - idx := maxIndexTxn(tx, s.getWatchTables("QueryList")...) + idx := maxIndexTxn(tx, s.getWatchTables("PreparedQueryList")...) // Query all of the prepared queries in the state store. - queries, err := tx.Get("queries", "id") + queries, err := tx.Get("prepared-queries", "id") if err != nil { - return 0, nil, fmt.Errorf("failed query lookup: %s", err) + return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) } // Go over all of the queries and build the response. diff --git a/consul/state/query_test.go b/consul/state/prepared_query_test.go similarity index 78% rename from consul/state/query_test.go rename to consul/state/prepared_query_test.go index 645c96fa36..a7f100d120 100644 --- a/consul/state/query_test.go +++ b/consul/state/prepared_query_test.go @@ -8,22 +8,22 @@ import ( "github.com/hashicorp/consul/consul/structs" ) -func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { +func TestStateStore_PreparedQuerySet_PreparedQueryGet(t *testing.T) { s := testStateStore(t) // Querying with no results returns nil. - idx, res, err := s.QueryGet(testUUID()) + idx, res, err := s.PreparedQueryGet(testUUID()) if idx != 0 || res != nil || err != nil { t.Fatalf("expected (0, nil, nil), got: (%d, %#v, %#v)", idx, res, err) } // Inserting a query with empty ID is disallowed. - if err := s.QuerySet(1, &structs.PreparedQuery{}); err == nil { + if err := s.PreparedQuerySet(1, &structs.PreparedQuery{}); err == nil { t.Fatalf("expected %#v, got: %#v", ErrMissingQueryID, err) } // Index is not updated if nothing is saved. - if idx := s.maxIndex("queries"); idx != 0 { + if idx := s.maxIndex("prepared-queries"); idx != 0 { t.Fatalf("bad index: %d", idx) } @@ -36,13 +36,13 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { } // The set will still fail because the service isn't registered yet. - err = s.QuerySet(1, query) + err = s.PreparedQuerySet(1, query) if err == nil || !strings.Contains(err.Error(), "invalid service") { t.Fatalf("bad: %v", err) } // Index is not updated if nothing is saved. - if idx := s.maxIndex("queries"); idx != 0 { + if idx := s.maxIndex("prepared-queries"); idx != 0 { t.Fatalf("bad index: %d", idx) } @@ -51,12 +51,12 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { testRegisterService(t, s, 2, "foo", "redis") // This should go through. - if err := s.QuerySet(3, query); err != nil { + if err := s.PreparedQuerySet(3, query); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 3 { + if idx := s.maxIndex("prepared-queries"); idx != 3 { t.Fatalf("bad index: %d", idx) } @@ -71,7 +71,7 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { ModifyIndex: 3, }, } - idx, actual, err := s.QueryGet(query.ID) + idx, actual, err := s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -84,19 +84,19 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { // Give it a name and set it again. query.Name = "test-query" - if err := s.QuerySet(4, query); err != nil { + if err := s.PreparedQuerySet(4, query); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 4 { + if idx := s.maxIndex("prepared-queries"); idx != 4 { t.Fatalf("bad index: %d", idx) } // Read it back and verify the data was updated as well as the index. expected.Name = "test-query" expected.ModifyIndex = 4 - idx, actual, err = s.QueryGet(query.ID) + idx, actual, err = s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -109,13 +109,13 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { // Try to tie it to a bogus session. query.Session = testUUID() - err = s.QuerySet(5, query) + err = s.PreparedQuerySet(5, query) if err == nil || !strings.Contains(err.Error(), "invalid session") { t.Fatalf("bad: %v", err) } // Index is not updated if nothing is saved. - if idx := s.maxIndex("queries"); idx != 4 { + if idx := s.maxIndex("prepared-queries"); idx != 4 { t.Fatalf("bad index: %d", idx) } @@ -127,19 +127,19 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { if err := s.SessionCreate(5, session); err != nil { t.Fatalf("err: %s", err) } - if err := s.QuerySet(6, query); err != nil { + if err := s.PreparedQuerySet(6, query); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 6 { + if idx := s.maxIndex("prepared-queries"); idx != 6 { t.Fatalf("bad index: %d", idx) } // Read it back and verify the data was updated as well as the index. expected.Session = query.Session expected.ModifyIndex = 6 - idx, actual, err = s.QueryGet(query.ID) + idx, actual, err = s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -159,18 +159,18 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { Service: "redis", }, } - err = s.QuerySet(7, evil) + err = s.PreparedQuerySet(7, evil) if err == nil || !strings.Contains(err.Error(), "aliases an existing query") { t.Fatalf("bad: %v", err) } // Index is not updated if nothing is saved. - if idx := s.maxIndex("queries"); idx != 6 { + if idx := s.maxIndex("prepared-queries"); idx != 6 { t.Fatalf("bad index: %d", idx) } // Sanity check to make sure it's not there. - idx, actual, err = s.QueryGet(evil.ID) + idx, actual, err = s.PreparedQueryGet(evil.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -182,7 +182,7 @@ func TestStateStore_Query_QuerySet_QueryGet(t *testing.T) { } } -func TestStateStore_Query_QueryDelete(t *testing.T) { +func TestStateStore_PreparedQueryDelete(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -198,22 +198,22 @@ func TestStateStore_Query_QueryDelete(t *testing.T) { } // Deleting a query that doesn't exist should be a no-op. - if err := s.QueryDelete(3, query.ID); err != nil { + if err := s.PreparedQueryDelete(3, query.ID); err != nil { t.Fatalf("err: %s", err) } // Index is not updated if nothing is saved. - if idx := s.maxIndex("queries"); idx != 0 { + if idx := s.maxIndex("prepared-queries"); idx != 0 { t.Fatalf("bad index: %d", idx) } // Now add the query to the data store. - if err := s.QuerySet(3, query); err != nil { + if err := s.PreparedQuerySet(3, query); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 3 { + if idx := s.maxIndex("prepared-queries"); idx != 3 { t.Fatalf("bad index: %d", idx) } @@ -228,7 +228,7 @@ func TestStateStore_Query_QueryDelete(t *testing.T) { ModifyIndex: 3, }, } - idx, actual, err := s.QueryGet(query.ID) + idx, actual, err := s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -240,17 +240,17 @@ func TestStateStore_Query_QueryDelete(t *testing.T) { } // Now delete it. - if err := s.QueryDelete(4, query.ID); err != nil { + if err := s.PreparedQueryDelete(4, query.ID); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 4 { + if idx := s.maxIndex("prepared-queries"); idx != 4 { t.Fatalf("bad index: %d", idx) } // Sanity check to make sure it's not there. - idx, actual, err = s.QueryGet(query.ID) + idx, actual, err = s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -262,7 +262,7 @@ func TestStateStore_Query_QueryDelete(t *testing.T) { } } -func TestStateStore_Query_QueryLookup(t *testing.T) { +func TestStateStore_PreparedQueryLookup(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -280,7 +280,7 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { // Try to lookup a query that's not there using something that looks // like a real ID. - idx, actual, err := s.QueryLookup(query.ID) + idx, actual, err := s.PreparedQueryLookup(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -293,7 +293,7 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { // Try to lookup a query that's not there using something that looks // like a name - idx, actual, err = s.QueryLookup(query.Name) + idx, actual, err = s.PreparedQueryLookup(query.Name) if err != nil { t.Fatalf("err: %s", err) } @@ -305,12 +305,12 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { } // Now actually insert the query. - if err := s.QuerySet(3, query); err != nil { + if err := s.PreparedQuerySet(3, query); err != nil { t.Fatalf("err: %s", err) } // Make sure the index got updated. - if idx := s.maxIndex("queries"); idx != 3 { + if idx := s.maxIndex("prepared-queries"); idx != 3 { t.Fatalf("bad index: %d", idx) } @@ -326,7 +326,7 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { ModifyIndex: 3, }, } - idx, actual, err = s.QueryLookup(query.ID) + idx, actual, err = s.PreparedQueryLookup(query.ID) if err != nil { t.Fatalf("err: %s", err) } @@ -338,7 +338,7 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { } // Read it back using the name and verify it again. - idx, actual, err = s.QueryLookup(query.Name) + idx, actual, err = s.PreparedQueryLookup(query.Name) if err != nil { t.Fatalf("err: %s", err) } @@ -351,12 +351,12 @@ func TestStateStore_Query_QueryLookup(t *testing.T) { // Make sure an empty lookup is well-behaved if there are actual queries // in the state store. - if _, _, err = s.QueryLookup(""); err != ErrMissingQueryID { + if _, _, err = s.PreparedQueryLookup(""); err != ErrMissingQueryID { t.Fatalf("bad: %v", err) } } -func TestStateStore_Query_QueryList(t *testing.T) { +func TestStateStore_PreparedQueryList(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -389,7 +389,7 @@ func TestStateStore_Query_QueryList(t *testing.T) { // Now create the queries. for i, query := range queries { - if err := s.QuerySet(uint64(4+i), query); err != nil { + if err := s.PreparedQuerySet(uint64(4+i), query); err != nil { t.Fatalf("err: %s", err) } } @@ -419,7 +419,7 @@ func TestStateStore_Query_QueryList(t *testing.T) { }, }, } - idx, actual, err := s.QueryList() + idx, actual, err := s.PreparedQueryList() if err != nil { t.Fatalf("err: %s", err) } @@ -431,7 +431,7 @@ func TestStateStore_Query_QueryList(t *testing.T) { } } -func TestStateStore_Query_Snapshot_Restore(t *testing.T) { +func TestStateStore_PreparedQuery_Snapshot_Restore(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -464,7 +464,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { // Now create the queries. for i, query := range queries { - if err := s.QuerySet(uint64(4+i), query); err != nil { + if err := s.PreparedQuerySet(uint64(4+i), query); err != nil { t.Fatalf("err: %s", err) } } @@ -474,7 +474,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { defer snap.Close() // Alter the real state store. - if err := s.QueryDelete(6, queries[0].ID); err != nil { + if err := s.PreparedQueryDelete(6, queries[0].ID); err != nil { t.Fatalf("err: %s", err) } @@ -506,7 +506,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { }, }, } - iter, err := snap.Queries() + iter, err := snap.PreparedQueries() if err != nil { t.Fatalf("err: %s", err) } @@ -523,7 +523,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { s := testStateStore(t) restore := s.Restore() for _, query := range dump { - if err := restore.Query(query); err != nil { + if err := restore.PreparedQuery(query); err != nil { t.Fatalf("err: %s", err) } } @@ -531,7 +531,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { // Read the restored queries back out and verify that they // match. - idx, actual, err := s.QueryList() + idx, actual, err := s.PreparedQueryList() if err != nil { t.Fatalf("err: %s", err) } @@ -544,7 +544,7 @@ func TestStateStore_Query_Snapshot_Restore(t *testing.T) { }() } -func TestStateStore_Query_Watches(t *testing.T) { +func TestStateStore_PreparedQuery_Watches(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -560,19 +560,19 @@ func TestStateStore_Query_Watches(t *testing.T) { // Call functions that update the queries table and make sure a watch // fires each time. - verifyWatch(t, s.getTableWatch("queries"), func() { - if err := s.QuerySet(3, query); err != nil { + verifyWatch(t, s.getTableWatch("prepared-queries"), func() { + if err := s.PreparedQuerySet(3, query); err != nil { t.Fatalf("err: %s", err) } }) - verifyWatch(t, s.getTableWatch("queries"), func() { - if err := s.QueryDelete(4, query.ID); err != nil { + verifyWatch(t, s.getTableWatch("prepared-queries"), func() { + if err := s.PreparedQueryDelete(4, query.ID); err != nil { t.Fatalf("err: %s", err) } }) - verifyWatch(t, s.getTableWatch("queries"), func() { + verifyWatch(t, s.getTableWatch("prepared-queries"), func() { restore := s.Restore() - if err := restore.Query(query); err != nil { + if err := restore.PreparedQuery(query); err != nil { t.Fatalf("err: %s", err) } restore.Commit() diff --git a/consul/state/schema.go b/consul/state/schema.go index 4bb354ad64..a85a5afcb5 100644 --- a/consul/state/schema.go +++ b/consul/state/schema.go @@ -30,7 +30,7 @@ func stateStoreSchema() *memdb.DBSchema { sessionChecksTableSchema, aclsTableSchema, coordinatesTableSchema, - queriesTableSchema, + preparedQueriesTableSchema, } // Add the tables to the root schema @@ -367,11 +367,11 @@ func coordinatesTableSchema() *memdb.TableSchema { } } -// queriesTableSchema returns a new table schema used for storing +// preparedQueriesTableSchema returns a new table schema used for storing // prepared queries. -func queriesTableSchema() *memdb.TableSchema { +func preparedQueriesTableSchema() *memdb.TableSchema { return &memdb.TableSchema{ - Name: "queries", + Name: "prepared-queries", Indexes: map[string]*memdb.IndexSchema{ "id": &memdb.IndexSchema{ Name: "id", diff --git a/consul/state/state_store.go b/consul/state/state_store.go index 286700a67d..76a8b5f3b7 100644 --- a/consul/state/state_store.go +++ b/consul/state/state_store.go @@ -413,8 +413,8 @@ func (s *StateStore) getWatchTables(method string) []string { return []string{"acls"} case "Coordinates": return []string{"coordinates"} - case "QueryGet", "QueryLookup", "QueryList": - return []string{"queries"} + case "PreparedQueryGet", "PreparedQueryLookup", "PreparedQueryList": + return []string{"prepared-queries"} } panic(fmt.Sprintf("Unknown method %s", method)) @@ -2141,9 +2141,9 @@ func (s *StateStore) deleteSessionTxn(tx *memdb.Txn, idx uint64, watches *DumbWa } // Delete any prepared queries. - queries, err := tx.Get("queries", "session", sessionID) + queries, err := tx.Get("prepared-queries", "session", sessionID) if err != nil { - return fmt.Errorf("failed query lookup: %s", err) + return fmt.Errorf("failed prepared query lookup: %s", err) } { var objs []interface{} @@ -2154,8 +2154,8 @@ func (s *StateStore) deleteSessionTxn(tx *memdb.Txn, idx uint64, watches *DumbWa // Do the delete in a separate loop so we don't trash the iterator. for _, obj := range objs { q := obj.(*structs.PreparedQuery) - if err := s.queryDeleteTxn(tx, idx, watches, q.ID); err != nil { - return fmt.Errorf("failed query delete: %s", err) + if err := s.preparedQueryDeleteTxn(tx, idx, watches, q.ID); err != nil { + return fmt.Errorf("failed prepared query delete: %s", err) } } } diff --git a/consul/state/state_store_test.go b/consul/state/state_store_test.go index c022a4c464..97e71c9daa 100644 --- a/consul/state/state_store_test.go +++ b/consul/state/state_store_test.go @@ -4445,7 +4445,7 @@ func TestStateStore_Session_Invalidate_Key_Delete_Behavior(t *testing.T) { } } -func TestStateStore_Session_Invalidate_Query_Delete(t *testing.T) { +func TestStateStore_Session_Invalidate_PreparedQuery_Delete(t *testing.T) { s := testStateStore(t) // Set up our test environment. @@ -4465,13 +4465,13 @@ func TestStateStore_Session_Invalidate_Query_Delete(t *testing.T) { Service: "redis", }, } - if err := s.QuerySet(4, query); err != nil { + if err := s.PreparedQuerySet(4, query); err != nil { t.Fatalf("err: %s", err) } // Invalidate the session and make sure the watches fire. verifyWatch(t, s.getTableWatch("sessions"), func() { - verifyWatch(t, s.getTableWatch("queries"), func() { + verifyWatch(t, s.getTableWatch("prepared-queries"), func() { if err := s.SessionDestroy(5, session.ID); err != nil { t.Fatalf("err: %v", err) } @@ -4491,7 +4491,7 @@ func TestStateStore_Session_Invalidate_Query_Delete(t *testing.T) { } // Make sure the query is gone and the index is updated. - idx, q2, err := s.QueryGet(query.ID) + idx, q2, err := s.PreparedQueryGet(query.ID) if err != nil { t.Fatalf("err: %s", err) } diff --git a/consul/structs/query.go b/consul/structs/prepared_query.go similarity index 79% rename from consul/structs/query.go rename to consul/structs/prepared_query.go index 500ba1753a..a63bf5b829 100644 --- a/consul/structs/query.go +++ b/consul/structs/prepared_query.go @@ -92,34 +92,29 @@ type PreparedQuery struct { type PreparedQueries []*PreparedQuery -type QueryOp string +type PreparedQueryOp string const ( - QueryCreate QueryOp = "create" - QueryUpdate = "update" - QueryDelete = "delete" + PreparedQueryCreate PreparedQueryOp = "create" + PreparedQueryUpdate = "update" + PreparedQueryDelete = "delete" ) // QueryRequest is used to create or change prepared queries. -type QueryRequest struct { +type PreparedQueryRequest struct { Datacenter string - Op QueryOp + Op PreparedQueryOp Query PreparedQuery WriteRequest } // RequestDatacenter returns the datacenter for a given request. -func (q *QueryRequest) RequestDatacenter() string { +func (q *PreparedQueryRequest) RequestDatacenter() string { return q.Datacenter } -// QueryResponse is used to return the ID of an updated query. -type QueryResponse struct { - ID string -} - -// QueryExecuteRequest is used to execute a prepared query. -type QueryExecuteRequest struct { +// PreparedQueryExecuteRequest is used to execute a prepared query. +type PreparedQueryExecuteRequest struct { Datacenter string QueryIDOrName string Source QuerySource @@ -127,26 +122,26 @@ type QueryExecuteRequest struct { } // RequestDatacenter returns the datacenter for a given request. -func (q *QueryExecuteRequest) RequestDatacenter() string { +func (q *PreparedQueryExecuteRequest) RequestDatacenter() string { return q.Datacenter } -// QueryExecuteRemoteRequest is used when running a local query in a remote -// datacenter. We have to ship the entire query over since it won't be +// PreparedQueryExecuteRemoteRequest is used when running a local query in a +// remote datacenter. We have to ship the entire query over since it won't be // present in the remote state store. -type QueryExecuteRemoteRequest struct { +type PreparedQueryExecuteRemoteRequest struct { Datacenter string Query PreparedQuery QueryOptions } // RequestDatacenter returns the datacenter for a given request. -func (q *QueryExecuteRemoteRequest) RequestDatacenter() string { +func (q *PreparedQueryExecuteRemoteRequest) RequestDatacenter() string { return q.Datacenter } -// QueryExecuteResponse has the results of executing a query. -type QueryExecuteResponse struct { +// PreparedQueryExecuteResponse has the results of executing a query. +type PreparedQueryExecuteResponse struct { Nodes CheckServiceNodes DNS QueryDNSOptions } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index e3cf706d57..fd1ef08a9a 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -35,7 +35,7 @@ const ( ACLRequestType TombstoneRequestType CoordinateBatchUpdateType - QueryRequestType + PreparedQueryRequestType ) const ( From c41a3d6c8d1a6a7f3484659303d986d2b12642a8 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 20:39:15 -0800 Subject: [PATCH 020/123] Changes "not" prefix from "~" to "!". --- consul/prepared_query_endpoint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 830989322b..6bfcfe74b0 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -312,13 +312,13 @@ func (p *PreparedQuery) execute(query *structs.PreparedQuery, } // tagFilter returns a list of nodes who satisfy the given tags. Nodes must have -// ALL the given tags, and none of the forbidden tags (prefixed with ~). +// ALL the given tags, and none of the forbidden tags (prefixed with !). func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes { // Build up lists of required and disallowed tags. must, not := make([]string, 0), make([]string, 0) for _, tag := range tags { tag = strings.ToLower(tag) - if strings.HasPrefix(tag, "~") { + if strings.HasPrefix(tag, "!") { tag = tag[1:] not = append(not, tag) } else { From 7ca3f0a466b1b2527970ab70bc28bf1e4dd97bfd Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 20:42:52 -0800 Subject: [PATCH 021/123] Adds an explicit ACL check that will fail vs. trying other DCs. --- consul/prepared_query_endpoint.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 6bfcfe74b0..3ebd836aa9 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -292,9 +292,14 @@ func (p *PreparedQuery) execute(query *structs.PreparedQuery, // the token stored with the query, NOT the passed-in one, which is // critical to how queries work (the query becomes a proxy for a lookup // using the ACL it was created with). - if err := p.srv.filterACL(query.Token, nodes); err != nil { + acl, err := p.srv.resolveToken(query.Token) + if err != nil { return err } + if acl != nil && !acl.ServiceRead(query.Service.Service) { + p.srv.logger.Printf("[WARN] consul.prepared_query: Execute of prepared query for service '%s' denied due to ACLs", query.Service.Service) + return permissionDeniedErr + } // Filter out any unhealthy nodes. nodes = nodes.Filter(query.Service.OnlyPassing) From 2f34b516509cba1f5e2494973bbd4b2427ea702e Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 20:59:16 -0800 Subject: [PATCH 022/123] Moves sort to a query-time decision and adds back the limit. --- consul/prepared_query_endpoint.go | 39 +++++++++++++++-------------- consul/structs/prepared_query.go | 41 ++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 3ebd836aa9..7cbde1c71a 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -160,17 +160,6 @@ func parseService(svc *structs.ServiceQuery) error { // - OnlyPassing is just a boolean so doesn't need further validation. // - Tags is a free-form list of tags and doesn't need further validation. - // Sort order must be one of the allowed values, or if not given we - // default to "shuffle" so there's load balancing. - switch svc.Sort { - case structs.QueryOrderShuffle: - case structs.QueryOrderSort: - case "": - svc.Sort = structs.QueryOrderShuffle - default: - return fmt.Errorf("Bad Sort '%s'", svc.Sort) - } - return nil } @@ -226,10 +215,13 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, // Shuffle the results in case coordinates are not available if they // requested an RTT sort. reply.Nodes.Shuffle() - if query.Service.Sort == structs.QueryOrderSort { - if err := p.srv.sortNodesByDistanceFrom(args.Source, reply.Nodes); err != nil { - return err - } + if err := p.srv.sortNodesByDistanceFrom(args.Source, reply.Nodes); err != nil { + return err + } + + // Apply the limit if given. + if args.Limit > 0 && len(reply.Nodes) > args.Limit { + reply.Nodes = reply.Nodes[:args.Limit] } // In the happy path where we found some healthy nodes we go with that @@ -237,7 +229,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, // by the query setup. if len(reply.Nodes) == 0 { wrapper := &queryServerWrapper{p.srv} - if err := queryFailover(wrapper, query, args.QueryOptions, reply); err != nil { + if err := queryFailover(wrapper, query, args, reply); err != nil { return err } } @@ -274,6 +266,11 @@ func (p *PreparedQuery) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRe // balance the load across the results. reply.Nodes.Shuffle() + // Apply the limit if given. + if args.Limit > 0 && len(reply.Nodes) > args.Limit { + reply.Nodes = reply.Nodes[:args.Limit] + } + return nil } @@ -403,7 +400,7 @@ func (q *queryServerWrapper) GetOtherDatacentersByDistance() ([]string, error) { // queryFailover runs an algorithm to determine which DCs to try and then calls // them to try to locate alternative services. func queryFailover(q queryServer, query *structs.PreparedQuery, - options structs.QueryOptions, + args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { // Build a candidate list of DCs, starting with the nearest N from RTTs. @@ -433,12 +430,16 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, } } - // Now try the selected DCs in priority order. + // Now try the selected DCs in priority order. Note that we pass along + // the limit since it can be applied remotely to save bandwidth. We also + // pass along the consistency mode information we were given, so that + // applies to the remote query as well. for _, dc := range dcs { remote := &structs.PreparedQueryExecuteRemoteRequest{ Datacenter: dc, Query: *query, - QueryOptions: options, + Limit: args.Limit, + QueryOptions: args.QueryOptions, } if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, remote, reply); err != nil { return err diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index a63bf5b829..4b6cde3b9e 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -4,11 +4,6 @@ import ( "time" ) -const ( - QueryOrderShuffle = "shuffle" - QueryOrderSort = "near_agent" -) - const ( QueryTTLMax = 24 * time.Hour QueryTTLMin = 10 * time.Second @@ -52,10 +47,6 @@ type ServiceQuery struct { // this list it must be present. If the tag is preceded with "~" then // it is disallowed. Tags []string - - // Sort has one of the QueryOrder* options which control how the output - // is sorted. If this is left blank we default to "shuffle". - Sort string } // PreparedQuery defines a complete prepared query, and is the structure we @@ -115,9 +106,22 @@ func (q *PreparedQueryRequest) RequestDatacenter() string { // PreparedQueryExecuteRequest is used to execute a prepared query. type PreparedQueryExecuteRequest struct { - Datacenter string + // 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 - Source QuerySource + + // Limit will trim the resulting list down to the given limit. + Limit int + + // Source is used to sort the results relative to a given node using + // network coordinates. + Source QuerySource + + // QueryOptions (unfortunately named here) controls the consistency + // settings for the query lookup itself, as well as the service lookups. QueryOptions } @@ -127,11 +131,20 @@ func (q *PreparedQueryExecuteRequest) RequestDatacenter() string { } // PreparedQueryExecuteRemoteRequest is used when running a local query in a -// remote datacenter. We have to ship the entire query over since it won't be -// present in the remote state store. +// remote datacenter. type PreparedQueryExecuteRemoteRequest struct { + // Datacenter is the target this request is intended for. Datacenter string - Query PreparedQuery + + // Query is a copy of the query to execute. We have to ship the entire + // query over since it won't be present in the remote state store. + Query PreparedQuery + + // Limit will trim the resulting list down to the given limit. + Limit int + + // QueryOptions (unfortunately named here) controls the consistency + // settings for the the service lookups. QueryOptions } From 666619dfc928c48a3c0d377bd369bff169901b5d Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 21:13:53 -0800 Subject: [PATCH 023/123] Skips unknown DCs during queries and chugs along in the face of errors. --- consul/prepared_query_endpoint.go | 49 +++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 7cbde1c71a..3840d069ca 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -3,6 +3,7 @@ package consul import ( "errors" "fmt" + "log" "strings" "time" @@ -366,6 +367,7 @@ func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServ // queryServer is a wrapper that makes it easier to test the failover logic. type queryServer interface { + GetLogger() *log.Logger GetOtherDatacentersByDistance() ([]string, error) ForwardDC(method, dc string, args interface{}, reply interface{}) error } @@ -375,9 +377,9 @@ type queryServerWrapper struct { srv *Server } -// ForwardDC calls into the server's RPC forwarder. -func (q *queryServerWrapper) ForwardDC(method, dc string, args interface{}, reply interface{}) error { - return q.srv.forwardDC(method, dc, args, reply) +// GetLogger returns the server's logger. +func (q *queryServerWrapper) GetLogger() *log.Logger { + return q.srv.logger } // GetOtherDatacentersByDistance calls into the server's fn and filters out the @@ -397,21 +399,35 @@ func (q *queryServerWrapper) GetOtherDatacentersByDistance() ([]string, error) { return result, nil } +// ForwardDC calls into the server's RPC forwarder. +func (q *queryServerWrapper) ForwardDC(method, dc string, args interface{}, reply interface{}) error { + return q.srv.forwardDC(method, dc, args, reply) +} + // queryFailover runs an algorithm to determine which DCs to try and then calls // them to try to locate alternative services. func queryFailover(q queryServer, query *structs.PreparedQuery, args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { - // Build a candidate list of DCs, starting with the nearest N from RTTs. + // Pull the list of other DCs. This is sorted by RTT in case the user + // has selected that. + nearest, err := q.GetOtherDatacentersByDistance() + if err != nil { + return err + } + + // This will help us filter unknown DCs supplied by the user. + known := make(map[string]struct{}) + for _, dc := range nearest { + known[dc] = struct{}{} + } + + // Build a candidate list of DCs to try, starting with the nearest N + // from RTTs. var dcs []string index := make(map[string]struct{}) if query.Service.Failover.NearestN > 0 { - nearest, err := q.GetOtherDatacentersByDistance() - if err != nil { - return err - } - for i, dc := range nearest { if !(i < query.Service.Failover.NearestN) { break @@ -424,8 +440,16 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, // Then add any DCs explicitly listed that weren't selected above. for _, dc := range query.Service.Failover.Datacenters { - _, ok := index[dc] - if !ok { + // This will prevent a log of other log spammage if we do not + // attempt to talk to datacenters we don't know about. + if _, ok := known[dc]; !ok { + q.GetLogger().Printf("[DEBUG] consul.prepared_query: Skipping unknown datacenter '%s' in prepared query", dc) + continue + } + + // This will make sure we don't re-try something that fails + // from the NearestN list. + if _, ok := index[dc]; !ok { dcs = append(dcs, dc) } } @@ -442,7 +466,8 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, QueryOptions: args.QueryOptions, } if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, remote, reply); err != nil { - return err + q.GetLogger().Printf("[WARN] consul.prepared_query: Failed querying for service '%s' in datacenter '%s': %s", query.Service.Service, dc, err) + continue } // We can stop if we found some nodes. From 0bd7e82686577e39258f8f0d58825f3f23c6883d Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 21:15:55 -0800 Subject: [PATCH 024/123] Clarifies comment about name vs. ID. --- consul/state/prepared_query.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index e8f2e48ab3..baba66c8a7 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -68,9 +68,12 @@ func (s *StateStore) preparedQuerySetTxn(tx *memdb.Txn, idx uint64, query *struc query.ModifyIndex = idx } - // Verify that the name doesn't alias any existing ID. If we didn't do - // this then a bad actor could steal traffic away from an existing DNS - // entry. + // Verify that the name doesn't alias any existing ID. We allow queries + // to be looked up by ID *or* name so we don't want anyone to try to + // register a query with a name equal to some other query's ID in an + // attempt to hijack it. We also look up by ID *then* name in order to + // prevent this, but it seems prudent to prevent these types of rogue + // queries from ever making it into the state store. if query.Name != "" { existing, err := tx.First("prepared-queries", "id", query.Name) From 58bb6e8ba46232a769ce3c5e503b58bf8fe2d9cc Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 21:48:35 -0800 Subject: [PATCH 025/123] Checks for valid UUIDs before calling in to index function. --- consul/state/prepared_query.go | 49 ++++++++++++++++------------- consul/state/prepared_query_test.go | 20 ++++++++++++ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index baba66c8a7..b404977e3c 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -2,12 +2,20 @@ package state import ( "fmt" - "strings" + "regexp" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/go-memdb" ) +// validUUID is used to check if a given string looks like a UUID +var validUUID = regexp.MustCompile(`^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`) + +// isUUID returns true if the given string is a valid UUID. +func isUUID(str string) bool { + return validUUID.MatchString(str) +} + // PreparedQueries is used to pull all the prepared queries from the snapshot. func (s *StateSnapshot) PreparedQueries() (memdb.ResultIterator, error) { iter, err := s.tx.Get("prepared-queries", "id") @@ -73,15 +81,14 @@ func (s *StateStore) preparedQuerySetTxn(tx *memdb.Txn, idx uint64, query *struc // register a query with a name equal to some other query's ID in an // attempt to hijack it. We also look up by ID *then* name in order to // prevent this, but it seems prudent to prevent these types of rogue - // queries from ever making it into the state store. - if query.Name != "" { + // queries from ever making it into the state store. Note that we have + // to see if the name looks like a UUID before checking since the UUID + // index will complain if we look up something that's not formatted + // like one. + if isUUID(query.Name) { existing, err := tx.First("prepared-queries", "id", query.Name) - - // This is a little unfortunate but the UUID index will complain - // if the name isn't formatted like a UUID, so we can safely - // ignore any UUID format-related errors. - if err != nil && !strings.Contains(err.Error(), "UUID") { - return fmt.Errorf("failed query lookup: %s", err) + if err != nil { + return fmt.Errorf("failed prepared query lookup: %s", err) } if existing != nil { return fmt.Errorf("name '%s' aliases an existing query id", query.Name) @@ -196,21 +203,21 @@ func (s *StateStore) PreparedQueryLookup(queryIDOrName string) (uint64, *structs return idx, nil, ErrMissingQueryID } - // Try first by ID. - query, err := tx.First("prepared-queries", "id", queryIDOrName) - - // This is a little unfortunate but the UUID index will complain - // if the name isn't formatted like a UUID, so we can safely - // ignore any UUID format-related errors. - if err != nil && !strings.Contains(err.Error(), "UUID") { - return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) - } - if query != nil { - return idx, query.(*structs.PreparedQuery), nil + // Try first by ID if it looks like they gave us an ID. We check the + // format before trying this because the UUID index will complain if + // we look up something that's not formatted like one. + if isUUID(queryIDOrName) { + query, err := tx.First("prepared-queries", "id", queryIDOrName) + if err != nil { + return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) + } + if query != nil { + return idx, query.(*structs.PreparedQuery), nil + } } // Then try by name. - query, err = tx.First("prepared-queries", "name", queryIDOrName) + query, err := tx.First("prepared-queries", "name", queryIDOrName) if err != nil { return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err) } diff --git a/consul/state/prepared_query_test.go b/consul/state/prepared_query_test.go index a7f100d120..d47fab840c 100644 --- a/consul/state/prepared_query_test.go +++ b/consul/state/prepared_query_test.go @@ -8,6 +8,26 @@ import ( "github.com/hashicorp/consul/consul/structs" ) +func TestStateStore_PreparedQuery_isUUID(t *testing.T) { + cases := map[string]bool{ + "": false, + "nope": false, + "f004177f-2c28-83b7-4229-eacc25fe55d1": true, + " f004177f-2c28-83b7-4229-eacc25fe55d1": false, // Leading whitespace + "f004177f-2c28-83b7-4229-eacc25fe55d1 ": false, // Trailing whitespace + "f004177f-2c28-83B7-4229-eacc25fe55d1": false, // Bad hex "83B7" + } + for i := 0; i < 100; i++ { + cases[testUUID()] = true + } + + for str, expected := range cases { + if actual := isUUID(str); actual != expected { + t.Fatalf("bad: '%s'", str) + } + } +} + func TestStateStore_PreparedQuerySet_PreparedQueryGet(t *testing.T) { s := testStateStore(t) From 333da2a96c1772d6a14ad6cbac29b57964019e44 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 9 Nov 2015 23:03:20 -0800 Subject: [PATCH 026/123] Adds lookup and list endpoints and basic end-to-end apply test. --- consul/prepared_query_endpoint.go | 70 ++++++++++++++++ consul/prepared_query_endpoint_test.go | 109 ++++++++++++++++++++++++- consul/structs/prepared_query.go | 30 +++++++ 3 files changed, 208 insertions(+), 1 deletion(-) 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. From 8222d3f46298a66f498417fd2e970c3336de15fb Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 10:29:55 -0800 Subject: [PATCH 027/123] Completes non-ACL version of apply test. --- consul/fsm.go | 2 +- consul/prepared_query_endpoint.go | 13 ++-- consul/prepared_query_endpoint_test.go | 87 ++++++++++++++++++++------ consul/structs/prepared_query.go | 7 +-- 4 files changed, 78 insertions(+), 31 deletions(-) diff --git a/consul/fsm.go b/consul/fsm.go index 7116a6255f..3be73fa2e0 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -276,7 +276,7 @@ func (c *consulFSM) applyPreparedQueryOperation(buf []byte, index uint64) interf defer metrics.MeasureSince([]string{"consul", "fsm", "prepared-query", string(req.Op)}, time.Now()) switch req.Op { case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: - return c.state.PreparedQuerySet(index, &req.Query) + return c.state.PreparedQuerySet(index, req.Query) case structs.PreparedQueryDelete: return c.state.PreparedQueryDelete(index, req.Query.ID) default: diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 40e371b8e1..fb25136e9a 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -79,7 +79,7 @@ func (p *PreparedQuery) Apply(args *structs.PreparedQueryRequest, reply *string) // Parse the query and prep it for the state store. switch args.Op { case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: - if err := parseQuery(&args.Query); err != nil { + if err := parseQuery(args.Query); err != nil { return fmt.Errorf("Invalid prepared query: %v", err) } @@ -182,7 +182,7 @@ func parseDNS(dns *structs.QueryDNSOptions) error { } // Lookup returns a single prepared query by ID or name. -func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply *structs.IndexedPreparedQuery) error { +func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply *structs.IndexedPreparedQueries) error { if done, err := p.srv.forward("PreparedQuery.Lookup", args, args, reply); done { return err } @@ -206,12 +206,17 @@ func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply return err } - if (query.Token != args.Token) && (acl != nil && !acl.QueryModify()) { + if (query != nil) && (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 + reply.Index = index + if query != nil { + reply.Queries = structs.PreparedQueries{query} + } else { + reply.Queries = nil + } return nil }) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index cca1e666e8..8ac16419d7 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -2,6 +2,7 @@ package consul import ( "os" + "reflect" "strings" "testing" @@ -43,7 +44,7 @@ func TestPreparedQuery_Apply(t *testing.T) { arg := structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, - Query: structs.PreparedQuery{ + Query: &structs.PreparedQuery{ Service: structs.ServiceQuery{ Service: "redis", }, @@ -91,13 +92,62 @@ func TestPreparedQuery_Apply(t *testing.T) { t.Fatalf("err: %v", err) } - // Capture the new ID and make the op an update. This should go through. - arg.Op = structs.PreparedQueryUpdate + // Capture the ID and read the query back to verify. arg.Query.ID = reply + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: arg.Query.ID, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, arg.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Make the op an update. This should go through now that we have an ID. + arg.Op = structs.PreparedQueryUpdate + arg.Query.Service.Failover.NearestN = 2 if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { t.Fatalf("err: %v", err) } + // Read back again to verify the update worked. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: arg.Query.ID, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, arg.Query) { + t.Fatalf("bad: %v", actual) + } + } + // Give a bogus op and make sure it fails. arg.Op = "nope" err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) @@ -113,16 +163,6 @@ func TestPreparedQuery_Apply(t *testing.T) { 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 @@ -130,12 +170,19 @@ func TestPreparedQuery_Apply(t *testing.T) { 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) + // Verify that this query is deleted. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: arg.Query.ID, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } } } diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index c21cd7e6de..03efd078fc 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -83,11 +83,6 @@ type PreparedQuery struct { type PreparedQueries []*PreparedQuery -type IndexedPreparedQuery struct { - Query *PreparedQuery - QueryMeta -} - type IndexedPreparedQueries struct { Queries PreparedQueries QueryMeta @@ -105,7 +100,7 @@ const ( type PreparedQueryRequest struct { Datacenter string Op PreparedQueryOp - Query PreparedQuery + Query *PreparedQuery WriteRequest } From d4d866c6d5e1fb7e14c6b791d7d4bd5569f75abe Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 11:16:17 -0800 Subject: [PATCH 028/123] Adds ACL cases for apply. --- consul/prepared_query_endpoint_test.go | 375 ++++++++++++++++++++++--- 1 file changed, 341 insertions(+), 34 deletions(-) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index 8ac16419d7..bb5b0f231b 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -22,7 +22,7 @@ func TestPreparedQuery_Apply(t *testing.T) { // Set up a node and service in the catalog. { - arg := structs.RegisterRequest{ + req := structs.RegisterRequest{ Datacenter: "dc1", Node: "foo", Address: "127.0.0.1", @@ -33,15 +33,14 @@ func TestPreparedQuery_Apply(t *testing.T) { }, } var reply struct{} - - err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &reply) + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply) if err != nil { t.Fatalf("err: %v", err) } } // Set up a bare bones query. - arg := structs.PreparedQueryRequest{ + query := structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ @@ -53,16 +52,16 @@ func TestPreparedQuery_Apply(t *testing.T) { var reply string // Set an ID which should fail the create. - arg.Query.ID = "nope" - err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + query.Query.ID = "nope" + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &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) + query.Op = structs.PreparedQueryUpdate + query.Query.ID = generateUUID() + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) if err == nil || !strings.Contains(err.Error(), "Cannot modify non-existent prepared query") { t.Fatalf("bad: %v", err) } @@ -70,34 +69,34 @@ func TestPreparedQuery_Apply(t *testing.T) { // 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) + query.Op = structs.PreparedQueryCreate + query.Query.ID = "" + query.Query.Service.Failover.NearestN = -1 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &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) + query.Query.Service.Failover.NearestN = 0 + query.Query.Service.Service = "nope" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &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 { + query.Query.Service.Service = "redis" + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { t.Fatalf("err: %v", err) } // Capture the ID and read the query back to verify. - arg.Query.ID = reply + query.Query.ID = reply { req := &structs.PreparedQuerySpecificRequest{ Datacenter: "dc1", - QueryIDOrName: arg.Query.ID, + QueryIDOrName: query.Query.ID, } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { @@ -112,15 +111,15 @@ func TestPreparedQuery_Apply(t *testing.T) { t.Fatalf("bad index: %d", resp.Index) } actual.CreateIndex, actual.ModifyIndex = 0, 0 - if !reflect.DeepEqual(actual, arg.Query) { + if !reflect.DeepEqual(actual, query.Query) { t.Fatalf("bad: %v", actual) } } // Make the op an update. This should go through now that we have an ID. - arg.Op = structs.PreparedQueryUpdate - arg.Query.Service.Failover.NearestN = 2 - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply); err != nil { + query.Op = structs.PreparedQueryUpdate + query.Query.Service.Failover.NearestN = 2 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { t.Fatalf("err: %v", err) } @@ -128,7 +127,7 @@ func TestPreparedQuery_Apply(t *testing.T) { { req := &structs.PreparedQuerySpecificRequest{ Datacenter: "dc1", - QueryIDOrName: arg.Query.ID, + QueryIDOrName: query.Query.ID, } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { @@ -143,30 +142,30 @@ func TestPreparedQuery_Apply(t *testing.T) { t.Fatalf("bad index: %d", resp.Index) } actual.CreateIndex, actual.ModifyIndex = 0, 0 - if !reflect.DeepEqual(actual, arg.Query) { + if !reflect.DeepEqual(actual, query.Query) { t.Fatalf("bad: %v", actual) } } // Give a bogus op and make sure it fails. - arg.Op = "nope" - err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &arg, &reply) + query.Op = "nope" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &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) + query.Op = structs.PreparedQueryUpdate + query.Query.Service.Failover.NearestN = -1 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) if err == nil || !strings.Contains(err.Error(), "Bad NearestN") { t.Fatalf("bad: %v", err) } // 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 { + query.Op = structs.PreparedQueryDelete + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { t.Fatalf("err: %v", err) } @@ -174,7 +173,315 @@ func TestPreparedQuery_Apply(t *testing.T) { { req := &structs.PreparedQuerySpecificRequest{ Datacenter: "dc1", - QueryIDOrName: arg.Query.ID, + QueryIDOrName: query.Query.ID, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } +} + +func TestPreparedQuery_Apply_ACLDeny(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 two ACLs with read permission to the service. + var token1, token2 string + { + var rules = ` + service "redis" { + policy = "read" + } + ` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply string + + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + token1 = reply + + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + token2 = reply + } + + // Set up a node and service in the catalog. + { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "redis", + Tags: []string{"master"}, + Port: 8000, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply struct{} + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply) + if err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up a bare bones query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + } + var reply string + + // Creating without a token should fail since the default policy is to + // deny. + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + // Now add the token and try again. + query.WriteRequest.Token = token1 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Capture the ID and set the token, then read back the query to verify. + query.Query.ID = reply + query.Query.Token = token1 + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Try to do an update with a different token that does have access to + // the service, but isn't the one that was used to create the query. + query.Op = structs.PreparedQueryUpdate + query.WriteRequest.Token = token2 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + // Try again with no token. + query.WriteRequest.Token = "" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + // Try again with the original token. This should go through. + query.WriteRequest.Token = token1 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Try to do a delete with a different token that does have access to + // the service, but isn't the one that was used to create the query. + query.Op = structs.PreparedQueryDelete + query.WriteRequest.Token = token2 + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + // Try again with no token. + query.WriteRequest.Token = "" + err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + // Try again with the original token. This should go through. + query.WriteRequest.Token = token1 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Make sure the query got deleted. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Make the query again. + query.Op = structs.PreparedQueryCreate + query.Query.ID = "" + query.Query.Token = "" + query.WriteRequest.Token = token1 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Check that it's there. + query.Query.ID = reply + query.Query.Token = token1 + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // A management token should be able to update the query no matter what. + query.Op = structs.PreparedQueryUpdate + query.WriteRequest.Token = "root" + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // That last update should have changed the token to the management one. + query.Query.Token = "root" + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Make another query. + query.Op = structs.PreparedQueryCreate + query.Query.ID = "" + query.Query.Token = "" + query.WriteRequest.Token = token1 + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Check that it's there. + query.Query.ID = reply + query.Query.Token = token1 + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // A management token should be able to delete the query no matter what. + query.Op = structs.PreparedQueryDelete + query.WriteRequest.Token = "root" + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Make sure the query got deleted. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { From 519666a97c2454784502a322183c113218b2030e Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 11:33:00 -0800 Subject: [PATCH 029/123] Adds query parsing unit tests. --- consul/prepared_query_endpoint_test.go | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index bb5b0f231b..c393a77c79 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -493,3 +493,45 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { } } } + +func TestPreparedQuery_parseQuery(t *testing.T) { + query := &structs.PreparedQuery{} + + err := parseQuery(query) + if err == nil || !strings.Contains(err.Error(), "Must provide a service") { + t.Fatalf("bad: %v", err) + } + + query.Service.Service = "foo" + if err := parseQuery(query); err != nil { + t.Fatalf("err: %v", err) + } + + query.Service.Failover.NearestN = -1 + err = parseQuery(query) + if err == nil || !strings.Contains(err.Error(), "Bad NearestN") { + t.Fatalf("bad: %v", err) + } + + query.Service.Failover.NearestN = 3 + if err := parseQuery(query); err != nil { + t.Fatalf("err: %v", err) + } + + query.DNS.TTL = "two fortnights" + err = parseQuery(query) + if err == nil || !strings.Contains(err.Error(), "Bad DNS TTL") { + t.Fatalf("bad: %v", err) + } + + query.DNS.TTL = "-3s" + err = parseQuery(query) + if err == nil || !strings.Contains(err.Error(), "must be >=0") { + t.Fatalf("bad: %v", err) + } + + query.DNS.TTL = "3s" + if err := parseQuery(query); err != nil { + t.Fatalf("err: %v", err) + } +} From fa414a2092ebae37afeedd553ff916f8960f9bf8 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 12:13:40 -0800 Subject: [PATCH 030/123] Adds tests for query lookup and list endpoints. --- consul/prepared_query_endpoint.go | 2 +- consul/prepared_query_endpoint_test.go | 387 +++++++++++++++++++++++++ 2 files changed, 388 insertions(+), 1 deletion(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index fb25136e9a..cbd7264ade 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -206,7 +206,7 @@ func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply return err } - if (query != nil) && (query.Token != args.Token) && (acl != nil && !acl.QueryModify()) { + if (query != nil) && (query.Token != args.Token) && (acl != nil && !acl.QueryList()) { 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 } diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index c393a77c79..f69e986b0d 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -535,3 +535,390 @@ func TestPreparedQuery_parseQuery(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestPreparedQuery_Lookup(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 two ACLs with read permission to the service. + var token1, token2 string + { + var rules = ` + service "redis" { + policy = "read" + } + ` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply string + + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + token1 = reply + + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + token2 = reply + } + + // Set up a node and service in the catalog. + { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "redis", + Tags: []string{"master"}, + Port: 8000, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply struct{} + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply) + if err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up a bare bones query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "my-query", + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + WriteRequest: structs.WriteRequest{Token: token1}, + } + var reply string + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Capture the ID and set the token, then read back the query to verify. + query.Query.ID = reply + query.Query.Token = token1 + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: token1}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Now try to read it with a token that has read access to the + // service but isn't the token used to create the query. This should + // be denied. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: token2}, + } + var resp structs.IndexedPreparedQueries + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Try again with no token, which should also be denied. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: ""}, + } + var resp structs.IndexedPreparedQueries + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // A management token should be able to read no matter what. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Try a lookup by name instead of ID. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.Name, + QueryOptions: structs.QueryOptions{Token: token1}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Try to lookup an unknown ID. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: generateUUID(), + QueryOptions: structs.QueryOptions{Token: token1}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Try to lookup an unknown name. + { + req := &structs.PreparedQuerySpecificRequest{ + Datacenter: "dc1", + QueryIDOrName: "nope", + QueryOptions: structs.QueryOptions{Token: token1}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } +} + +func TestPreparedQuery_List(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 read permission to the service. + var token string + { + var rules = ` + service "redis" { + policy = "read" + } + ` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply string + + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + token = reply + } + + // Set up a node and service in the catalog. + { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "redis", + Tags: []string{"master"}, + Port: 8000, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply struct{} + err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply) + if err != nil { + t.Fatalf("err: %v", err) + } + } + + // Query with a legit management token but no queries. + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Set up a bare bones query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "my-query", + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + WriteRequest: structs.WriteRequest{Token: token}, + } + var reply string + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Capture the ID and set the token, then try to list all the queries. + // A management token is required so this should be denied. + query.Query.ID = reply + query.Query.Token = token + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: token}, + } + var resp structs.IndexedPreparedQueries + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", req, &resp) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // An empty token should fail in a similar way. + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: ""}, + } + var resp structs.IndexedPreparedQueries + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", req, &resp) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + if len(resp.Queries) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Now try a legit management token. + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedPreparedQueries + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.List", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Queries) != 1 { + t.Fatalf("bad: %v", resp) + } + actual := resp.Queries[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } +} From 7ded6c7a4a91cd9956f19a4fbd4f5262e4c20a4f Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 14:46:08 -0800 Subject: [PATCH 031/123] Adds a leader forwarding case for prepared queries. --- consul/prepared_query_endpoint_test.go | 70 ++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index f69e986b0d..35a8ded495 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1,6 +1,8 @@ package consul import ( + "fmt" + "net/rpc" "os" "reflect" "strings" @@ -494,6 +496,74 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { } } +func TestPreparedQuery_Apply_ForwardLeader(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec1 := rpcClient(t, s1) + defer codec1.Close() + + dir2, s2 := testServer(t) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + codec2 := rpcClient(t, s2) + defer codec2.Close() + + // Try to join. + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForLeader(t, s1.RPC, "dc1") + testutil.WaitForLeader(t, s2.RPC, "dc1") + + // Use the follower as the client. + var codec rpc.ClientCodec + if !s1.IsLeader() { + codec = codec1 + } else { + codec = codec2 + } + + // Set up a node and service in the catalog. + { + req := 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", &req, &reply) + if err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up a bare bones query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + } + + // Make sure the apply works even when forwarded through the non-leader. + var reply string + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } +} + func TestPreparedQuery_parseQuery(t *testing.T) { query := &structs.PreparedQuery{} From a9e9d5e311f5c9806cd7963ece8b8420d86348f7 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 15:16:41 -0800 Subject: [PATCH 032/123] Adds execute leader forward test for prepared queries. --- consul/prepared_query_endpoint_test.go | 131 +++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index 35a8ded495..881715c46b 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -992,3 +992,134 @@ func TestPreparedQuery_List(t *testing.T) { } } } + +func TestPreparedQuery_Execute_ForwardLeader(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec1 := rpcClient(t, s1) + defer codec1.Close() + + dir2, s2 := testServer(t) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + codec2 := rpcClient(t, s2) + defer codec2.Close() + + // Try to join. + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForLeader(t, s1.RPC, "dc1") + testutil.WaitForLeader(t, s2.RPC, "dc1") + + // Use the follower as the client. + var codec rpc.ClientCodec + if !s1.IsLeader() { + codec = codec1 + } else { + codec = codec2 + } + + // Set up a node and service in the catalog. + { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "redis", + Tags: []string{"master"}, + Port: 8000, + }, + } + var reply struct{} + if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up a bare bones query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "redis", + }, + }, + } + var reply string + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Apply", &query, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Execute it through the follower. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: reply, + } + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 1 { + t.Fatalf("bad: %v", reply) + } + } + + // Execute it through the follower with consistency turned on. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: reply, + QueryOptions: structs.QueryOptions{RequireConsistent: true}, + } + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 1 { + t.Fatalf("bad: %v", reply) + } + } + + // Remote execute it through the follower. + { + req := structs.PreparedQueryExecuteRemoteRequest{ + Datacenter: "dc1", + Query: *query.Query, + } + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.ExecuteRemote", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 1 { + t.Fatalf("bad: %v", reply) + } + } + + // Remote execute it through the follower with consistency turned on. + { + req := structs.PreparedQueryExecuteRemoteRequest{ + Datacenter: "dc1", + Query: *query.Query, + QueryOptions: structs.QueryOptions{RequireConsistent: true}, + } + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.ExecuteRemote", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 1 { + t.Fatalf("bad: %v", reply) + } + } +} From 86ead892ab97b344adc07f340b5e7aa936b40ea4 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 17:42:25 -0800 Subject: [PATCH 033/123] Removes unused ACL filter. --- consul/acl.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/consul/acl.go b/consul/acl.go index 479bc3c206..9d2c1d94b0 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -399,10 +399,6 @@ func (s *Server) filterACL(token string, subj interface{}) error { filt.filterNodeServices(v.NodeServices) } - case *structs.CheckServiceNodes: - // TODO (slackpad) - Add a test for this! - filt.filterCheckServiceNodes(v) - case *structs.IndexedCheckServiceNodes: filt.filterCheckServiceNodes(&v.Nodes) From 30a18220afcf3f2e72ec0892bd87e9d737cdf464 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 17:42:41 -0800 Subject: [PATCH 034/123] Adds status information about failovers to query results. --- consul/prepared_query_endpoint.go | 10 ++++++++-- consul/structs/prepared_query.go | 12 +++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index cbd7264ade..2a1599f2ef 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -386,6 +386,9 @@ func (p *PreparedQuery) execute(query *structs.PreparedQuery, reply.Nodes = nodes reply.DNS = query.DNS + // Stamp the result for this datacenter. + reply.Datacenter = p.srv.config.Datacenter + return nil } @@ -533,18 +536,21 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, // the limit since it can be applied remotely to save bandwidth. We also // pass along the consistency mode information we were given, so that // applies to the remote query as well. - for _, dc := range dcs { + for i, dc := range dcs { remote := &structs.PreparedQueryExecuteRemoteRequest{ Datacenter: dc, Query: *query, Limit: args.Limit, QueryOptions: args.QueryOptions, } - if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, remote, reply); err != nil { + if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, &remote, &reply); err != nil { q.GetLogger().Printf("[WARN] consul.prepared_query: Failed querying for service '%s' in datacenter '%s': %s", query.Service.Service, dc, err) continue } + // Keep track of the number of failovers. + reply.Failovers = i + 1 + // We can stop if we found some nodes. if len(reply.Nodes) > 0 { break diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index 03efd078fc..dbd1c04ac2 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -180,6 +180,16 @@ func (q *PreparedQueryExecuteRemoteRequest) RequestDatacenter() string { // PreparedQueryExecuteResponse has the results of executing a query. type PreparedQueryExecuteResponse struct { + // Nodes has the nodes that were output by the query. Nodes CheckServiceNodes - DNS QueryDNSOptions + + // 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 } From 14170535e736c9883df28f6053b8a13caba229d3 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 17:42:52 -0800 Subject: [PATCH 035/123] Adds execute tests for prepared queries. --- consul/prepared_query_endpoint_test.go | 542 ++++++++++++++++++++++++- 1 file changed, 538 insertions(+), 4 deletions(-) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index 881715c46b..59a7dde6da 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -7,10 +7,12 @@ import ( "reflect" "strings" "testing" + "time" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/testutil" "github.com/hashicorp/net-rpc-msgpackrpc" + "github.com/hashicorp/serf/coordinate" ) func TestPreparedQuery_Apply(t *testing.T) { @@ -870,12 +872,9 @@ func TestPreparedQuery_List(t *testing.T) { }, WriteRequest: structs.WriteRequest{Token: "root"}, } - var reply string - - if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &reply); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token); err != nil { t.Fatalf("err: %v", err) } - token = reply } // Set up a node and service in the catalog. @@ -993,6 +992,541 @@ func TestPreparedQuery_List(t *testing.T) { } } +// 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 +// sections so it's still pretty easy to read. +func TestPreparedQuery_Execute(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() + codec1 := rpcClient(t, s1) + defer codec1.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc2" + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + codec2 := rpcClient(t, s2) + defer codec2.Close() + + testutil.WaitForLeader(t, s1.RPC, "dc1") + testutil.WaitForLeader(t, s2.RPC, "dc2") + + // Try to WAN join. + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + testutil.WaitForResult( + func() (bool, error) { + return len(s1.WANMembers()) > 1, nil + }, + func(err error) { + t.Fatalf("Failed waiting for WAN join: %v", err) + }) + + // Create an ACL with read permission to the service. + var token string + { + var rules = ` + service "foo" { + policy = "read" + } + ` + + 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(codec1, "ACL.Apply", &req, &token); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Set up some nodes in each DC that host the service. + { + for i := 0; i < 10; i++ { + for _, dc := range []string{"dc1", "dc2"} { + req := structs.RegisterRequest{ + Datacenter: dc, + Node: fmt.Sprintf("node%d", i+1), + Address: fmt.Sprintf("127.0.0.%d", i+1), + Service: &structs.NodeService{ + Service: "foo", + Port: 8000, + Tags: []string{dc, fmt.Sprintf("tag%d", i+1)}, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + + var codec rpc.ClientCodec + if dc == "dc1" { + codec = codec1 + } else { + codec = codec2 + } + + var reply struct{} + if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + } + } + } + + // Set up a service query. + query := structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "foo", + }, + DNS: structs.QueryDNSOptions{ + TTL: "10s", + }, + }, + WriteRequest: structs.WriteRequest{Token: token}, + } + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Apply", &query, &query.Query.ID); err != nil { + t.Fatalf("err: %v", err) + } + + // Run a query that doesn't exist. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: "nope", + } + + var reply structs.PreparedQueryExecuteResponse + err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply) + if err == nil || !strings.Contains(err.Error(), ErrQueryNotFound.Error()) { + t.Fatalf("bad: %v", err) + } + + if len(reply.Nodes) != 0 { + t.Fatalf("bad: %v", reply) + } + } + + // Run the registered query. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 10 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + } + + // Try with a limit. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + Limit: 3, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + } + + // Push a coordinate for one of the nodes so we can try an RTT sort. We + // have to sleep a little while for the coordinate batch to get flushed. + { + req := structs.CoordinateUpdateRequest{ + Datacenter: "dc1", + Node: "node3", + Coord: coordinate.NewCoordinate(coordinate.DefaultConfig()), + } + var out struct{} + if err := msgpackrpc.CallWithCodec(codec1, "Coordinate.Update", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } + time.Sleep(2 * s1.config.CoordinateUpdatePeriod) + } + + // Try an RTT sort. We don't have any other coordinates in there but + // showing that the node with a coordinate is always first proves we + // call the RTT sorting function, which is tested elsewhere. + for i := 0; i < 100; i++ { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + Source: structs.QuerySource{ + Datacenter: "dc1", + Node: "node3", + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 10 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + if reply.Nodes[0].Node.Node != "node3" { + t.Fatalf("bad: %v", reply) + } + } + + // Make sure the shuffle looks like it's working. + uniques := make(map[string]struct{}) + for i := 0; i < 100; i++ { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 10 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + var names []string + for _, node := range reply.Nodes { + names = append(names, node.Node.Node) + } + key := strings.Join(names, "|") + uniques[key] = struct{}{} + } + + // We have to allow for the fact that there won't always be a unique + // shuffle each pass, so we just look for smell here without the test + // being flaky. + if len(uniques) < 50 { + t.Fatalf("unique shuffle ratio too low: %d/100", len(uniques)) + } + + // Update the health of a node to mark it critical. + setHealth := func(node string, health string) { + req := structs.RegisterRequest{ + Datacenter: "dc1", + Node: node, + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "foo", + Port: 8000, + Tags: []string{"dc1", "tag1"}, + }, + Check: &structs.HealthCheck{ + Name: "failing", + Status: health, + ServiceID: "foo", + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var reply struct{} + if err := msgpackrpc.CallWithCodec(codec1, "Catalog.Register", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + } + setHealth("node1", structs.HealthCritical) + + // The failing node should be filtered. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 9 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + for _, node := range reply.Nodes { + if node.Node.Node == "node1" { + t.Fatalf("bad: %v", node) + } + } + } + + // Upgrade it to a warning and re-query, should be 10 nodes again. + setHealth("node1", structs.HealthWarning) + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 10 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + } + + // Make the query more picky so it excludes warning nodes. + query.Op = structs.PreparedQueryUpdate + query.Query.Service.OnlyPassing = true + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Apply", &query, &query.Query.ID); err != nil { + t.Fatalf("err: %v", err) + } + + // The node in the warning state should be filtered. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 9 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + for _, node := range reply.Nodes { + if node.Node.Node == "node1" { + t.Fatalf("bad: %v", node) + } + } + } + + // Make the query more picky by adding a tag filter. This just proves we + // call into the tag filter, it is tested more thoroughly in a separate + // test. + query.Query.Service.Tags = []string{"!tag3"} + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Apply", &query, &query.Query.ID); err != nil { + t.Fatalf("err: %v", err) + } + + // The node in the warning state should be filtered as well as the node + // with the filtered tag. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 8 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + for _, node := range reply.Nodes { + if node.Node.Node == "node1" || node.Node.Node == "node3" { + t.Fatalf("bad: %v", node) + } + } + } + + // Now fail everything in dc1 and we should get an empty list back. + for i := 0; i < 10; i++ { + setHealth(fmt.Sprintf("node%d", i+1), structs.HealthCritical) + } + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 0 || + reply.Datacenter != "dc1" || reply.Failovers != 0 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + } + + // Modify the query to have it fail over to a bogus DC and then dc2. + query.Query.Service.Failover.Datacenters = []string{"bogus", "dc2"} + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Apply", &query, &query.Query.ID); err != nil { + t.Fatalf("err: %v", err) + } + + // Now we should see 9 nodes from dc2 (we have the tag filter still). + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 9 || + reply.Datacenter != "dc2" || reply.Failovers != 1 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + for _, node := range reply.Nodes { + if node.Node.Node == "node3" { + t.Fatalf("bad: %v", node) + } + } + } + + // Make sure the limit and query options are forwarded. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + Limit: 3, + QueryOptions: structs.QueryOptions{RequireConsistent: true}, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc2" || reply.Failovers != 1 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + for _, node := range reply.Nodes { + if node.Node.Node == "node3" { + t.Fatalf("bad: %v", node) + } + } + } + + // Make sure the remote shuffle looks like it's working. + uniques = make(map[string]struct{}) + for i := 0; i < 100; i++ { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + if err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + if len(reply.Nodes) != 9 || + reply.Datacenter != "dc2" || reply.Failovers != 1 || + !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + t.Fatalf("bad: %v", reply) + } + var names []string + for _, node := range reply.Nodes { + names = append(names, node.Node.Node) + } + key := strings.Join(names, "|") + uniques[key] = struct{}{} + } + + // We have to allow for the fact that there won't always be a unique + // shuffle each pass, so we just look for smell here without the test + // being flaky. + if len(uniques) < 50 { + t.Fatalf("unique shuffle ratio too low: %d/100", len(uniques)) + } + + // Finally, take away the token's ability to read the service. + { + var rules = ` + service "foo" { + policy = "deny" + } + ` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + ID: token, + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + if err := msgpackrpc.CallWithCodec(codec1, "ACL.Apply", &req, &token); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Now the query should be denied. + { + req := structs.PreparedQueryExecuteRequest{ + Datacenter: "dc1", + QueryIDOrName: query.Query.ID, + } + + var reply structs.PreparedQueryExecuteResponse + err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("bad: %v", err) + } + + if len(reply.Nodes) != 0 { + t.Fatalf("bad: %v", reply) + } + } +} + func TestPreparedQuery_Execute_ForwardLeader(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) From eefdb56d1e34622e43c8047904a3c35836a7c299 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 18:23:37 -0800 Subject: [PATCH 036/123] Adds tag filter tests. --- consul/prepared_query_endpoint.go | 11 ++-- consul/prepared_query_endpoint_test.go | 87 ++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 2a1599f2ef..aff1a1b2a1 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -393,7 +393,8 @@ func (p *PreparedQuery) execute(query *structs.PreparedQuery, } // tagFilter returns a list of nodes who satisfy the given tags. Nodes must have -// ALL the given tags, and none of the forbidden tags (prefixed with !). +// ALL the given tags, and NONE of the forbidden tags (prefixed with !). Note +// for performance this modifies the original slice. func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServiceNodes { // Build up lists of required and disallowed tags. must, not := make([]string, 0), make([]string, 0) @@ -413,9 +414,11 @@ func tagFilter(tags []string, nodes structs.CheckServiceNodes) structs.CheckServ // Index the tags so lookups this way are cheaper. index := make(map[string]struct{}) - for _, tag := range node.Service.Tags { - tag = strings.ToLower(tag) - index[tag] = struct{}{} + if node.Service != nil { + for _, tag := range node.Service.Tags { + tag = strings.ToLower(tag) + index[tag] = struct{}{} + } } // Bail if any of the required tags are missing. diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index 59a7dde6da..5017a43b8f 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -5,6 +5,7 @@ import ( "net/rpc" "os" "reflect" + "sort" "strings" "testing" "time" @@ -1657,3 +1658,89 @@ func TestPreparedQuery_Execute_ForwardLeader(t *testing.T) { } } } + +func TestPreparedQuery_tagFilter(t *testing.T) { + testNodes := func() structs.CheckServiceNodes { + return structs.CheckServiceNodes{ + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node1"}, + Service: &structs.NodeService{Tags: []string{"foo"}}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node2"}, + Service: &structs.NodeService{Tags: []string{"foo", "BAR"}}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node3"}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node4"}, + Service: &structs.NodeService{Tags: []string{"foo", "baz"}}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node5"}, + Service: &structs.NodeService{Tags: []string{"foo", "zoo"}}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node6"}, + Service: &structs.NodeService{Tags: []string{"bar"}}, + }, + } + } + + // This always sorts so that it's not annoying to compare after the swap + // operations that the algorithm performs. + stringify := func(nodes structs.CheckServiceNodes) string { + var names []string + for _, node := range nodes { + names = append(names, node.Node.Node) + } + sort.Strings(names) + return strings.Join(names, "|") + } + + ret := stringify(tagFilter([]string{}, testNodes())) + if ret != "node1|node2|node3|node4|node5|node6" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"foo"}, testNodes())) + if ret != "node1|node2|node4|node5" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"!foo"}, testNodes())) + if ret != "node3|node6" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"!foo", "bar"}, testNodes())) + if ret != "node6" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"!foo", "!bar"}, testNodes())) + if ret != "node3" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"nope"}, testNodes())) + if ret != "" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"bar"}, testNodes())) + if ret != "node2|node6" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"BAR"}, testNodes())) + if ret != "node2|node6" { + t.Fatalf("bad: %s", ret) + } + + ret = stringify(tagFilter([]string{"bAr"}, testNodes())) + if ret != "node2|node6" { + t.Fatalf("bad: %s", ret) + } +} From bbc5185000589c1fc9d7d21be7a095943368d030 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 18:30:12 -0800 Subject: [PATCH 037/123] Adds a test for the server wrapper. --- consul/prepared_query_endpoint_test.go | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index 5017a43b8f..e8c379eb03 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1744,3 +1744,59 @@ func TestPreparedQuery_tagFilter(t *testing.T) { t.Fatalf("bad: %s", ret) } } + +func TestPreparedQuery_Wrapper(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() + codec1 := rpcClient(t, s1) + defer codec1.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc2" + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + codec2 := rpcClient(t, s2) + defer codec2.Close() + + testutil.WaitForLeader(t, s1.RPC, "dc1") + testutil.WaitForLeader(t, s2.RPC, "dc2") + + // Try to WAN join. + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + testutil.WaitForResult( + func() (bool, error) { + return len(s1.WANMembers()) > 1, nil + }, + func(err error) { + t.Fatalf("Failed waiting for WAN join: %v", err) + }) + + // Try all the operations on a real server via the wrapper. + wrapper := &queryServerWrapper{s1} + wrapper.GetLogger().Printf("[DEBUG] Test") + + ret, err := wrapper.GetOtherDatacentersByDistance() + if err != nil { + t.Fatalf("err: %v", err) + } + if len(ret) != 1 || ret[0] != "dc2" { + t.Fatalf("bad: %v", ret) + } + + if err := wrapper.ForwardDC("Status.Ping", "dc2", &struct{}{}, &struct{}{}); err != nil { + t.Fatalf("err: %v", err) + } +} From d06e2a535d51d1341923ef88d0575728572c83ed Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 10 Nov 2015 21:16:04 -0800 Subject: [PATCH 038/123] Adds test for remote datacenter selection and query logic. --- consul/prepared_query_endpoint.go | 10 +- consul/prepared_query_endpoint_test.go | 379 +++++++++++++++++++++++++ 2 files changed, 384 insertions(+), 5 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index aff1a1b2a1..b3863f71f2 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -305,7 +305,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, // by the query setup. if len(reply.Nodes) == 0 { wrapper := &queryServerWrapper{p.srv} - if err := queryFailover(wrapper, query, args, reply); err != nil { + if err := queryFailover(wrapper, query, args.Limit, args.QueryOptions, reply); err != nil { return err } } @@ -488,7 +488,7 @@ func (q *queryServerWrapper) ForwardDC(method, dc string, args interface{}, repl // queryFailover runs an algorithm to determine which DCs to try and then calls // them to try to locate alternative services. func queryFailover(q queryServer, query *structs.PreparedQuery, - args *structs.PreparedQueryExecuteRequest, + limit int, options structs.QueryOptions, reply *structs.PreparedQueryExecuteResponse) error { // Pull the list of other DCs. This is sorted by RTT in case the user @@ -543,10 +543,10 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, remote := &structs.PreparedQueryExecuteRemoteRequest{ Datacenter: dc, Query: *query, - Limit: args.Limit, - QueryOptions: args.QueryOptions, + Limit: limit, + QueryOptions: options, } - if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, &remote, &reply); err != nil { + if err := q.ForwardDC("PreparedQuery.ExecuteRemote", dc, remote, reply); err != nil { q.GetLogger().Printf("[WARN] consul.prepared_query: Failed querying for service '%s' in datacenter '%s': %s", query.Service.Service, dc, err) continue } diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index e8c379eb03..c28eaf063a 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1,7 +1,9 @@ package consul import ( + "bytes" "fmt" + "log" "net/rpc" "os" "reflect" @@ -1800,3 +1802,380 @@ func TestPreparedQuery_Wrapper(t *testing.T) { t.Fatalf("err: %v", err) } } + +type mockQueryServer struct { + Datacenters []string + DatacentersError error + QueryLog []string + QueryFn func(dc string, args interface{}, reply interface{}) error + Logger *log.Logger + LogBuffer *bytes.Buffer +} + +func (m *mockQueryServer) JoinQueryLog() string { + return strings.Join(m.QueryLog, "|") +} + +func (m *mockQueryServer) GetLogger() *log.Logger { + if m.Logger == nil { + m.LogBuffer = new(bytes.Buffer) + m.Logger = log.New(m.LogBuffer, "", 0) + } + return m.Logger +} + +func (m *mockQueryServer) GetOtherDatacentersByDistance() ([]string, error) { + return m.Datacenters, m.DatacentersError +} + +func (m *mockQueryServer) ForwardDC(method, dc string, args interface{}, reply interface{}) error { + m.QueryLog = append(m.QueryLog, fmt.Sprintf("%s:%s", dc, method)) + if ret, ok := reply.(*structs.PreparedQueryExecuteResponse); ok { + ret.Datacenter = dc + } + if m.QueryFn != nil { + return m.QueryFn(dc, args, reply) + } else { + return nil + } +} + +func TestPreparedQuery_queryFailover(t *testing.T) { + query := &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Failover: structs.QueryDatacenterOptions{ + NearestN: 0, + Datacenters: []string{""}, + }, + }, + } + + nodes := func() structs.CheckServiceNodes { + return structs.CheckServiceNodes{ + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node1"}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node2"}, + }, + structs.CheckServiceNode{ + Node: &structs.Node{Node: "node3"}, + }, + } + } + + // Datacenters are available but the query doesn't use them. + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 0 || reply.Datacenter != "" || reply.Failovers != 0 { + t.Fatalf("bad: %v", reply) + } + } + + // Make it fail to get datacenters. + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + DatacentersError: fmt.Errorf("XXX"), + } + + var reply structs.PreparedQueryExecuteResponse + err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply) + if err == nil || !strings.Contains(err.Error(), "XXX") { + t.Fatalf("bad: %v", err) + } + if len(reply.Nodes) != 0 || reply.Datacenter != "" || reply.Failovers != 0 { + t.Fatalf("bad: %v", reply) + } + } + + // The query wants to use other datacenters but none are available. + query.Service.Failover.NearestN = 3 + { + mock := &mockQueryServer{ + Datacenters: []string{}, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 0 || reply.Datacenter != "" || reply.Failovers != 0 { + t.Fatalf("bad: %v", reply) + } + } + + // Try the first three nearest datacenters, first one has the data. + query.Service.Failover.NearestN = 3 + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc1" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc1" || reply.Failovers != 1 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Try the first three nearest datacenters, last one has the data. + query.Service.Failover.NearestN = 3 + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc3" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc3" || reply.Failovers != 3 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc3:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Try the first four nearest datacenters, nobody has the data. + query.Service.Failover.NearestN = 4 + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 0 || + reply.Datacenter != "xxx" || reply.Failovers != 4 { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc3:PreparedQuery.ExecuteRemote|xxx:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Try the first two nearest datacenters, plus a user-specified one that + // has the data. + query.Service.Failover.NearestN = 2 + query.Service.Failover.Datacenters = []string{"dc4"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc4" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc4" || reply.Failovers != 3 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc4:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Add in a hard-coded value that overlaps with the nearest list. + query.Service.Failover.NearestN = 2 + query.Service.Failover.Datacenters = []string{"dc4", "dc1"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc4" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc4" || reply.Failovers != 3 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc4:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Now add a bogus user-defined one to the mix. + query.Service.Failover.NearestN = 2 + query.Service.Failover.Datacenters = []string{"nope", "dc4", "dc1"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc4" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc4" || reply.Failovers != 3 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc4:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + if !strings.Contains(mock.LogBuffer.String(), "Skipping unknown datacenter") { + t.Fatalf("bad: %s", mock.LogBuffer.String()) + } + } + + // Same setup as before but dc1 is going to return an error and should + // get skipped over, still yielding data from dc4 which comes later. + query.Service.Failover.NearestN = 2 + query.Service.Failover.Datacenters = []string{"dc4", "dc1"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "dc1" { + return fmt.Errorf("XXX") + } else if dc == "dc4" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "dc4" || reply.Failovers != 3 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc1:PreparedQuery.ExecuteRemote|dc2:PreparedQuery.ExecuteRemote|dc4:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + if !strings.Contains(mock.LogBuffer.String(), "Failed querying") { + t.Fatalf("bad: %s", mock.LogBuffer.String()) + } + } + + // Just use a hard-coded list and now xxx has the data. + query.Service.Failover.NearestN = 0 + query.Service.Failover.Datacenters = []string{"dc3", "xxx"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "xxx" { + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 0, structs.QueryOptions{}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "xxx" || reply.Failovers != 2 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "dc3:PreparedQuery.ExecuteRemote|xxx:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } + + // Make sure the limit and query options are plumbed through. + query.Service.Failover.NearestN = 0 + query.Service.Failover.Datacenters = []string{"xxx"} + { + mock := &mockQueryServer{ + Datacenters: []string{"dc1", "dc2", "dc3", "xxx", "dc4"}, + QueryFn: func(dc string, args interface{}, reply interface{}) error { + inp := args.(*structs.PreparedQueryExecuteRemoteRequest) + ret := reply.(*structs.PreparedQueryExecuteResponse) + if dc == "xxx" { + if inp.Limit != 5 { + t.Fatalf("bad: %d", inp.Limit) + } + if inp.RequireConsistent != true { + t.Fatalf("bad: %v", inp.RequireConsistent) + } + ret.Nodes = nodes() + } + return nil + }, + } + + var reply structs.PreparedQueryExecuteResponse + if err := queryFailover(mock, query, 5, structs.QueryOptions{RequireConsistent: true}, &reply); err != nil { + t.Fatalf("err: %v", err) + } + if len(reply.Nodes) != 3 || + reply.Datacenter != "xxx" || reply.Failovers != 1 || + !reflect.DeepEqual(reply.Nodes, nodes()) { + t.Fatalf("bad: %v", reply) + } + if queries := mock.JoinQueryLog(); queries != "xxx:PreparedQuery.ExecuteRemote" { + t.Fatalf("bad: %s", queries) + } + } +} From a57d642fa03cdcb3b6381476c3e49be47bd58bcf Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 08:48:03 -0800 Subject: [PATCH 039/123] Always increments the failovers counter, even for error-ed DCs. --- consul/prepared_query_endpoint.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index b3863f71f2..c7b820e118 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -535,11 +535,16 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, } } - // Now try the selected DCs in priority order. Note that we pass along - // the limit since it can be applied remotely to save bandwidth. We also - // pass along the consistency mode information we were given, so that - // applies to the remote query as well. - for i, dc := range dcs { + // Now try the selected DCs in priority order. + failovers := 0 + for _, dc := range dcs { + // This keeps track of how many iterations we actually run. + failovers++ + + // Note that we pass along the limit since it can be applied + // remotely to save bandwidth. We also pass along the consistency + // mode information we were given, so that applies to the remote + // query as well. remote := &structs.PreparedQueryExecuteRemoteRequest{ Datacenter: dc, Query: *query, @@ -551,14 +556,15 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, continue } - // Keep track of the number of failovers. - reply.Failovers = i + 1 - // We can stop if we found some nodes. if len(reply.Nodes) > 0 { break } } + // Set this at the end because the response from the remote doesn't have + // this information. + reply.Failovers = failovers + return nil } From 7af41edf52ca06c19d6d0b35f907775af9379a03 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 12:20:40 -0800 Subject: [PATCH 040/123] Changes Lookup to Get since we don't need it (only Execute does). --- consul/prepared_query_endpoint.go | 12 +- consul/prepared_query_endpoint_test.go | 152 +++++++++---------------- consul/structs/prepared_query.go | 5 +- 3 files changed, 63 insertions(+), 106 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index c7b820e118..741feee200 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -181,9 +181,9 @@ 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.IndexedPreparedQueries) error { - if done, err := p.srv.forward("PreparedQuery.Lookup", args, args, reply); done { +// Get returns a single prepared query by ID. +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 } @@ -199,15 +199,15 @@ func (p *PreparedQuery) Lookup(args *structs.PreparedQuerySpecificRequest, reply return p.srv.blockingRPC( &args.QueryOptions, &reply.QueryMeta, - state.GetQueryWatch("PreparedQueryLookup"), + state.GetQueryWatch("PreparedQueryGet"), func() error { - index, query, err := state.PreparedQueryLookup(args.QueryIDOrName) + index, query, err := state.PreparedQueryGet(args.QueryID) if err != nil { return err } if (query != nil) && (query.Token != args.Token) && (acl != nil && !acl.QueryList()) { - 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) + p.srv.logger.Printf("[WARN] consul.prepared_query: Request to get prepared query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.QueryID) return permissionDeniedErr } diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index c28eaf063a..a2629bbec3 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -102,11 +102,11 @@ func TestPreparedQuery_Apply(t *testing.T) { query.Query.ID = reply { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, + Datacenter: "dc1", + QueryID: query.Query.ID, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -133,11 +133,11 @@ func TestPreparedQuery_Apply(t *testing.T) { // Read back again to verify the update worked. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, + Datacenter: "dc1", + QueryID: query.Query.ID, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -179,11 +179,11 @@ func TestPreparedQuery_Apply(t *testing.T) { // Verify that this query is deleted. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, + Datacenter: "dc1", + QueryID: query.Query.ID, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -288,12 +288,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { query.Query.Token = token1 { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -357,12 +357,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { // Make sure the query got deleted. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -385,12 +385,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { query.Query.Token = token1 { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -418,12 +418,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { query.Query.Token = "root" { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -454,12 +454,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { query.Query.Token = token1 { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -486,12 +486,12 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { // Make sure the query got deleted. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -611,7 +611,7 @@ func TestPreparedQuery_parseQuery(t *testing.T) { } } -func TestPreparedQuery_Lookup(t *testing.T) { +func TestPreparedQuery_Get(t *testing.T) { dir1, s1 := testServerWithConfig(t, func(c *Config) { c.ACLDatacenter = "dc1" c.ACLMasterToken = "root" @@ -698,12 +698,12 @@ func TestPreparedQuery_Lookup(t *testing.T) { query.Query.Token = token1 { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: token1}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: token1}, } var resp structs.IndexedPreparedQueries - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -725,12 +725,12 @@ func TestPreparedQuery_Lookup(t *testing.T) { // be denied. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: token2}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: token2}, } var resp structs.IndexedPreparedQueries - err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp) + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp) if err == nil || !strings.Contains(err.Error(), permissionDenied) { t.Fatalf("bad: %v", err) } @@ -743,12 +743,12 @@ func TestPreparedQuery_Lookup(t *testing.T) { // Try again with no token, which should also be denied. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: ""}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: ""}, } var resp structs.IndexedPreparedQueries - err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp) + err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp) if err == nil || !strings.Contains(err.Error(), permissionDenied) { t.Fatalf("bad: %v", err) } @@ -761,12 +761,12 @@ func TestPreparedQuery_Lookup(t *testing.T) { // A management token should be able to read no matter what. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.ID, - QueryOptions: structs.QueryOptions{Token: "root"}, + Datacenter: "dc1", + QueryID: query.Query.ID, + QueryOptions: structs.QueryOptions{Token: "root"}, } var resp structs.IndexedPreparedQueries - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } @@ -783,57 +783,15 @@ func TestPreparedQuery_Lookup(t *testing.T) { } } - // Try a lookup by name instead of ID. + // Try to get an unknown ID. { req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: query.Query.Name, - QueryOptions: structs.QueryOptions{Token: token1}, + Datacenter: "dc1", + QueryID: generateUUID(), + QueryOptions: structs.QueryOptions{Token: token1}, } var resp structs.IndexedPreparedQueries - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { - t.Fatalf("err: %v", err) - } - - if len(resp.Queries) != 1 { - t.Fatalf("bad: %v", resp) - } - actual := resp.Queries[0] - if resp.Index != actual.ModifyIndex { - t.Fatalf("bad index: %d", resp.Index) - } - actual.CreateIndex, actual.ModifyIndex = 0, 0 - if !reflect.DeepEqual(actual, query.Query) { - t.Fatalf("bad: %v", actual) - } - } - - // Try to lookup an unknown ID. - { - req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: generateUUID(), - QueryOptions: structs.QueryOptions{Token: token1}, - } - var resp structs.IndexedPreparedQueries - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { - t.Fatalf("err: %v", err) - } - - if len(resp.Queries) != 0 { - t.Fatalf("bad: %v", resp) - } - } - - // Try to lookup an unknown name. - { - req := &structs.PreparedQuerySpecificRequest{ - Datacenter: "dc1", - QueryIDOrName: "nope", - QueryOptions: structs.QueryOptions{Token: token1}, - } - var resp structs.IndexedPreparedQueries - if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Lookup", req, &resp); err != nil { + if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { t.Fatalf("err: %v", err) } diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index dbd1c04ac2..bb49d1f264 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -115,9 +115,8 @@ 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 + // QueryID is the ID of a query. + QueryID string // QueryOptions (unfortunately named here) controls the consistency // settings for the query lookup itself, as well as the service lookups. From 5d06a87d82a135230db8713ba5a61a0d40e9751c Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 17:27:25 -0800 Subject: [PATCH 041/123] Adds an RPC endpoint injection method for testing. --- consul/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/consul/server.go b/consul/server.go index 83602c4272..eec5802127 100644 --- a/consul/server.go +++ b/consul/server.go @@ -689,6 +689,12 @@ func (s *Server) RPC(method string, args interface{}, reply interface{}) error { return codec.err } +// InjectEndpoint is used to substitute an endpoint for testing. +func (s *Server) InjectEndpoint(endpoint interface{}) error { + s.logger.Printf("[WARN] consul: endpoint injected; this should only be used for testing") + return s.rpcServer.Register(endpoint) +} + // Stats is used to return statistics for debugging and insight // for various sub-systems func (s *Server) Stats() map[string]map[string]string { From 57be55103c7a2c1e0b1b90b527049ca94c1febea Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 17:27:51 -0800 Subject: [PATCH 042/123] 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 } From 8e1bea0192cecae1ca3192dba3b7ab8fb7dc579e Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 21:22:51 -0800 Subject: [PATCH 043/123] Completes FSM support for prepared queries. --- consul/fsm.go | 32 ++++++++++++ consul/fsm_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/consul/fsm.go b/consul/fsm.go index 3be73fa2e0..1d1049e969 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -273,6 +273,7 @@ func (c *consulFSM) applyPreparedQueryOperation(buf []byte, index uint64) interf if err := structs.Decode(buf, &req); err != nil { panic(fmt.Errorf("failed to decode request: %v", err)) } + defer metrics.MeasureSince([]string{"consul", "fsm", "prepared-query", string(req.Op)}, time.Now()) switch req.Op { case structs.PreparedQueryCreate, structs.PreparedQueryUpdate: @@ -392,6 +393,15 @@ func (c *consulFSM) Restore(old io.ReadCloser) error { return err } + case structs.PreparedQueryRequestType: + var req structs.PreparedQuery + if err := dec.Decode(&req); err != nil { + return err + } + if err := restore.PreparedQuery(&req); err != nil { + return err + } + default: return fmt.Errorf("Unrecognized msg type: %v", msgType) } @@ -440,6 +450,12 @@ func (s *consulSnapshot) Persist(sink raft.SnapshotSink) error { sink.Cancel() return err } + + if err := s.persistPreparedQueries(sink, encoder); err != nil { + sink.Cancel() + return err + } + return nil } @@ -586,6 +602,22 @@ func (s *consulSnapshot) persistTombstones(sink raft.SnapshotSink, return nil } +func (s *consulSnapshot) persistPreparedQueries(sink raft.SnapshotSink, + encoder *codec.Encoder) error { + queries, err := s.state.PreparedQueries() + if err != nil { + return err + } + + for query := queries.Next(); query != nil; query = queries.Next() { + sink.Write([]byte{byte(structs.PreparedQueryRequestType)}) + if err := encoder.Encode(query.(*structs.PreparedQuery)); err != nil { + return err + } + } + return nil +} + func (s *consulSnapshot) Release() { s.state.Close() } diff --git a/consul/fsm_test.go b/consul/fsm_test.go index e54ed883a2..a29bceb173 100644 --- a/consul/fsm_test.go +++ b/consul/fsm_test.go @@ -397,6 +397,20 @@ func TestFSM_SnapshotRestore(t *testing.T) { t.Fatalf("err: %s", err) } + query := structs.PreparedQuery{ + ID: generateUUID(), + Service: structs.ServiceQuery{ + Service: "web", + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: 14, + ModifyIndex: 14, + }, + } + if err := fsm.state.PreparedQuerySet(14, &query); err != nil { + t.Fatalf("err: %s", err) + } + // Snapshot snap, err := fsm.Snapshot() if err != nil { @@ -514,6 +528,18 @@ func TestFSM_SnapshotRestore(t *testing.T) { if !reflect.DeepEqual(coords, updates) { t.Fatalf("bad: %#v", coords) } + + // Verify queries are restored. + _, queries, err := fsm2.state.PreparedQueryList() + if err != nil { + t.Fatalf("err: %s", err) + } + if len(queries) != 1 { + t.Fatalf("bad: %#v", queries) + } + if !reflect.DeepEqual(queries[0], &query) { + t.Fatalf("bad: %#v", queries[0]) + } } func TestFSM_KVSSet(t *testing.T) { @@ -1049,6 +1075,103 @@ func TestFSM_ACL_Set_Delete(t *testing.T) { } } +func TestFSM_PreparedQuery_CRUD(t *testing.T) { + fsm, err := NewFSM(nil, os.Stderr) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Register a service to query on. + fsm.state.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"}) + fsm.state.EnsureService(2, "foo", &structs.NodeService{ID: "web", Service: "web", Tags: nil, Address: "127.0.0.1", Port: 80}) + + // Create a new query. + query := structs.PreparedQueryRequest{ + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + ID: generateUUID(), + Service: structs.ServiceQuery{ + Service: "web", + }, + }, + } + { + buf, err := structs.Encode(structs.PreparedQueryRequestType, query) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := fsm.Apply(makeLog(buf)) + if resp != nil { + t.Fatalf("resp: %v", resp) + } + } + + // Verify it's in the state store. + { + _, actual, err := fsm.state.PreparedQueryGet(query.Query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Make an update to the query. + query.Op = structs.PreparedQueryUpdate + query.Query.Name = "my-query" + { + buf, err := structs.Encode(structs.PreparedQueryRequestType, query) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := fsm.Apply(makeLog(buf)) + if resp != nil { + t.Fatalf("resp: %v", resp) + } + } + + // Verify the update. + { + _, actual, err := fsm.state.PreparedQueryGet(query.Query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual.CreateIndex, actual.ModifyIndex = 0, 0 + if !reflect.DeepEqual(actual, query.Query) { + t.Fatalf("bad: %v", actual) + } + } + + // Delete the query. + query.Op = structs.PreparedQueryDelete + { + buf, err := structs.Encode(structs.PreparedQueryRequestType, query) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := fsm.Apply(makeLog(buf)) + if resp != nil { + t.Fatalf("resp: %v", resp) + } + } + + // Make sure it's gone. + { + _, actual, err := fsm.state.PreparedQueryGet(query.Query.ID) + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual != nil { + t.Fatalf("bad: %v", actual) + } + } +} + func TestFSM_TombstoneReap(t *testing.T) { fsm, err := NewFSM(nil, os.Stderr) if err != nil { From 34b685cb4c22d95d794ae27914755ad494968c81 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 21:38:59 -0800 Subject: [PATCH 044/123] Adds a unit test for the new RTT getDatacentersByDistance fn. --- consul/rtt.go | 2 -- consul/rtt_test.go | 84 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/consul/rtt.go b/consul/rtt.go index a824d89d91..11aa32917f 100644 --- a/consul/rtt.go +++ b/consul/rtt.go @@ -380,8 +380,6 @@ func getDatacenterMaps(s serfer, dcs []string) []structs.DatacenterMap { return maps } -// TODO (slackpad) - Add a unit test for this! - // getDatacentersByDistance will return the list of DCs, sorted in order // of increasing distance based on the median distance to that DC from all // servers we know about in the WAN gossip pool. This will sort by name all diff --git a/consul/rtt_test.go b/consul/rtt_test.go index 45652e27af..f12e6c7c47 100644 --- a/consul/rtt_test.go +++ b/consul/rtt_test.go @@ -141,7 +141,7 @@ func seedCoordinates(t *testing.T, codec rpc.ClientCodec, server *Server) { time.Sleep(2 * server.config.CoordinateUpdatePeriod) } -func TestRtt_sortNodesByDistanceFrom(t *testing.T) { +func TestRTT_sortNodesByDistanceFrom(t *testing.T) { dir, server := testServer(t) defer os.RemoveAll(dir) defer server.Shutdown() @@ -202,7 +202,7 @@ func TestRtt_sortNodesByDistanceFrom(t *testing.T) { verifyNodeSort(t, nodes, "node1,node4,node5,node2,node3,apple") } -func TestRtt_sortNodesByDistanceFrom_Nodes(t *testing.T) { +func TestRTT_sortNodesByDistanceFrom_Nodes(t *testing.T) { dir, server := testServer(t) defer os.RemoveAll(dir) defer server.Shutdown() @@ -251,7 +251,7 @@ func TestRtt_sortNodesByDistanceFrom_Nodes(t *testing.T) { verifyNodeSort(t, nodes, "node2,node3,node5,node4,node1,apple") } -func TestRtt_sortNodesByDistanceFrom_ServiceNodes(t *testing.T) { +func TestRTT_sortNodesByDistanceFrom_ServiceNodes(t *testing.T) { dir, server := testServer(t) defer os.RemoveAll(dir) defer server.Shutdown() @@ -300,7 +300,7 @@ func TestRtt_sortNodesByDistanceFrom_ServiceNodes(t *testing.T) { verifyServiceNodeSort(t, nodes, "node2,node3,node5,node4,node1,apple") } -func TestRtt_sortNodesByDistanceFrom_HealthChecks(t *testing.T) { +func TestRTT_sortNodesByDistanceFrom_HealthChecks(t *testing.T) { dir, server := testServer(t) defer os.RemoveAll(dir) defer server.Shutdown() @@ -349,7 +349,7 @@ func TestRtt_sortNodesByDistanceFrom_HealthChecks(t *testing.T) { verifyHealthCheckSort(t, checks, "node2,node3,node5,node4,node1,apple") } -func TestRtt_sortNodesByDistanceFrom_CheckServiceNodes(t *testing.T) { +func TestRTT_sortNodesByDistanceFrom_CheckServiceNodes(t *testing.T) { dir, server := testServer(t) defer os.RemoveAll(dir) defer server.Shutdown() @@ -473,7 +473,7 @@ func (s *mockServer) GetNodesForDatacenter(dc string) []string { return nodes } -func TestRtt_getDatacenterDistance(t *testing.T) { +func TestRTT_getDatacenterDistance(t *testing.T) { s := newMockServer() // The serfer's own DC is always 0 ms away. @@ -508,7 +508,7 @@ func TestRtt_getDatacenterDistance(t *testing.T) { } } -func TestRtt_sortDatacentersByDistance(t *testing.T) { +func TestRTT_sortDatacentersByDistance(t *testing.T) { s := newMockServer() dcs := []string{"acdc", "dc0", "dc1", "dc2", "dcX"} @@ -533,7 +533,7 @@ func TestRtt_sortDatacentersByDistance(t *testing.T) { } } -func TestRtt_getDatacenterMaps(t *testing.T) { +func TestRTT_getDatacenterMaps(t *testing.T) { s := newMockServer() dcs := []string{"dc0", "acdc", "dc1", "dc2", "dcX"} @@ -578,3 +578,71 @@ func TestRtt_getDatacenterMaps(t *testing.T) { t.Fatalf("bad: %v", maps[4]) } } + +func TestRTT_getDatacentersByDistance(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "xxx" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec1 := rpcClient(t, s1) + defer codec1.Close() + + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc1" + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + codec2 := rpcClient(t, s2) + defer codec2.Close() + + dir3, s3 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc2" + }) + defer os.RemoveAll(dir3) + defer s3.Shutdown() + codec3 := rpcClient(t, s3) + defer codec3.Close() + + testutil.WaitForLeader(t, s1.RPC, "xxx") + testutil.WaitForLeader(t, s2.RPC, "dc1") + testutil.WaitForLeader(t, s3.RPC, "dc2") + + // Do the WAN joins. + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + if _, err := s3.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + testutil.WaitForResult( + func() (bool, error) { + return len(s1.WANMembers()) > 2, nil + }, + func(err error) { + t.Fatalf("Failed waiting for WAN join: %v", err) + }) + + // Get the DCs by distance. We don't have coordinate updates yet, but + // having xxx show up first proves we are calling the distance sort, + // since it would normally do a string sort. + dcs, err := s1.getDatacentersByDistance() + if err != nil { + t.Fatalf("err: %s", err) + } + if len(dcs) != 3 || dcs[0] != "xxx" { + t.Fatalf("bad: %v", dcs) + } + + // Let's disable coordinates just to be sure. + s1.config.DisableCoordinates = true + dcs, err = s1.getDatacentersByDistance() + if err != nil { + t.Fatalf("err: %s", err) + } + if len(dcs) != 3 || dcs[0] != "dc1" { + t.Fatalf("bad: %v", dcs) + } +} From c955799baf29f59b01a85af0ea47980ead3fa522 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 22:34:10 -0800 Subject: [PATCH 045/123] Makes an empty prepared query list an empty slice, not a nil one. --- consul/state/prepared_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index b404977e3c..1570d20d6c 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -243,7 +243,7 @@ func (s *StateStore) PreparedQueryList() (uint64, structs.PreparedQueries, error } // Go over all of the queries and build the response. - var result structs.PreparedQueries + result := make(structs.PreparedQueries, 0) for query := queries.Next(); query != nil; query = queries.Next() { result = append(result, query.(*structs.PreparedQuery)) } From 6634cd6567b296b109e08dfb2c10e30f1eeea7f1 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 22:34:46 -0800 Subject: [PATCH 046/123] Adds query metadata to prepared query execute response. --- consul/prepared_query_endpoint.go | 2 ++ consul/prepared_query_endpoint_test.go | 36 +++++++++++++++++--------- consul/structs/prepared_query.go | 3 +++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index a84032142c..18a647691b 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -268,6 +268,7 @@ func (p *PreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, defer metrics.MeasureSince([]string{"consul", "prepared-query", "execute"}, 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 @@ -327,6 +328,7 @@ func (p *PreparedQuery) ExecuteRemote(args *structs.PreparedQueryExecuteRemoteRe defer metrics.MeasureSince([]string{"consul", "prepared-query", "execute_remote"}, 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 diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index a2629bbec3..b526ac8705 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1100,7 +1100,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } } @@ -1120,7 +1121,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 3 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } } @@ -1160,7 +1162,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } if reply.Nodes[0].Node.Node != "node3" { @@ -1183,7 +1186,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } var names []string @@ -1240,7 +1244,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } for _, node := range reply.Nodes { @@ -1265,7 +1270,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } } @@ -1291,7 +1297,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } for _, node := range reply.Nodes { @@ -1324,7 +1331,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 8 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } for _, node := range reply.Nodes { @@ -1351,7 +1359,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 0 || reply.Datacenter != "dc1" || reply.Failovers != 0 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } } @@ -1376,7 +1385,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc2" || reply.Failovers != 1 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } for _, node := range reply.Nodes { @@ -1402,7 +1412,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 3 || reply.Datacenter != "dc2" || reply.Failovers != 1 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } for _, node := range reply.Nodes { @@ -1427,7 +1438,8 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc2" || reply.Failovers != 1 || - !reflect.DeepEqual(reply.DNS, query.Query.DNS) { + !reflect.DeepEqual(reply.DNS, query.Query.DNS) || + !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) } var names []string diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index 9f9e2fb96b..e737323fd4 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -198,4 +198,7 @@ type PreparedQueryExecuteResponse struct { // Failovers is a count of how many times we had to query a remote // datacenter. Failovers int + + // QueryMeta has freshness information about the query. + QueryMeta } From da20e6668be87d3699148faf8c15b2ca484a1ab9 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 11 Nov 2015 22:54:55 -0800 Subject: [PATCH 047/123] Adds a note about obfuscating query name/ID from the logs. --- command/agent/http.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/command/agent/http.go b/command/agent/http.go index 04d0e76192..9828444d8f 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -310,6 +310,15 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque } } + // TODO (slackpad) We may want to consider redacting prepared + // query names/IDs here since they are proxies for tokens. But, + // knowing one only gives you read access to service listings + // which is pretty trivial, so it's probably not worth the code + // complexity and overhead of filtering them out. You can't + // recover the token it's a proxy for with just the query info; + // you'd need the actual token (or a management token) to read + // that back. + // Invoke the handler start := time.Now() defer func() { From 5e7523ea4b01ce62b11182816ede72850dc298f0 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Thu, 12 Nov 2015 09:19:33 -0800 Subject: [PATCH 048/123] Adds a slightly more flexible mock system so we can test DNS. --- command/agent/agent.go | 34 +++++++++++++++++++ command/agent/prepared_query_endpoint.go | 14 ++------ command/agent/prepared_query_endpoint_test.go | 24 ++++++------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 14db1ea3de..a9e3363285 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -9,6 +9,7 @@ import ( "net" "os" "path/filepath" + "reflect" "regexp" "strconv" "sync" @@ -104,6 +105,11 @@ type Agent struct { shutdown bool shutdownCh chan struct{} shutdownLock sync.Mutex + + // endpoints lets you override RPC endpoints for testing. Not all + // agent methods use this, so use with care and never override + // outside of a unit test. + endpoints map[string]string } // Create is used to create a new Agent. Returns @@ -158,6 +164,7 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { eventCh: make(chan serf.UserEvent, 1024), eventBuf: make([]*UserEvent, 256), shutdownCh: make(chan struct{}), + endpoints: make(map[string]string), } // Initialize the local state @@ -1456,3 +1463,30 @@ func (a *Agent) DisableNodeMaintenance() { a.RemoveCheck(nodeMaintCheckID, true) a.logger.Printf("[INFO] agent: Node left maintenance mode") } + +// InjectEndpoint overrides the given endpoint with a substitute one. Note +// that not all agent methods use this mechanism, and that is should only +// be used for testing. +func (a *Agent) InjectEndpoint(endpoint string, handler interface{}) error { + if a.server == nil { + return fmt.Errorf("agent must be a server") + } + + if err := a.server.InjectEndpoint(handler); err != nil { + return err + } + name := reflect.Indirect(reflect.ValueOf(handler)).Type().Name() + a.endpoints[endpoint] = name + + a.logger.Printf("[WARN] agent: endpoint injected; this should only be used for testing") + return nil +} + +// getEndpoint returns the endpoint name to use for the given endpoint, +// which may be overridden. +func (a *Agent) getEndpoint(endpoint string) string { + if override, ok := a.endpoints[endpoint]; ok { + return override + } + return endpoint +} diff --git a/command/agent/prepared_query_endpoint.go b/command/agent/prepared_query_endpoint.go index a27978c89a..5d9c07212d 100644 --- a/command/agent/prepared_query_endpoint.go +++ b/command/agent/prepared_query_endpoint.go @@ -21,12 +21,7 @@ type preparedQueryCreateResponse struct { // 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) { + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) switch req.Method { case "POST": // Create a new prepared query. args := structs.PreparedQueryRequest{ @@ -82,12 +77,6 @@ func parseLimit(req *http.Request, limit *int) error { // 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) { @@ -95,6 +84,7 @@ func (s *HTTPServer) preparedQuerySpecific(endpoint string, resp http.ResponseWr id = strings.TrimSuffix(id, preparedQueryExecuteSuffix) } + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) switch req.Method { case "GET": // Execute or retrieve a prepared query. if execute { diff --git a/command/agent/prepared_query_endpoint_test.go b/command/agent/prepared_query_endpoint_test.go index 01384d0284..23a39b196c 100644 --- a/command/agent/prepared_query_endpoint_test.go +++ b/command/agent/prepared_query_endpoint_test.go @@ -62,7 +62,7 @@ func (m *MockPreparedQuery) Execute(args *structs.PreparedQueryExecuteRequest, func TestPreparedQuery_Create(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -126,7 +126,7 @@ func TestPreparedQuery_Create(t *testing.T) { } resp := httptest.NewRecorder() - obj, err := srv.preparedQueryGeneral("MockPreparedQuery", resp, req) + obj, err := srv.PreparedQueryGeneral(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -146,7 +146,7 @@ func TestPreparedQuery_Create(t *testing.T) { func TestPreparedQuery_List(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -176,7 +176,7 @@ func TestPreparedQuery_List(t *testing.T) { } resp := httptest.NewRecorder() - obj, err := srv.preparedQueryGeneral("MockPreparedQuery", resp, req) + obj, err := srv.PreparedQueryGeneral(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -196,7 +196,7 @@ func TestPreparedQuery_List(t *testing.T) { func TestPreparedQuery_Execute(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -230,7 +230,7 @@ func TestPreparedQuery_Execute(t *testing.T) { } resp := httptest.NewRecorder() - obj, err := srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + obj, err := srv.PreparedQuerySpecific(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -250,7 +250,7 @@ func TestPreparedQuery_Execute(t *testing.T) { func TestPreparedQuery_Get(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -281,7 +281,7 @@ func TestPreparedQuery_Get(t *testing.T) { } resp := httptest.NewRecorder() - obj, err := srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + obj, err := srv.PreparedQuerySpecific(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -301,7 +301,7 @@ func TestPreparedQuery_Get(t *testing.T) { func TestPreparedQuery_Update(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -367,7 +367,7 @@ func TestPreparedQuery_Update(t *testing.T) { } resp := httptest.NewRecorder() - _, err = srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + _, err = srv.PreparedQuerySpecific(resp, req) if err != nil { t.Fatalf("err: %v", err) } @@ -380,7 +380,7 @@ func TestPreparedQuery_Update(t *testing.T) { func TestPreparedQuery_Delete(t *testing.T) { httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} - if err := srv.agent.server.InjectEndpoint(&m); err != nil { + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { t.Fatalf("err: %v", err) } @@ -418,7 +418,7 @@ func TestPreparedQuery_Delete(t *testing.T) { } resp := httptest.NewRecorder() - _, err = srv.preparedQuerySpecific("MockPreparedQuery", resp, req) + _, err = srv.PreparedQuerySpecific(resp, req) if err != nil { t.Fatalf("err: %v", err) } From 4a0a60af55a851734aa453675f01f2e47b2d7880 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Thu, 12 Nov 2015 09:28:05 -0800 Subject: [PATCH 049/123] Adds DNS support for prepared queries (needs tests). --- command/agent/dns.go | 88 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index d2152f8161..4f7ef33f2d 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -289,7 +289,7 @@ func (d *DNSServer) dispatch(network string, req, resp *dns.Msg) { // Split into the label parts labels := dns.SplitDomainName(qName) - // The last label is either "node", "service" or a datacenter name + // The last label is either "node", "service", "query", or a datacenter name PARSE: n := len(labels) if n == 0 { @@ -336,6 +336,14 @@ PARSE: node := strings.Join(labels[:n-1], ".") d.nodeLookup(network, datacenter, node, req, resp) + case "query": + if len(labels) == 1 { + goto INVALID + } + // Allow a "." in the query name, just join all the parts. + query := strings.Join(labels[:n-1], ".") + d.preparedQueryLookup(network, datacenter, query, req, resp) + default: // Store the DC, and re-parse datacenter = labels[n-1] @@ -535,6 +543,84 @@ RPC: } } +// preparedQueryLookup is used to handle a prepared query. +func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, resp *dns.Msg) { + // Execute the prepared query. + args := structs.PreparedQueryExecuteRequest{ + Datacenter: datacenter, + QueryIDOrName: query, + QueryOptions: structs.QueryOptions{ + Token: d.agent.config.ACLToken, + AllowStale: d.config.AllowStale, + }, + } + + // If the network is not TCP then we just get enough responses to + // tell that things got truncated. This saves bandwidth since we + // will trim the list anyway. + if network != "tcp" { + args.Limit = maxServiceResponses + 1 + } + + endpoint := d.agent.getEndpoint(preparedQueryEndpoint) + var out structs.PreparedQueryExecuteResponse +RPC: + if err := d.agent.RPC(endpoint+".Execute", &args, &out); err != nil { + d.logger.Printf("[ERR] dns: rpc error: %v", err) + resp.SetRcode(req, dns.RcodeServerFailure) + return + } + + // Verify that request is not too stale, redo the request. + if args.AllowStale && out.LastContact > d.config.MaxStale { + args.AllowStale = false + d.logger.Printf("[WARN] dns: Query results too stale, re-requesting") + goto RPC + } + + // Determine the TTL. The parse should never fail since we vet it when + // the query is created, but we check anyway. + var ttl time.Duration + if out.DNS.TTL != "" { + var err error + ttl, err = time.ParseDuration(out.DNS.TTL) + if err != nil { + d.logger.Printf("[WARN] dns: Failed to parse TTL '%s' for prepared query '%s', ignoring", out.DNS.TTL, query) + } + } + + // If we have no nodes, return not found! + if len(out.Nodes) == 0 { + d.addSOA(d.domain, resp) + resp.SetRcode(req, dns.RcodeNameError) + return + } + + // Add various responses depending on the request. + qType := req.Question[0].Qtype + d.serviceNodeRecords(out.Nodes, req, resp, ttl) + if qType == dns.TypeSRV { + d.serviceSRVRecords(datacenter, out.Nodes, req, resp, ttl) + } + + // If the network is not TCP, restrict the number of responses. + if network != "tcp" && len(resp.Answer) > maxServiceResponses { + resp.Answer = resp.Answer[:maxServiceResponses] + + // Flag that there are more records to return in the UDP + // response. + if d.config.EnableTruncate { + resp.Truncated = true + } + } + + // If the answer is empty, return not found. + if len(resp.Answer) == 0 { + d.addSOA(d.domain, resp) + return + } +} + // serviceNodeRecords is used to add the node records for a service lookup func (d *DNSServer) serviceNodeRecords(nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration) { qName := req.Question[0].Name From 81b43135f9655bf118b949c467440a2b96309a71 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 03:39:07 -0800 Subject: [PATCH 050/123] Adds unit tests for prepared queries and DNS, using existing tests for equivalence. --- command/agent/dns.go | 26 +- command/agent/dns_test.go | 2082 +++++++++++++++--------- consul/prepared_query_endpoint_test.go | 2 +- 3 files changed, 1350 insertions(+), 760 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index 4f7ef33f2d..a5816b3d65 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul/structs" "github.com/miekg/dns" ) @@ -555,17 +556,25 @@ func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, }, } - // If the network is not TCP then we just get enough responses to - // tell that things got truncated. This saves bandwidth since we - // will trim the list anyway. - if network != "tcp" { - args.Limit = maxServiceResponses + 1 - } + // TODO (slackpad) - What's a safe limit we can set here? It seems like + // with dup filtering done at this level we need to get everything to + // match the previous behavior. We can optimize by pushing more filtering + // into the query execution, but for now I think we need to get the full + // response. endpoint := d.agent.getEndpoint(preparedQueryEndpoint) var out structs.PreparedQueryExecuteResponse RPC: if err := d.agent.RPC(endpoint+".Execute", &args, &out); err != nil { + // If they give a bogus query name, treat that as a name error, + // not a full on server error. We have to use a string compare + // here since the RPC layer loses the type information. + if err.Error() == consul.ErrQueryNotFound.Error() { + d.addSOA(d.domain, resp) + resp.SetRcode(req, dns.RcodeNameError) + return + } + d.logger.Printf("[ERR] dns: rpc error: %v", err) resp.SetRcode(req, dns.RcodeServerFailure) return @@ -578,6 +587,11 @@ RPC: goto RPC } + // TODO (slackpad) Do we want to apply the DNS server's per-service TTL + // configs if the query's TTL is not set? That seems like it adds a lot + // of complexity (we'd have to plumb the service name back with the query + // results to do this), but is it what people would expect? + // Determine the TTL. The parse should never fail since we vet it when // the query is created, but we check anyway. var ttl time.Duration diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index f71703ebc1..41a2356a85 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -506,86 +506,118 @@ func TestDNS_ServiceLookup(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, + // Register a node with a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeSRV) + // Look up the service directly and via prepared query. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok := in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Port != 12345 { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Target != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.A.String() != "127.0.0.1" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } } - if len(in.Answer) != 1 { - t.Fatalf("Bad: %#v", in) + // Lookup a non-existing service/query, we should receive an SOA. + questions = []string{ + "nodb.service.consul.", + "nope.query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) - srvRec, ok := in.Answer[0].(*dns.SRV) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[0]) - } - if srvRec.Port != 12345 { - t.Fatalf("Bad: %#v", srvRec) - } - if srvRec.Target != "foo.node.dc1.consul." { - t.Fatalf("Bad: %#v", srvRec) - } - if srvRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Answer[0]) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - aRec, ok := in.Extra[0].(*dns.A) - if !ok { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.Hdr.Name != "foo.node.dc1.consul." { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.A.String() != "127.0.0.1" { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Extra[0]) - } + if len(in.Ns) != 1 { + t.Fatalf("Bad: %#v", in) + } - // lookup a non-existing service, we should receive a SOA - m = new(dns.Msg) - m.SetQuestion("nodb.service.consul.", dns.TypeSRV) - - c = new(dns.Client) - addr, _ = srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err = c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(in.Ns) != 1 { - t.Fatalf("Bad: %#v", in) - } - - soaRec, ok := in.Ns[0].(*dns.SOA) - if !ok { - t.Fatalf("Bad: %#v", in.Ns[0]) - } - if soaRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Ns[0]) + soaRec, ok := in.Ns[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + if soaRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Ns[0]) + } } } @@ -596,64 +628,90 @@ func TestDNS_ServiceLookup_ServiceAddress(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Address: "127.0.0.2", - Port: 12345, - }, + // Register a node with a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Address: "127.0.0.2", + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeSRV) + // Look up the service directly and via prepared query. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - if len(in.Answer) != 1 { - t.Fatalf("Bad: %#v", in) - } + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } - srvRec, ok := in.Answer[0].(*dns.SRV) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[0]) - } - if srvRec.Port != 12345 { - t.Fatalf("Bad: %#v", srvRec) - } - if srvRec.Target != "foo.node.dc1.consul." { - t.Fatalf("Bad: %#v", srvRec) - } - if srvRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Answer[0]) - } + srvRec, ok := in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Port != 12345 { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Target != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } - aRec, ok := in.Extra[0].(*dns.A) - if !ok { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.Hdr.Name != "foo.node.dc1.consul." { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.A.String() != "127.0.0.2" { - t.Fatalf("Bad: %#v", in.Extra[0]) - } - if aRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Extra[0]) + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.A.String() != "127.0.0.2" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } } } @@ -664,35 +722,69 @@ func TestDNS_CaseInsensitiveServiceLookup(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "Db", - Tags: []string{"Master"}, - Port: 12345, - }, + // Register a node with a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "Db", + Tags: []string{"Master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query, as well as a name. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "somequery", + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - m := new(dns.Msg) - m.SetQuestion("mASTER.dB.service.consul.", dns.TypeSRV) - - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) + // Try some variations to make sure case doesn't matter. + questions := []string{ + "master.db.service.consul.", + "mASTER.dB.service.consul.", + "MASTER.dB.service.consul.", + "db.service.consul.", + "DB.service.consul.", + "Db.service.consul.", + "somequery.query.consul.", + "SomeQuery.query.consul.", + "SOMEQUERY.query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) - if len(in.Answer) != 1 { - t.Fatalf("empty lookup: %#v", in) + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("empty lookup: %#v", in) + } } } @@ -757,62 +849,52 @@ func TestDNS_ServiceLookup_TagPeriod(t *testing.T) { } } -func TestDNS_ServiceLookup_Dedup(t *testing.T) { +func TestDNS_ServiceLookup_PreparedQueryNamePeriod(t *testing.T) { dir, srv := makeDNSServer(t) defer os.RemoveAll(dir) defer srv.agent.Shutdown() testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, + // Register a node with a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) - } + // Register a prepared query with a period in the name. + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "some.query.we.like", + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } - args = &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db2", - Service: "db", - Tags: []string{"slave"}, - Port: 12345, - }, - } - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) - } - - args = &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db3", - Service: "db", - Tags: []string{"slave"}, - Port: 12346, - }, - } - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + var id string + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeANY) + m.SetQuestion("some.query.we.like.query.consul.", dns.TypeSRV) c := new(dns.Client) addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) @@ -825,108 +907,17 @@ func TestDNS_ServiceLookup_Dedup(t *testing.T) { t.Fatalf("Bad: %#v", in) } - aRec, ok := in.Answer[0].(*dns.A) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[0]) - } - if aRec.A.String() != "127.0.0.1" { - t.Fatalf("Bad: %#v", in.Answer[0]) - } -} - -func TestDNS_ServiceLookup_Dedup_SRV(t *testing.T) { - dir, srv := makeDNSServer(t) - defer os.RemoveAll(dir) - defer srv.agent.Shutdown() - - testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - } - - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) - } - - args = &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db2", - Service: "db", - Tags: []string{"slave"}, - Port: 12345, - }, - } - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) - } - - args = &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - ID: "db3", - Service: "db", - Tags: []string{"slave"}, - Port: 12346, - }, - } - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) - } - - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeSRV) - - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(in.Answer) != 2 { - t.Fatalf("Bad: %#v", in) - } - srvRec, ok := in.Answer[0].(*dns.SRV) if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } - if srvRec.Port != 12345 && srvRec.Port != 12346 { + if srvRec.Port != 12345 { t.Fatalf("Bad: %#v", srvRec) } if srvRec.Target != "foo.node.dc1.consul." { t.Fatalf("Bad: %#v", srvRec) } - srvRec, ok = in.Answer[1].(*dns.SRV) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[1]) - } - if srvRec.Port != 12346 && srvRec.Port != 12345 { - t.Fatalf("Bad: %#v", srvRec) - } - if srvRec.Port == in.Answer[0].(*dns.SRV).Port { - t.Fatalf("should be a different port") - } - if srvRec.Target != "foo.node.dc1.consul." { - t.Fatalf("Bad: %#v", srvRec) - } - aRec, ok := in.Extra[0].(*dns.A) if !ok { t.Fatalf("Bad: %#v", in.Extra[0]) @@ -939,6 +930,242 @@ func TestDNS_ServiceLookup_Dedup_SRV(t *testing.T) { } } +func TestDNS_ServiceLookup_Dedup(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register a single node with multiple instances of a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db2", + Service: "db", + Tags: []string{"slave"}, + Port: 12345, + }, + } + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db3", + Service: "db", + Tags: []string{"slave"}, + Port: 12346, + }, + } + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Look up the service directly and via prepared query, make sure only + // one IP is returned. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + aRec, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if aRec.A.String() != "127.0.0.1" { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + } +} + +func TestDNS_ServiceLookup_Dedup_SRV(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register a single node with multiple instances of a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db2", + Service: "db", + Tags: []string{"slave"}, + Port: 12345, + }, + } + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + ID: "db3", + Service: "db", + Tags: []string{"slave"}, + Port: 12346, + }, + } + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Look up the service directly and via prepared query, make sure only + // one IP is returned and two unique ports are returned. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 2 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok := in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Port != 12345 && srvRec.Port != 12346 { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Target != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", srvRec) + } + + srvRec, ok = in.Answer[1].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[1]) + } + if srvRec.Port != 12346 && srvRec.Port != 12345 { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Port == in.Answer[0].(*dns.SRV).Port { + t.Fatalf("should be a different port") + } + if srvRec.Target != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", srvRec) + } + + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != "foo.node.dc1.consul." { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.A.String() != "127.0.0.1" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + } +} + func TestDNS_Recurse(t *testing.T) { recursor := makeRecursor(t, []dns.RR{dnsA("apple.com", "1.2.3.4")}) defer recursor.Shutdown() @@ -974,127 +1201,153 @@ func TestDNS_ServiceLookup_FilterCritical(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "serf", - Name: "serf", - Status: structs.HealthCritical, - }, + // Register nodes with health checks in various states. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "serf", + Name: "serf", + Status: structs.HealthCritical, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args2 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "serf", + Name: "serf", + Status: structs.HealthCritical, + }, + } + if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args3 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthCritical, + }, + } + if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args4 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "baz", + Address: "127.0.0.3", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + if err := srv.agent.RPC("Catalog.Register", args4, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args5 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "quux", + Address: "127.0.0.4", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthWarning, + }, + } + if err := srv.agent.RPC("Catalog.Register", args5, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - args2 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "serf", - Name: "serf", - Status: structs.HealthCritical, - }, - } - if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { - t.Fatalf("err: %v", err) + // Look up the service directly and via prepared query. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) - args3 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthCritical, - }, - } - if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { - t.Fatalf("err: %v", err) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - args4 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "baz", - Address: "127.0.0.3", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - } - if err := srv.agent.RPC("Catalog.Register", args4, &out); err != nil { - t.Fatalf("err: %v", err) - } + // Only 4 and 5 are not failing, so we should get 2 answers + if len(in.Answer) != 2 { + t.Fatalf("Bad: %#v", in) + } - args5 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "quux", - Address: "127.0.0.4", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthWarning, - }, - } - if err := srv.agent.RPC("Catalog.Register", args5, &out); err != nil { - t.Fatalf("err: %v", err) - } + ips := make(map[string]bool) + for _, resp := range in.Answer { + aRec := resp.(*dns.A) + ips[aRec.A.String()] = true + } - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeANY) - - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Only 4 and 5 are not failing, so we should get 2 answers - if len(in.Answer) != 2 { - t.Fatalf("Bad: %#v", in) - } - - ips := make(map[string]bool) - for _, resp := range in.Answer { - aRec := resp.(*dns.A) - ips[aRec.A.String()] = true - } - - if !ips["127.0.0.3"] { - t.Fatalf("Bad: %#v should contain 127.0.0.3 (state healthy)", in) - } - if !ips["127.0.0.4"] { - t.Fatalf("Bad: %#v should contain 127.0.0.4 (state warning)", in) + if !ips["127.0.0.3"] { + t.Fatalf("Bad: %#v should contain 127.0.0.3 (state healthy)", in) + } + if !ips["127.0.0.4"] { + t.Fatalf("Bad: %#v should contain 127.0.0.4 (state warning)", in) + } } } @@ -1105,84 +1358,110 @@ func TestDNS_ServiceLookup_OnlyFailing(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "serf", - Name: "serf", - Status: structs.HealthCritical, - }, + // Register nodes with all health checks in a critical state. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "serf", + Name: "serf", + Status: structs.HealthCritical, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args2 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "serf", + Name: "serf", + Status: structs.HealthCritical, + }, + } + if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args3 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthCritical, + }, + } + if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - args2 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "serf", - Name: "serf", - Status: structs.HealthCritical, - }, - } - if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { - t.Fatalf("err: %v", err) + // Look up the service directly and via prepared query. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) - args3 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthCritical, - }, - } - if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { - t.Fatalf("err: %v", err) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeANY) + // All 3 are failing, so we should get 0 answers and an NXDOMAIN response + if len(in.Answer) != 0 { + t.Fatalf("Bad: %#v", in) + } - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - // All 3 are failing, so we should get 0 answers and an NXDOMAIN response - if len(in.Answer) != 0 { - t.Fatalf("Bad: %#v", in) - } - - if in.Rcode != dns.RcodeNameError { - t.Fatalf("Bad: %#v", in) + if in.Rcode != dns.RcodeNameError { + t.Fatalf("Bad: %#v", in) + } } } @@ -1195,112 +1474,139 @@ func TestDNS_ServiceLookup_OnlyPassing(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthPassing, - }, + // Register nodes with health checks in various states. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthPassing, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args2 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "127.0.0.2", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthWarning, + }, + } + + if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args3 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "baz", + Address: "127.0.0.3", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthCritical, + }, + } + + if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args4 := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "quux", + Address: "127.0.0.4", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + Check: &structs.HealthCheck{ + CheckID: "db", + Name: "db", + ServiceID: "db", + Status: structs.HealthUnknown, + }, + } + + if err := srv.agent.RPC("Catalog.Register", args4, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "db", + OnlyPassing: true, + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - args2 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "bar", - Address: "127.0.0.2", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthWarning, - }, + // Look up the service directly and via prepared query. + questions := []string{ + "db.service.consul.", + id + ".query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) - if err := srv.agent.RPC("Catalog.Register", args2, &out); err != nil { - t.Fatalf("err: %v", err) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - args3 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "baz", - Address: "127.0.0.3", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthCritical, - }, - } + // Only 1 is passing, so we should only get 1 answer + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } - if err := srv.agent.RPC("Catalog.Register", args3, &out); err != nil { - t.Fatalf("err: %v", err) - } + resp := in.Answer[0] + aRec := resp.(*dns.A) - args4 := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "quux", - Address: "127.0.0.4", - Service: &structs.NodeService{ - Service: "db", - Tags: []string{"master"}, - Port: 12345, - }, - Check: &structs.HealthCheck{ - CheckID: "db", - Name: "db", - ServiceID: "db", - Status: structs.HealthUnknown, - }, - } - - if err := srv.agent.RPC("Catalog.Register", args4, &out); err != nil { - t.Fatalf("err: %v", err) - } - - m := new(dns.Msg) - m.SetQuestion("db.service.consul.", dns.TypeANY) - - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Only 1 is passing, so we should only get 1 answer - if len(in.Answer) != 1 { - t.Fatalf("Bad: %#v", in) - } - - resp := in.Answer[0] - aRec := resp.(*dns.A) - - if aRec.A.String() != "127.0.0.1" { - t.Fatalf("Bad: %#v", in.Answer[0]) + if aRec.A.String() != "127.0.0.1" { + t.Fatalf("Bad: %#v", in.Answer[0]) + } } } @@ -1311,7 +1617,7 @@ func TestDNS_ServiceLookup_Randomize(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes + // Register a large set of nodes. for i := 0; i < 3*maxServiceResponses; i++ { args := &structs.RegisterRequest{ Datacenter: "dc1", @@ -1329,46 +1635,70 @@ func TestDNS_ServiceLookup_Randomize(t *testing.T) { } } - // Ensure the response is randomized each time. - uniques := map[string]struct{}{} - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - for i := 0; i < 10; i++ { - m := new(dns.Msg) - m.SetQuestion("web.service.consul.", dns.TypeANY) - - c := new(dns.Client) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "web", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } - - // Response length should be truncated - // We should get an A record for each response - if len(in.Answer) != maxServiceResponses { - t.Fatalf("Bad: %#v", len(in.Answer)) - } - - // Collect all the names - var names []string - for _, rec := range in.Answer { - switch v := rec.(type) { - case *dns.SRV: - names = append(names, v.Target) - case *dns.A: - names = append(names, v.A.String()) - } - } - nameS := strings.Join(names, "|") - - // Tally the results - uniques[nameS] = struct{}{} } - // Give some wiggle room. Since the responses are randomized and there - // is a finite number of combinations, requiring 0 duplicates every - // test run eventually gives us failures. - if len(uniques) < 2 { - t.Fatalf("unique response ratio too low: %d/10\n%v", len(uniques), uniques) + // Look up the service directly and via prepared query. Ensure the + // response is randomized each time. + questions := []string{ + "web.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + uniques := map[string]struct{}{} + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + for i := 0; i < 10; i++ { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) + + c := new(dns.Client) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Response length should be truncated and we should get + // an A record for each response. + if len(in.Answer) != maxServiceResponses { + t.Fatalf("Bad: %#v", len(in.Answer)) + } + + // Collect all the names. + var names []string + for _, rec := range in.Answer { + switch v := rec.(type) { + case *dns.SRV: + names = append(names, v.Target) + case *dns.A: + names = append(names, v.A.String()) + } + } + nameS := strings.Join(names, "|") + + // Tally the results. + uniques[nameS] = struct{}{} + } + + // Give some wiggle room. Since the responses are randomized and + // there is a finite number of combinations, requiring 0 + // duplicates every test run eventually gives us failures. + if len(uniques) < 2 { + t.Fatalf("unique response ratio too low: %d/10\n%v", len(uniques), uniques) + } } } @@ -1381,7 +1711,7 @@ func TestDNS_ServiceLookup_Truncate(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes + // Register nodes a large number of nodes. for i := 0; i < 3*maxServiceResponses; i++ { args := &structs.RegisterRequest{ Datacenter: "dc1", @@ -1399,20 +1729,44 @@ func TestDNS_ServiceLookup_Truncate(t *testing.T) { } } - // Ensure the response is randomized each time. - m := new(dns.Msg) - m.SetQuestion("web.service.consul.", dns.TypeANY) - - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - c := new(dns.Client) - in, _, err := c.Exchange(m, addr.String()) - if err != nil && err != dns.ErrTruncated { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "web", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - // Check for the truncate bit - if !in.Truncated { - t.Fatalf("should have truncate bit") + // Look up the service directly and via prepared query. Ensure the + // response is truncated each time. + questions := []string{ + "web.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) + + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + c := new(dns.Client) + in, _, err := c.Exchange(m, addr.String()) + if err != nil && err != dns.ErrTruncated { + t.Fatalf("err: %v", err) + } + + // Check for the truncate bit + if !in.Truncated { + t.Fatalf("should have truncate bit") + } } } @@ -1423,7 +1777,7 @@ func TestDNS_ServiceLookup_MaxResponses(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register nodes + // Register a large number of nodes. for i := 0; i < 6*maxServiceResponses; i++ { nodeAddress := fmt.Sprintf("127.0.0.%d", i+1) if i > 3 { @@ -1445,41 +1799,63 @@ func TestDNS_ServiceLookup_MaxResponses(t *testing.T) { } } - // Ensure the response is randomized each time. - m := new(dns.Msg) - m.SetQuestion("web.service.consul.", dns.TypeANY) - - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - c := new(dns.Client) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "web", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - if len(in.Answer) != 3 { - t.Fatalf("should receive 3 answers for ANY") + // Look up the service directly and via prepared query. + questions := []string{ + "web.service.consul.", + id + ".query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) - m.SetQuestion("web.service.consul.", dns.TypeA) - in, _, err = c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + c := new(dns.Client) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 3 { + t.Fatalf("should receive 3 answers for ANY") + } + + m.SetQuestion(question, dns.TypeA) + in, _, err = c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 3 { + t.Fatalf("should receive 3 answers for A") + } + + m.SetQuestion(question, dns.TypeAAAA) + in, _, err = c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 3 { + t.Fatalf("should receive 3 answers for AAAA") + } } - - if len(in.Answer) != 3 { - t.Fatalf("should receive 3 answers for A") - } - - m.SetQuestion("web.service.consul.", dns.TypeAAAA) - in, _, err = c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(in.Answer) != 3 { - t.Fatalf("should receive 3 answers for AAAA") - } - } func TestDNS_ServiceLookup_CNAME(t *testing.T) { @@ -1497,58 +1873,84 @@ func TestDNS_ServiceLookup_CNAME(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // Register node - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "google", - Address: "www.google.com", - Service: &structs.NodeService{ - Service: "search", - Port: 80, - }, + // Register a node with a name for an address. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "google", + Address: "www.google.com", + Service: &structs.NodeService{ + Service: "search", + Port: 80, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register an equivalent prepared query. + var id string + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Service: structs.ServiceQuery{ + Service: "search", + }, + }, + } + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - m := new(dns.Msg) - m.SetQuestion("search.service.consul.", dns.TypeANY) + // Look up the service directly and via prepared query. + questions := []string{ + "search.service.consul.", + id + ".query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeANY) - c := new(dns.Client) - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - // Service CNAME, google CNAME, google A record - if len(in.Answer) != 3 { - t.Fatalf("Bad: %#v", in) - } + // Service CNAME, google CNAME, google A record + if len(in.Answer) != 3 { + t.Fatalf("Bad: %#v", in) + } - // Should have service CNAME - cnRec, ok := in.Answer[0].(*dns.CNAME) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[0]) - } - if cnRec.Target != "www.google.com." { - t.Fatalf("Bad: %#v", in.Answer[0]) - } + // Should have service CNAME + cnRec, ok := in.Answer[0].(*dns.CNAME) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if cnRec.Target != "www.google.com." { + t.Fatalf("Bad: %#v", in.Answer[0]) + } - // Should have google CNAME - cnRec, ok = in.Answer[1].(*dns.CNAME) - if !ok { - t.Fatalf("Bad: %#v", in.Answer[1]) - } - if cnRec.Target != "google.com." { - t.Fatalf("Bad: %#v", in.Answer[1]) - } + // Should have google CNAME + cnRec, ok = in.Answer[1].(*dns.CNAME) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[1]) + } + if cnRec.Target != "google.com." { + t.Fatalf("Bad: %#v", in.Answer[1]) + } - // Check we recursively resolve - if _, ok := in.Answer[2].(*dns.A); !ok { - t.Fatalf("Bad: %#v", in.Answer[2]) + // Check we recursively resolve + if _, ok := in.Answer[2].(*dns.A); !ok { + t.Fatalf("Bad: %#v", in.Answer[2]) + } } } @@ -1781,6 +2183,129 @@ func TestDNS_ServiceLookup_TTL(t *testing.T) { } } +func TestDNS_PreparedQuery_TTL(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register a node and a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Register prepared queries with and without a TTL set. + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "ttl", + Service: structs.ServiceQuery{ + Service: "db", + }, + DNS: structs.QueryDNSOptions{ + TTL: "10s", + }, + }, + } + + var id string + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "nottl", + Service: structs.ServiceQuery{ + Service: "db", + }, + }, + } + + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Make sure the TTL is set when requested. + m := new(dns.Msg) + m.SetQuestion("ttl.query.consul.", dns.TypeSRV) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok := in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Hdr.Ttl != 10 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 10 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + + // And the TTL should default to 0 otherwise. + m = new(dns.Msg) + m.SetQuestion("nottl.query.consul.", dns.TypeSRV) + in, _, err = c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok = in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok = in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } +} + func TestDNS_ServiceLookup_SRV_RFC(t *testing.T) { dir, srv := makeDNSServer(t) defer os.RemoveAll(dir) @@ -2008,86 +2533,137 @@ func TestDNS_NonExistingLookupEmptyAorAAAA(t *testing.T) { testutil.WaitForLeader(t, srv.agent.RPC, "dc1") - // register v6 only service - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foov6", - Address: "fe80::1", - Service: &structs.NodeService{ - Service: "webv6", - Port: 8000, - }, + // Register a v6-only service and a v4-only service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foov6", + Address: "fe80::1", + Service: &structs.NodeService{ + Service: "webv6", + Port: 8000, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foov4", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "webv4", + Port: 8000, + }, + } + + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - var out struct{} - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Register equivalent prepared queries. + { + args := &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "webv4", + Service: structs.ServiceQuery{ + Service: "webv4", + }, + }, + } + + var id string + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } + + args = &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "webv6", + Service: structs.ServiceQuery{ + Service: "webv6", + }, + }, + } + + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - // register v4 only service - args = &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foov4", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Service: "webv4", - Port: 8000, - }, + // Check for ipv6 records on ipv4-only service directly and via the + // prepared query. + questions := []string{ + "webv4.service.consul.", + "webv4.query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeAAAA) + + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + c := new(dns.Client) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Ns) != 1 { + t.Fatalf("Bad: %#v", in) + } + + soaRec, ok := in.Ns[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + if soaRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + + if in.Rcode != dns.RcodeSuccess { + t.Fatalf("Bad: %#v", in) + } } - if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { - t.Fatalf("err: %v", err) + // Check for ipv4 records on ipv6-only service directly and via the + // prepared query. + questions = []string{ + "webv6.service.consul.", + "webv6.query.consul.", } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeA) - // check for ipv6 records on ipv4 only service - m := new(dns.Msg) - m.SetQuestion("webv4.service.consul.", dns.TypeAAAA) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + c := new(dns.Client) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } - addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) - c := new(dns.Client) - in, _, err := c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) + if len(in.Ns) != 1 { + t.Fatalf("Bad: %#v", in) + } + + soaRec, ok := in.Ns[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + if soaRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + + if in.Rcode != dns.RcodeSuccess { + t.Fatalf("Bad: %#v", in) + } } - - if len(in.Ns) != 1 { - t.Fatalf("Bad: %#v", in) - } - - soaRec, ok := in.Ns[0].(*dns.SOA) - if !ok { - t.Fatalf("Bad: %#v", in.Ns[0]) - } - if soaRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Ns[0]) - } - - if in.Rcode != dns.RcodeSuccess { - t.Fatalf("Bad: %#v", in) - } - - // check for ipv4 records on ipv6 only service - m.SetQuestion("webv6.service.consul.", dns.TypeA) - - in, _, err = c.Exchange(m, addr.String()) - if err != nil { - t.Fatalf("err: %v", err) - } - - if len(in.Ns) != 1 { - t.Fatalf("Bad: %#v", in) - } - - soaRec, ok = in.Ns[0].(*dns.SOA) - if !ok { - t.Fatalf("Bad: %#v", in.Ns[0]) - } - if soaRec.Hdr.Ttl != 0 { - t.Fatalf("Bad: %#v", in.Ns[0]) - } - - if in.Rcode != dns.RcodeSuccess { - t.Fatalf("Bad: %#v", in) - } - } diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index b526ac8705..dfc2a5193a 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1077,7 +1077,7 @@ func TestPreparedQuery_Execute(t *testing.T) { var reply structs.PreparedQueryExecuteResponse err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply) - if err == nil || !strings.Contains(err.Error(), ErrQueryNotFound.Error()) { + if err == nil || err.Error() != ErrQueryNotFound.Error() { t.Fatalf("bad: %v", err) } From e9480ecb0234c0815338ef6da2c1d43bef02a385 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 10:38:44 -0800 Subject: [PATCH 051/123] Plumbs the service name back and uses agent-specific TTL settings as a fallback. --- command/agent/dns.go | 15 ++-- command/agent/dns_test.go | 95 ++++++++++++++++++++++---- consul/prepared_query_endpoint.go | 1 + consul/prepared_query_endpoint_test.go | 14 +++- consul/structs/prepared_query.go | 3 + 5 files changed, 107 insertions(+), 21 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index a5816b3d65..83bc980b40 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -587,13 +587,10 @@ RPC: goto RPC } - // TODO (slackpad) Do we want to apply the DNS server's per-service TTL - // configs if the query's TTL is not set? That seems like it adds a lot - // of complexity (we'd have to plumb the service name back with the query - // results to do this), but is it what people would expect? - // Determine the TTL. The parse should never fail since we vet it when - // the query is created, but we check anyway. + // the query is created, but we check anyway. If the query didn't + // specify a TTL then we will try to use the agent's service-specific + // TTL configs. var ttl time.Duration if out.DNS.TTL != "" { var err error @@ -601,6 +598,12 @@ RPC: if err != nil { d.logger.Printf("[WARN] dns: Failed to parse TTL '%s' for prepared query '%s', ignoring", out.DNS.TTL, query) } + } else if d.config.ServiceTTL != nil { + var ok bool + ttl, ok = d.config.ServiceTTL[out.Service] + if !ok { + ttl = d.config.ServiceTTL["*"] + } } // If we have no nodes, return not found! diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index 41a2356a85..b27befcea1 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -2087,7 +2087,6 @@ func TestDNS_ServiceLookup_TTL(t *testing.T) { } c.AllowStale = true c.MaxStale = time.Second - } dir, srv := makeDNSServerConfig(t, nil, confFn) defer os.RemoveAll(dir) @@ -2184,7 +2183,15 @@ func TestDNS_ServiceLookup_TTL(t *testing.T) { } func TestDNS_PreparedQuery_TTL(t *testing.T) { - dir, srv := makeDNSServer(t) + confFn := func(c *DNSConfig) { + c.ServiceTTL = map[string]time.Duration{ + "db": 10 * time.Second, + "*": 5 * time.Second, + } + c.AllowStale = true + c.MaxStale = time.Second + } + dir, srv := makeDNSServerConfig(t, nil, confFn) defer os.RemoveAll(dir) defer srv.agent.Shutdown() @@ -2207,20 +2214,34 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { t.Fatalf("err: %v", err) } + + args = &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "api", + Port: 2222, + }, + } + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } } - // Register prepared queries with and without a TTL set. + // Register prepared queries with and without a TTL set for "db", as + // well as one for "api". { args := &structs.PreparedQueryRequest{ Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ - Name: "ttl", + Name: "db-ttl", Service: structs.ServiceQuery{ Service: "db", }, DNS: structs.QueryDNSOptions{ - TTL: "10s", + TTL: "18s", }, }, } @@ -2234,7 +2255,7 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { Datacenter: "dc1", Op: structs.PreparedQueryCreate, Query: &structs.PreparedQuery{ - Name: "nottl", + Name: "db-nottl", Service: structs.ServiceQuery{ Service: "db", }, @@ -2244,11 +2265,27 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { t.Fatalf("err: %v", err) } + + args = &structs.PreparedQueryRequest{ + Datacenter: "dc1", + Op: structs.PreparedQueryCreate, + Query: &structs.PreparedQuery{ + Name: "api-nottl", + Service: structs.ServiceQuery{ + Service: "api", + }, + }, + } + + if err := srv.agent.RPC("PreparedQuery.Apply", args, &id); err != nil { + t.Fatalf("err: %v", err) + } } - // Make sure the TTL is set when requested. + // Make sure the TTL is set when requested, and overrides the agent- + // specific config since the query takes precedence. m := new(dns.Msg) - m.SetQuestion("ttl.query.consul.", dns.TypeSRV) + m.SetQuestion("db-ttl.query.consul.", dns.TypeSRV) c := new(dns.Client) addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) @@ -2265,7 +2302,7 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } - if srvRec.Hdr.Ttl != 10 { + if srvRec.Hdr.Ttl != 18 { t.Fatalf("Bad: %#v", in.Answer[0]) } @@ -2273,13 +2310,14 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if !ok { t.Fatalf("Bad: %#v", in.Extra[0]) } - if aRec.Hdr.Ttl != 10 { + if aRec.Hdr.Ttl != 18 { t.Fatalf("Bad: %#v", in.Extra[0]) } - // And the TTL should default to 0 otherwise. + // And the TTL should take the service-specific value from the agent's + // config otherwise. m = new(dns.Msg) - m.SetQuestion("nottl.query.consul.", dns.TypeSRV) + m.SetQuestion("db-nottl.query.consul.", dns.TypeSRV) in, _, err = c.Exchange(m, addr.String()) if err != nil { t.Fatalf("err: %v", err) @@ -2293,7 +2331,7 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if !ok { t.Fatalf("Bad: %#v", in.Answer[0]) } - if srvRec.Hdr.Ttl != 0 { + if srvRec.Hdr.Ttl != 10 { t.Fatalf("Bad: %#v", in.Answer[0]) } @@ -2301,7 +2339,36 @@ func TestDNS_PreparedQuery_TTL(t *testing.T) { if !ok { t.Fatalf("Bad: %#v", in.Extra[0]) } - if aRec.Hdr.Ttl != 0 { + if aRec.Hdr.Ttl != 10 { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + + // If there's no query TTL and no service-specific value then the wild + // card value should be used. + m = new(dns.Msg) + m.SetQuestion("api-nottl.query.consul.", dns.TypeSRV) + in, _, err = c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok = in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Hdr.Ttl != 5 { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + aRec, ok = in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Ttl != 5 { t.Fatalf("Bad: %#v", in.Extra[0]) } } diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 18a647691b..77b8e68bb2 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -386,6 +386,7 @@ func (p *PreparedQuery) execute(query *structs.PreparedQuery, } // Capture the nodes and pass the DNS information through to the reply. + reply.Service = query.Service.Service reply.Nodes = nodes reply.DNS = query.DNS diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index dfc2a5193a..adb7dceea6 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -1077,7 +1077,7 @@ func TestPreparedQuery_Execute(t *testing.T) { var reply structs.PreparedQueryExecuteResponse err := msgpackrpc.CallWithCodec(codec1, "PreparedQuery.Execute", &req, &reply) - if err == nil || err.Error() != ErrQueryNotFound.Error() { + if err == nil || err.Error() != ErrQueryNotFound.Error() { t.Fatalf("bad: %v", err) } @@ -1100,6 +1100,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1121,6 +1122,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 3 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1162,6 +1164,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1186,6 +1189,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1244,6 +1248,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1270,6 +1275,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 10 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1297,6 +1303,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1331,6 +1338,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 8 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1359,6 +1367,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 0 || reply.Datacenter != "dc1" || reply.Failovers != 0 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1385,6 +1394,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc2" || reply.Failovers != 1 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1412,6 +1422,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 3 || reply.Datacenter != "dc2" || reply.Failovers != 1 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) @@ -1438,6 +1449,7 @@ func TestPreparedQuery_Execute(t *testing.T) { if len(reply.Nodes) != 9 || reply.Datacenter != "dc2" || reply.Failovers != 1 || + reply.Service != query.Query.Service.Service || !reflect.DeepEqual(reply.DNS, query.Query.DNS) || !reply.QueryMeta.KnownLeader { t.Fatalf("bad: %v", reply) diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index e737323fd4..a38adb52cd 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -186,6 +186,9 @@ func (q *PreparedQueryExecuteRemoteRequest) RequestDatacenter() string { // 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 CheckServiceNodes From 67fd4fa78d0038b887a9da74c765a0c9ae296494 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 12:57:06 -0800 Subject: [PATCH 052/123] Returns a 404 from a get or execute of a nonexistent query. --- command/agent/prepared_query_endpoint.go | 15 ++++++++ command/agent/prepared_query_endpoint_test.go | 34 +++++++++++++++++++ consul/prepared_query_endpoint.go | 11 +++--- consul/prepared_query_endpoint_test.go | 16 ++++++--- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/command/agent/prepared_query_endpoint.go b/command/agent/prepared_query_endpoint.go index 5d9c07212d..4c549c6b0f 100644 --- a/command/agent/prepared_query_endpoint.go +++ b/command/agent/prepared_query_endpoint.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul/structs" ) @@ -101,6 +102,13 @@ func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.R var reply structs.PreparedQueryExecuteResponse if err := s.agent.RPC(endpoint+".Execute", &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 @@ -114,6 +122,13 @@ func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.R var reply structs.IndexedPreparedQueries if err := s.agent.RPC(endpoint+".Get", &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.Queries, nil diff --git a/command/agent/prepared_query_endpoint_test.go b/command/agent/prepared_query_endpoint_test.go index 23a39b196c..41b905249a 100644 --- a/command/agent/prepared_query_endpoint_test.go +++ b/command/agent/prepared_query_endpoint_test.go @@ -245,6 +245,23 @@ func TestPreparedQuery_Execute(t *testing.T) { t.Fatalf("bad: %v", r) } }) + + httpTest(t, func(srv *HTTPServer) { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/not-there/execute", 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) { @@ -296,6 +313,23 @@ func TestPreparedQuery_Get(t *testing.T) { t.Fatalf("bad: %v", r) } }) + + httpTest(t, func(srv *HTTPServer) { + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/f004177f-2c28-83b7-4229-eacc25fe55d1", 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_Update(t *testing.T) { diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 77b8e68bb2..3cdb9c213b 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -206,18 +206,17 @@ func (p *PreparedQuery) Get(args *structs.PreparedQuerySpecificRequest, if err != nil { return err } + if query == nil { + return ErrQueryNotFound + } - if (query != nil) && (query.Token != args.Token) && (acl != nil && !acl.QueryList()) { + if (query.Token != args.Token) && (acl != nil && !acl.QueryList()) { p.srv.logger.Printf("[WARN] consul.prepared_query: Request to get prepared query '%s' denied because ACL didn't match ACL used to create the query, and a management token wasn't supplied", args.QueryID) return permissionDeniedErr } reply.Index = index - if query != nil { - reply.Queries = structs.PreparedQueries{query} - } else { - reply.Queries = nil - } + reply.Queries = structs.PreparedQueries{query} return nil }) diff --git a/consul/prepared_query_endpoint_test.go b/consul/prepared_query_endpoint_test.go index adb7dceea6..59d2297ea4 100644 --- a/consul/prepared_query_endpoint_test.go +++ b/consul/prepared_query_endpoint_test.go @@ -184,7 +184,9 @@ func TestPreparedQuery_Apply(t *testing.T) { } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { - t.Fatalf("err: %v", err) + if err.Error() != ErrQueryNotFound.Error() { + t.Fatalf("err: %v", err) + } } if len(resp.Queries) != 0 { @@ -363,7 +365,9 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { - t.Fatalf("err: %v", err) + if err.Error() != ErrQueryNotFound.Error() { + t.Fatalf("err: %v", err) + } } if len(resp.Queries) != 0 { @@ -492,7 +496,9 @@ func TestPreparedQuery_Apply_ACLDeny(t *testing.T) { } var resp structs.IndexedPreparedQueries if err = msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { - t.Fatalf("err: %v", err) + if err.Error() != ErrQueryNotFound.Error() { + t.Fatalf("err: %v", err) + } } if len(resp.Queries) != 0 { @@ -792,7 +798,9 @@ func TestPreparedQuery_Get(t *testing.T) { } var resp structs.IndexedPreparedQueries if err := msgpackrpc.CallWithCodec(codec, "PreparedQuery.Get", req, &resp); err != nil { - t.Fatalf("err: %v", err) + if err.Error() != ErrQueryNotFound.Error() { + t.Fatalf("err: %v", err) + } } if len(resp.Queries) != 0 { From 4715c04c987c0d0f410c256c324d7cbdbbdc804e Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 17:18:15 -0800 Subject: [PATCH 053/123] Adds a test to make sure a stale retry terminates. --- command/agent/dns.go | 4 +++- command/agent/dns_test.go | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index 83bc980b40..bab2e90e82 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -560,7 +560,9 @@ func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, // with dup filtering done at this level we need to get everything to // match the previous behavior. We can optimize by pushing more filtering // into the query execution, but for now I think we need to get the full - // response. + // response. We could also choose a large arbitrary number that will + // likely work in practice, like 10*maxServiceResponses which should help + // reduce bandwidth if there are thousands of nodes available. endpoint := d.agent.getEndpoint(preparedQueryEndpoint) var out structs.PreparedQueryExecuteResponse diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index b27befcea1..f8d641e5a6 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -2734,3 +2734,52 @@ func TestDNS_NonExistingLookupEmptyAorAAAA(t *testing.T) { } } } + +func TestDNS_PreparedQuery_AllowStale(t *testing.T) { + confFn := func(c *DNSConfig) { + c.AllowStale = true + c.MaxStale = time.Second + } + dir, srv := makeDNSServerConfig(t, nil, confFn) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + m := MockPreparedQuery{} + if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { + t.Fatalf("err: %v", err) + } + + m.executeFn = func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { + // Return a response that's perpetually too stale. + reply.LastContact = 2 * time.Second + return nil + } + + // Make sure that the lookup terminates and results in an SOA since + // the query doesn't exist. + { + m := new(dns.Msg) + m.SetQuestion("nope.query.consul.", dns.TypeSRV) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Ns) != 1 { + t.Fatalf("Bad: %#v", in) + } + + soaRec, ok := in.Ns[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + if soaRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + } +} From f60fc872d1fab57b78412cf707a24e4564601a84 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 17:23:24 -0800 Subject: [PATCH 054/123] Gets rid of some unused constants. --- consul/structs/prepared_query.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index a38adb52cd..ba966c6a3e 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -1,14 +1,5 @@ package structs -import ( - "time" -) - -const ( - QueryTTLMax = 24 * time.Hour - QueryTTLMin = 10 * time.Second -) - // QueryDatacenterOptions sets options about how we fail over if there are no // healthy nodes in the local datacenter. type QueryDatacenterOptions struct { From 712a3dba2fe6fd1ee9b9bd2bd65254dced47d5a1 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 13 Nov 2015 18:07:35 -0800 Subject: [PATCH 055/123] 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) + } +} From 800e946bf14fb91155d4e74d62acfdc6e7085895 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 12:45:34 -0800 Subject: [PATCH 056/123] Adds and updates docs for prepared queries. --- website/Gemfile | 2 +- website/source/docs/agent/dns.html.markdown | 20 ++ website/source/docs/agent/http.html.markdown | 1 + .../docs/agent/http/query.html.markdown | 319 ++++++++++++++++++ .../docs/guides/dns-cache.html.markdown | 7 + .../docs/internals/sessions.html.markdown | 5 + website/source/layouts/docs.erb | 4 + 7 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 website/source/docs/agent/http/query.html.markdown diff --git a/website/Gemfile b/website/Gemfile index 2b35e28106..d0e5a286b7 100644 --- a/website/Gemfile +++ b/website/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -ruby "2.2.2" +ruby "2.2.3" gem "middleman-hashicorp", github: "hashicorp/middleman-hashicorp" diff --git a/website/source/docs/agent/dns.html.markdown b/website/source/docs/agent/dns.html.markdown index cf148039e1..4a06353e96 100644 --- a/website/source/docs/agent/dns.html.markdown +++ b/website/source/docs/agent/dns.html.markdown @@ -174,6 +174,26 @@ rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 Again, note that the SRV record returns the port of the service as well as its IP. +### Prepared Query Lookups + +The format of a prepared query lookup is: + + .query[.datacenter]. + +The `datacenter` is optional, and if not provided, the datacenter of this Consul +agent is assumed. + +The `query or name` is the ID or given name of an existing +[Prepared Query](/docs/agent/http/query.html). These behave like standard service +queries but provide a much richer set of features, such as filtering by multiple +tags and automatically failing over to look for services in remote datacenters if +no healthy nodes are available in the local datacenter. + +To allow for simple load balancing, the set of nodes returned is randomized each time. +Both A and SRV records are supported. SRV records provide the port that a service is +registered on, enabling clients to avoid relying on well-known ports. SRV records are +only served if the client specifically requests them. + ### UDP Based DNS Queries When the DNS query is performed using UDP, Consul will truncate the results diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index f8ac0cd9b2..38936a14ba 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -21,6 +21,7 @@ Each endpoint manages a different aspect of Consul: * [event](http/event.html) - User Events * [health](http/health.html) - Health checks * [kv](http/kv.html) - Key/Value store +* [query](http/query.html) - Prepared Queries * [session](http/session.html) - Sessions * [status](http/status.html) - Consul system status diff --git a/website/source/docs/agent/http/query.html.markdown b/website/source/docs/agent/http/query.html.markdown new file mode 100644 index 0000000000..4f352435e0 --- /dev/null +++ b/website/source/docs/agent/http/query.html.markdown @@ -0,0 +1,319 @@ +--- +layout: "docs" +page_title: "Prepared Queries (HTTP)" +sidebar_current: "docs-agent-http-query" +description: > + The Query endpoints are used to manage and execute prepared queries. +--- + +# Prepared Query HTTP Endpoint + +The Prepared Query endpoints are used to create, update, destroy, and execute +prepared queries. + +Prepared queries allow you to register a complex service query and then execute +it later via its ID or name to get a set of healthy nodes that provide a given +service. This is particularly useful in combination with Consul's +[DNS Interface](/docs/agent/dns.html) as it allows for much richer queries than +would be possible given the limited interface DNS provides. + +The following endpoints are supported: + +* [`/v1/query`](#general): Creates a new prepared query or lists + all prepared queries +* [`/v1/query/`](#specific): Updates, fetches, or deletes + a prepared query +* [`/v1/query//execute`](#execute): Executes a + prepared query by its ID or optional name + +Not all endpoints support blocking queries and all consistency modes, +see details in the sections below. + +The query endpoints support the use of ACL tokens. Prepared queries have some +special handling of ACL tokens that are highlighted in the sections below. + +### /v1/query + +The general query endpoint supports the `POST` and `GET` methods. + +#### POST Method + +When using the `POST` method, Consul will create a new prepared query and return +its ID if it is created successfully. + +By default, the datacenter of the agent is queried; however, the dc can be +provided using the "?dc=" query parameter. + +The create operation expects a JSON request body that defines the prepared query, +like this example: + +```javascript +{ + "Name": "my-query", + "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", + "Service": { + "Service": "redis", + "Failover": { + "NearestN": 3, + "Datacenters": ["dc1", "dc2"] + }, + "OnlyPassing": false, + "Tags": ["master", "!experimental"] + }, + "DNS": { + "TTL": "10s" + } +} +``` + +Only the `Service` field inside the `Service` structure is mandatory, all other +fields will take their default values if they are not included. + +`Name` is an optional friendly name that can be used to execute a query instead +of using its ID. + +`Session` provides a way to automatically remove a prepared query when the +given session is invalidated. This is optional, and if not given the prepared +query must be manually removed when no longer needed. + +The set of fields inside the `Service` structure define the query's behavior. + +`Service` is the name of the service to query. This is required. + +`Failover` contains two fields, both of which are optional, and determine what +happens if no healthy nodes are available in the local datacenter when the query +is executed. It allows the use of nodes in other datacenters with very little +configuration. + +If `NearestN` is set to a value greater than zero, then the query +will be forwarded to up to `NearestN` other datacenters based on their estimated +network round trip time using [Network Coordinates](/docs/internals/coordinates.html) +from the WAN gossip pool. The median round trip time from the server handling the +query to the servers in the remote datacenter is used to determine the priority. +The default value is zero. All Consul servers must be running version 0.6.0 or +above in order for this feature to work correctly. If any servers are not running +the required version of Consul they will be considered last since they won't have +any available network coordinate information. + +`Datacenters` contains a fixed list of remote datacenters to forward the query +to if there are no healthy nodes in the local datacenter. Datacenters are queried +in the order given in the list. If this option is combined with `NearestN`, then +the `NearestN` queries will be performed first, followed by the list given by +`Datacenters`. A given datacenter will only be queried one time during a failover, +even if it is selected by both `NearestN` and is listed in `Datacenters`. The +default value is an empty list. + +`OnlyPassing` controls the behavior of the query's health check filtering. If +this is set to false, the results will include nodes with checks in the passing +as well as the warning states. If this is set to true, only nodes with checks +in the passing state will be returned. The default value is false. + +`Tags` provides a list of service tags to filter the query results. For a service +to pass the tag filter it must have *all* of the required tags, and *none* of the +excluded tags (prefixed with `!`). The default value is an empty list, which does +no tag filtering. + +`TTL` in the `DNS` structure is a duration string that can use "s" as a +suffix for seconds. It controls how the TTL is set when query results are served +over DNS. If this isn't specified, then the Consul agent configuration for the given +service will be used (see [DNS Caching](/docs/guides/dns-cache.html)). If this is +specified, it will take precedence over any Consul agent-specific configuration. +If no TTL is specified here or at the Consul agent level, then the TTL will +default to 0. + +The return code is 200 on success and the ID of the created query is returned in +a JSON body: + +```javascript +{ + "ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05" +} +``` + +If ACLs are enabled, then the provided token will be used to check access to +the service being queried, and it will be saved along with the query for use +when the query is executed. This is key to allowing prepared queries to work +via the DNS interface, and it's important to note that prepared query IDs and +names become a read-only proxy for the token used to create the query. + +The query IDs that Consul generates are done in the same manner as ACL tokens, +so provide equal strength, but names may be more guessable and should be used +carefully with ACLs. Also, the token used to create the prepared query (or a +management token) is required to read the query back, so the ability to execute +a prepared query is not enough to get access to the actual token. + +#### GET Method + +When using the GET method, Consul will provide a listing of all prepared queries. + +By default, the datacenter of the agent is queried; however, the dc can be +provided using the "?dc=" query parameter. This endpoint supports blocking +queries and all consistency modes. + +Since this listing includes sensitive ACL tokens, this is a privileged endpoint +and always requires a management token to be supplied if ACLs are enabled. + +This returns a JSON list of prepared queries, which looks like: + +```javascript +[ + { + "ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + "Name": "my-query", + "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", + "Token": "", + "Service": { + "Service": "redis", + "Failover": { + "NearestN": 3, + "Datacenters": ["dc1", "dc2"] + }, + "OnlyPassing": false, + "Tags": ["master", "!experimental"] + }, + "DNS": { + "TTL": "10s" + }, + "RaftIndex": { + "CreateIndex": 23, + "ModifyIndex": 42 + } + } +] +``` + +### /v1/query/\ + +The query-specific endpoint supports the `GET`, `PUT`, and `DELETE` methods. The +\ argument is the ID of an existing prepared query. + +#### PUT Method + +The `PUT` method allows an existing prepared query to be updated. + +By default, the datacenter of the agent is queried; however, the dc can be +provided using the "?dc=" query parameter. + +If ACLs are enabled, then the same token used to create the query (or a +management token) must be supplied. + +The body is the same as is used to create a prepared query, as described above. + +If the API call succeeds, a 200 status code is returned. + +#### GET Method + +The `GET` method allows an existing prepared query to be fetched. + +By default, the datacenter of the agent is queried; however, the dc can be +provided using the "?dc=" query parameter. This endpoint supports blocking +queries and all consistency modes. + +The returned response is the same as the list of prepared queries above, +only with a single item present. If the query does not exist then a 404 +status code will be returned. + +If ACLs are enabled, then the same token used to create the query (or a +management token) must be supplied. + +#### DELETE Method + +The `DELETE` method is used to delete a prepared query. + +By default, the datacenter of the agent is queried; however, the dc can be +provided using the "?dc=" query parameter. + +If ACLs are enabled, then the same token used to create the query (or a +management token) must be supplied. + +No body is required as part of this request. + +If the API call succeeds, a 200 status code is returned. + +### /v1/query/\/execute + +The query execute endpoint supports only the `GET` method and is used to +execute a prepared query. The \ argument is the ID or name +of an existing prepared query. + +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. + +Adding the optional "?near=" parameter with a node name will sort the resulting +list in ascending order based on the estimated round trip time from that node. +Passing "?near=_agent" will use the agent's node for the sort. If this is not +present, then the nodes will be shuffled randomly and will be in a different +order each time the query is executed. + +An optional "?limit=" parameter can be used to limit the size of the list to +the given number of nodes. This is applied after any sorting or shuffling. + +The ACL token supplied when the prepared query was created will be used to +execute the request, so no ACL token needs to be supplied (it will be ignored). + +No body is required as part of this request. + +If the query does not exist then a 404 status code will be returned. Otherwise, +a JSON body will be returned like this: + +```javascript +{ + "Service": "redis", + "Nodes": [ + { + "Node": { + "Node": "foobar", + "Address": "10.1.10.12" + }, + "Service": { + "ID": "redis", + "Service": "redis", + "Tags": null, + "Port": 8000 + }, + "Checks": [ + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + }, + { + "Node": "foobar", + "CheckID": "serfHealth", + "Name": "Serf Health Status", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "", + "ServiceName": "" + } + ], + "DNS": { + "TTL": "10s" + }, + "Datacenter": "dc3", + "Failovers": 2 + } +} +``` + +The `Nodes` section contains the list of healthy nodes providing the given +service, as specified by the constraints of the prepared query. + +`Service` has the service name that the query was selecting. This is useful +for context in case an empty list of nodes is returned. + +`DNS` has information used when serving the results over DNS. This is just a +copy of the structure given when the prepared query was created. + +`Datacenter` has the datacenter that ultimately provided the list of nodes +and `Failvovers` has the number of remote datacenters that were queried +while executing the query. This provides some insight into where the data +came from. This will be zero during non-failover operations where there +were healthy nodes found in the local datacenter. diff --git a/website/source/docs/guides/dns-cache.html.markdown b/website/source/docs/guides/dns-cache.html.markdown index 20a7921b7d..dbeffe3554 100644 --- a/website/source/docs/guides/dns-cache.html.markdown +++ b/website/source/docs/guides/dns-cache.html.markdown @@ -93,3 +93,10 @@ a wildcard TTL and a specific TTL for a service might look like this: This sets all lookups to "web.service.consul" to use a 30 second TTL while lookups to "db.service.consul" or "api.service.consul" will use the 5 second TTL from the wildcard. + +[Prepared Queries](/docs/agent/http/query.html) provide an additional +level of control over TTL. They allow for the TTL to be defined along with +the query, and they can be changed on the fly by updating the query definition. +If a TTL is not configured for a prepared query, then it will fall back to the +service-specific configuration defined in the Consul agent as described above, +and ultimately to 0 if no TTL is configured for the service in the Consul agent. diff --git a/website/source/docs/internals/sessions.html.markdown b/website/source/docs/internals/sessions.html.markdown index d864da5ecc..da100decc2 100644 --- a/website/source/docs/internals/sessions.html.markdown +++ b/website/source/docs/internals/sessions.html.markdown @@ -145,3 +145,8 @@ the goal of Consul to protect against misbehaving clients. The primitives provided by sessions and the locking mechanisms of the KV store can be used to build client-side leader election algorithms. These are covered in more detail in the [Leader Election guide](/docs/guides/leader-election.html). + +## Prepared Query Integration + +Prepared queries may be attached to a session in order to automatically delete +the prepared query when the session is invalidated. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 166b165552..92dd3e4b98 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -178,6 +178,10 @@ Network Coordinates + > + Prepared Queries + + > Sessions From 162c6bafefee86f5585dc91113c2f1be42796d9a Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 12:49:35 -0800 Subject: [PATCH 057/123] Updates the changelog. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d14a0088a5..d4105902b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ FEATURES: * Consul now builds under Go 1.5.1 by default [GH-1345] * Added built-in support for running health checks inside Docker containers [GH-1343] +* Added prepared queries which support service health queries with rich + features such as filters for multiple tags and failover to remote datacenters + based on network coordinates; these are available via HTTP as well as the + DNS interface [GH-1389] BUG FIXES: From 46d5afa57461b712ee7edde28c061007aac45443 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 16:13:40 -0800 Subject: [PATCH 058/123] Adds a test to ensure we don't return a nil slice. --- consul/state/prepared_query_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/consul/state/prepared_query_test.go b/consul/state/prepared_query_test.go index d47fab840c..bb057ec972 100644 --- a/consul/state/prepared_query_test.go +++ b/consul/state/prepared_query_test.go @@ -379,6 +379,18 @@ func TestStateStore_PreparedQueryLookup(t *testing.T) { func TestStateStore_PreparedQueryList(t *testing.T) { s := testStateStore(t) + // Make sure an empty (non-nil) slice is returned if there are no queries. + idx, actual, err := s.PreparedQueryList() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 0 { + t.Fatalf("bad index: %d", idx) + } + if actual == nil || len(actual) != 0 { + t.Fatalf("bad: %v", actual) + } + // Set up our test environment. testRegisterNode(t, s, 1, "foo") testRegisterService(t, s, 2, "foo", "redis") @@ -439,7 +451,7 @@ func TestStateStore_PreparedQueryList(t *testing.T) { }, }, } - idx, actual, err := s.PreparedQueryList() + idx, actual, err = s.PreparedQueryList() if err != nil { t.Fatalf("err: %s", err) } From b8ddb2197883940b845e6de79bc326fd0c88c239 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 21:59:08 -0800 Subject: [PATCH 059/123] Adds a paranoia set of the nodes slice to nil. --- consul/prepared_query_endpoint.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/consul/prepared_query_endpoint.go b/consul/prepared_query_endpoint.go index 3cdb9c213b..f331460954 100644 --- a/consul/prepared_query_endpoint.go +++ b/consul/prepared_query_endpoint.go @@ -544,6 +544,14 @@ func queryFailover(q queryServer, query *structs.PreparedQuery, // This keeps track of how many iterations we actually run. failovers++ + // Be super paranoid and set the nodes slice to nil since it's + // the same slice we used before. We know there's nothing in + // there, but the underlying msgpack library has a policy of + // updating the slice when it's non-nil, and that feels dirty. + // Let's just set it to nil so there's no way to communicate + // through this slice across successive RPC calls. + reply.Nodes = nil + // Note that we pass along the limit since it can be applied // remotely to save bandwidth. We also pass along the consistency // mode information we were given, so that applies to the remote From e1ce1a34b0a63803ad909a4935e7073ebc9407a4 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Sat, 14 Nov 2015 21:59:23 -0800 Subject: [PATCH 060/123] Moves conversion of nil slices up to HTTP layer for prepared queries. --- command/agent/prepared_query_endpoint.go | 10 +++ command/agent/prepared_query_endpoint_test.go | 68 +++++++++++++++++++ consul/state/prepared_query.go | 2 +- consul/state/prepared_query_test.go | 4 +- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/command/agent/prepared_query_endpoint.go b/command/agent/prepared_query_endpoint.go index 4c549c6b0f..2f4b78e49b 100644 --- a/command/agent/prepared_query_endpoint.go +++ b/command/agent/prepared_query_endpoint.go @@ -54,6 +54,11 @@ func (s *HTTPServer) PreparedQueryGeneral(resp http.ResponseWriter, req *http.Re if err := s.agent.RPC(endpoint+".List", &args, &reply); err != nil { return nil, err } + + // Use empty list instead of nil. + if reply.Queries == nil { + reply.Queries = make(structs.PreparedQueries, 0) + } return reply.Queries, nil default: @@ -111,6 +116,11 @@ func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.R } return nil, err } + + // Use empty list instead of nil. + if reply.Nodes == nil { + reply.Nodes = make(structs.CheckServiceNodes, 0) + } return reply, nil } else { args := structs.PreparedQuerySpecificRequest{ diff --git a/command/agent/prepared_query_endpoint_test.go b/command/agent/prepared_query_endpoint_test.go index 41b905249a..ac095320c1 100644 --- a/command/agent/prepared_query_endpoint_test.go +++ b/command/agent/prepared_query_endpoint_test.go @@ -144,6 +144,40 @@ func TestPreparedQuery_Create(t *testing.T) { } func TestPreparedQuery_List(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.listFn = func(args *structs.DCSpecificRequest, reply *structs.IndexedPreparedQueries) error { + // Return an empty response. + return nil + } + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/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.(structs.PreparedQueries) + if !ok { + t.Fatalf("unexpected: %T", obj) + } + if r == nil || len(r) != 0 { + t.Fatalf("bad: %v", r) + } + }) + httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { @@ -194,6 +228,40 @@ func TestPreparedQuery_List(t *testing.T) { } func TestPreparedQuery_Execute(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.executeFn = func(args *structs.PreparedQueryExecuteRequest, reply *structs.PreparedQueryExecuteResponse) error { + // Just return an empty response. + return nil + } + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("GET", "/v1/query/my-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 r.Nodes == nil || len(r.Nodes) != 0 { + t.Fatalf("bad: %v", r) + } + }) + httpTest(t, func(srv *HTTPServer) { m := MockPreparedQuery{} if err := srv.agent.InjectEndpoint("PreparedQuery", &m); err != nil { diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index 1570d20d6c..b404977e3c 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -243,7 +243,7 @@ func (s *StateStore) PreparedQueryList() (uint64, structs.PreparedQueries, error } // Go over all of the queries and build the response. - result := make(structs.PreparedQueries, 0) + var result structs.PreparedQueries for query := queries.Next(); query != nil; query = queries.Next() { result = append(result, query.(*structs.PreparedQuery)) } diff --git a/consul/state/prepared_query_test.go b/consul/state/prepared_query_test.go index bb057ec972..7f7dd0b7af 100644 --- a/consul/state/prepared_query_test.go +++ b/consul/state/prepared_query_test.go @@ -379,7 +379,7 @@ func TestStateStore_PreparedQueryLookup(t *testing.T) { func TestStateStore_PreparedQueryList(t *testing.T) { s := testStateStore(t) - // Make sure an empty (non-nil) slice is returned if there are no queries. + // Make sure nothing is returned for an empty query idx, actual, err := s.PreparedQueryList() if err != nil { t.Fatalf("err: %s", err) @@ -387,7 +387,7 @@ func TestStateStore_PreparedQueryList(t *testing.T) { if idx != 0 { t.Fatalf("bad index: %d", idx) } - if actual == nil || len(actual) != 0 { + if len(actual) != 0 { t.Fatalf("bad: %v", actual) } From 06b918e46d9d525ee5b95938013f1d47984b6283 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 16 Nov 2015 22:57:47 -0800 Subject: [PATCH 061/123] Makes UUID regex case-insensitive. --- consul/state/prepared_query.go | 2 +- consul/state/prepared_query_test.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index b404977e3c..68a94524ca 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -9,7 +9,7 @@ import ( ) // validUUID is used to check if a given string looks like a UUID -var validUUID = regexp.MustCompile(`^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`) +var validUUID = regexp.MustCompile(`(?i)^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`) // isUUID returns true if the given string is a valid UUID. func isUUID(str string) bool { diff --git a/consul/state/prepared_query_test.go b/consul/state/prepared_query_test.go index 7f7dd0b7af..49817882ff 100644 --- a/consul/state/prepared_query_test.go +++ b/consul/state/prepared_query_test.go @@ -13,9 +13,14 @@ func TestStateStore_PreparedQuery_isUUID(t *testing.T) { "": false, "nope": false, "f004177f-2c28-83b7-4229-eacc25fe55d1": true, + "F004177F-2C28-83B7-4229-EACC25FE55D1": true, + "x004177f-2c28-83b7-4229-eacc25fe55d1": false, // Bad hex + "f004177f-xc28-83b7-4229-eacc25fe55d1": false, // Bad hex + "f004177f-2c28-x3b7-4229-eacc25fe55d1": false, // Bad hex + "f004177f-2c28-83b7-x229-eacc25fe55d1": false, // Bad hex + "f004177f-2c28-83b7-4229-xacc25fe55d1": false, // Bad hex " f004177f-2c28-83b7-4229-eacc25fe55d1": false, // Leading whitespace "f004177f-2c28-83b7-4229-eacc25fe55d1 ": false, // Trailing whitespace - "f004177f-2c28-83B7-4229-eacc25fe55d1": false, // Bad hex "83B7" } for i := 0; i < 100; i++ { cases[testUUID()] = true From 8fc6a6a986d19bd4dce42e83aeb10f37e1c40591 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 16 Nov 2015 23:12:44 -0800 Subject: [PATCH 062/123] Switches to helpers for prepared query API wrappers. --- api/prepared_query.go | 58 ++++++------------------------------------- 1 file changed, 7 insertions(+), 51 deletions(-) diff --git a/api/prepared_query.go b/api/prepared_query.go index 2ab1ccceb5..3ec53d7d19 100644 --- a/api/prepared_query.go +++ b/api/prepared_query.go @@ -123,37 +123,15 @@ func (c *PreparedQuery) Create(query *PreparedQueryDefinition, q *WriteOptions) // 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 + return c.c.write("/v1/query/"+query.ID, query, nil, q) } // 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 { + qm, err := c.c.query("/v1/query", &out, q) + if err != nil { return nil, nil, err } return out, qm, nil @@ -161,20 +139,9 @@ func (c *PreparedQuery) List(q *QueryOptions) ([]*PreparedQueryDefinition, *Quer // 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 { + qm, err := c.c.query("/v1/query/"+queryID, &out, q) + if err != nil { return nil, nil, err } return out, qm, nil @@ -199,20 +166,9 @@ func (c *PreparedQuery) Delete(queryID string, q *QueryOptions) (*QueryMeta, err // 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 { + qm, err := c.c.query("/v1/query/"+queryIDOrName+"/execute", &out, q) + if err != nil { return nil, nil, err } return out, qm, nil From 1059a8b3a3941e53674d58e11baf89aecc7fcd07 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 08:29:20 -0800 Subject: [PATCH 063/123] Removes a useless empty import and fixes some stale comments. --- api/prepared_query.go | 4 +--- consul/structs/prepared_query.go | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api/prepared_query.go b/api/prepared_query.go index 3ec53d7d19..c8141887c4 100644 --- a/api/prepared_query.go +++ b/api/prepared_query.go @@ -1,7 +1,5 @@ package api -import () - // QueryDatacenterOptions sets options about how we fail over if there are no // healthy nodes in the local datacenter. type QueryDatacenterOptions struct { @@ -37,7 +35,7 @@ type ServiceQuery struct { 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 + // this list it must be present. If the tag is preceded with "!" then // it is disallowed. Tags []string } diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index ba966c6a3e..c2d04d4e6a 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -35,7 +35,7 @@ type ServiceQuery struct { 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 + // this list it must be present. If the tag is preceded with "!" then // it is disallowed. Tags []string } From cd6be4a88d9705bb2eb5745492c65308ce10425b Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 08:40:47 -0800 Subject: [PATCH 064/123] Avoids taking the length again when parsing DNS queries. --- command/agent/dns.go | 6 ++++-- command/agent/dns_test.go | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index bab2e90e82..33db8ba7e4 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -330,17 +330,19 @@ PARSE: } case "node": - if len(labels) == 1 { + if n == 1 { goto INVALID } + // Allow a "." in the node name, just join all the parts node := strings.Join(labels[:n-1], ".") d.nodeLookup(network, datacenter, node, req, resp) case "query": - if len(labels) == 1 { + if n == 1 { goto INVALID } + // Allow a "." in the query name, just join all the parts. query := strings.Join(labels[:n-1], ".") d.preparedQueryLookup(network, datacenter, query, req, resp) diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index f8d641e5a6..2c2c8fd0a2 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -2783,3 +2783,43 @@ func TestDNS_PreparedQuery_AllowStale(t *testing.T) { } } } + +func TestDNS_InvalidQueries(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Try invalid forms of queries that should hit the special invalid case + // of our query parser. + questions := []string{ + "consul.", + "node.consul.", + "service.consul.", + "query.consul.", + } + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Ns) != 1 { + t.Fatalf("Bad: %#v", in) + } + + soaRec, ok := in.Ns[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + if soaRec.Hdr.Ttl != 0 { + t.Fatalf("Bad: %#v", in.Ns[0]) + } + } +} From 049da2cef23f449d6a889afa372e33afc60234b8 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 09:16:04 -0800 Subject: [PATCH 065/123] Breaks up huge HTTP endpoint functions. --- command/agent/prepared_query_endpoint.go | 280 +++++++++++++---------- 1 file changed, 157 insertions(+), 123 deletions(-) diff --git a/command/agent/prepared_query_endpoint.go b/command/agent/prepared_query_endpoint.go index 2f4b78e49b..af6ed76912 100644 --- a/command/agent/prepared_query_endpoint.go +++ b/command/agent/prepared_query_endpoint.go @@ -20,46 +20,57 @@ 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) { - endpoint := s.agent.getEndpoint(preparedQueryEndpoint) - 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 { +// preparedQueryCreate makes a new prepared query. +func (s *HTTPServer) preparedQueryCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + 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 structs.IndexedPreparedQueries - if err := s.agent.RPC(endpoint+".List", &args, &reply); err != nil { - return nil, err - } + var reply string + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return preparedQueryCreateResponse{reply}, nil +} - // Use empty list instead of nil. - if reply.Queries == nil { - reply.Queries = make(structs.PreparedQueries, 0) - } - return reply.Queries, nil +// preparedQueryList returns all the prepared queries. +func (s *HTTPServer) preparedQueryList(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + var args structs.DCSpecificRequest + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var reply structs.IndexedPreparedQueries + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".List", &args, &reply); err != nil { + return nil, err + } + + // Use empty list instead of nil. + if reply.Queries == nil { + reply.Queries = make(structs.PreparedQueries, 0) + } + return reply.Queries, nil +} + +// PreparedQueryGeneral handles all the general prepared query requests. +func (s *HTTPServer) PreparedQueryGeneral(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + switch req.Method { + case "POST": + return s.preparedQueryCreate(resp, req) + + case "GET": + return s.preparedQueryList(resp, req) default: resp.WriteHeader(405) @@ -80,7 +91,109 @@ func parseLimit(req *http.Request, limit *int) error { return nil } -// PreparedQuerySpecifc handles all the prepared query requests specific to a +// preparedQueryExecute executes a prepared query. +func (s *HTTPServer) preparedQueryExecute(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + 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 + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".Execute", &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 + } + + // Use empty list instead of nil. + if reply.Nodes == nil { + reply.Nodes = make(structs.CheckServiceNodes, 0) + } + return reply, nil +} + +// preparedQueryGet returns a single prepared query. +func (s *HTTPServer) preparedQueryGet(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.PreparedQuerySpecificRequest{ + QueryID: id, + } + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + var reply structs.IndexedPreparedQueries + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".Get", &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.Queries, nil +} + +// preparedQueryUpdate updates a prepared query. +func (s *HTTPServer) preparedQueryUpdate(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + 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 + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return nil, nil +} + +// preparedQueryDelete deletes prepared query. +func (s *HTTPServer) preparedQueryDelete(id string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + args := structs.PreparedQueryRequest{ + Op: structs.PreparedQueryDelete, + Query: &structs.PreparedQuery{ + ID: id, + }, + } + s.parseDC(req, &args.Datacenter) + s.parseToken(req, &args.Token) + + var reply string + endpoint := s.agent.getEndpoint(preparedQueryEndpoint) + if err := s.agent.RPC(endpoint+".Apply", &args, &reply); err != nil { + return nil, err + } + return nil, nil +} + +// PreparedQuerySpecific handles all the prepared query requests specific to a // particular query. func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { id := strings.TrimPrefix(req.URL.Path, "/v1/query/") @@ -90,98 +203,19 @@ func (s *HTTPServer) PreparedQuerySpecific(resp http.ResponseWriter, req *http.R id = strings.TrimSuffix(id, preparedQueryExecuteSuffix) } - endpoint := s.agent.getEndpoint(preparedQueryEndpoint) switch req.Method { - case "GET": // Execute or retrieve a prepared query. + case "GET": 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 { - // 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 - } - - // Use empty list instead of nil. - if reply.Nodes == nil { - reply.Nodes = make(structs.CheckServiceNodes, 0) - } - return reply, nil + return s.preparedQueryExecute(id, resp, req) } 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 { - // 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.Queries, nil + return s.preparedQueryGet(id, resp, req) } - 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 - } - } + case "PUT": + return s.preparedQueryUpdate(id, resp, req) - // 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 + case "DELETE": + return s.preparedQueryDelete(id, resp, req) default: resp.WriteHeader(405) From 533e6fc89eb233f5dbaa15384cfa744e267289b1 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 09:25:20 -0800 Subject: [PATCH 066/123] Returns a zero index for a lookup error case. --- consul/state/prepared_query.go | 2 +- consul/state/prepared_query_test.go | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/consul/state/prepared_query.go b/consul/state/prepared_query.go index 68a94524ca..ce39dea54d 100644 --- a/consul/state/prepared_query.go +++ b/consul/state/prepared_query.go @@ -200,7 +200,7 @@ func (s *StateStore) PreparedQueryLookup(queryIDOrName string) (uint64, *structs // but we check it here to be explicit about it (we'd never want to // return the results from the first query w/o a name). if queryIDOrName == "" { - return idx, nil, ErrMissingQueryID + return 0, nil, ErrMissingQueryID } // Try first by ID if it looks like they gave us an ID. We check the diff --git a/consul/state/prepared_query_test.go b/consul/state/prepared_query_test.go index 49817882ff..e4da659945 100644 --- a/consul/state/prepared_query_test.go +++ b/consul/state/prepared_query_test.go @@ -376,8 +376,15 @@ func TestStateStore_PreparedQueryLookup(t *testing.T) { // Make sure an empty lookup is well-behaved if there are actual queries // in the state store. - if _, _, err = s.PreparedQueryLookup(""); err != ErrMissingQueryID { - t.Fatalf("bad: %v", err) + idx, actual, err = s.PreparedQueryLookup("") + if err != ErrMissingQueryID { + t.Fatalf("bad: %v ", err) + } + if idx != 0 { + t.Fatalf("bad index: %d", idx) + } + if actual != nil { + t.Fatalf("bad: %v", actual) } } From e3ef204ac7a429a311312c436a9ffa56b0729d18 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 09:27:10 -0800 Subject: [PATCH 067/123] Makes all the query ops the correct type. --- consul/structs/prepared_query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consul/structs/prepared_query.go b/consul/structs/prepared_query.go index c2d04d4e6a..af360f7d24 100644 --- a/consul/structs/prepared_query.go +++ b/consul/structs/prepared_query.go @@ -83,8 +83,8 @@ type PreparedQueryOp string const ( PreparedQueryCreate PreparedQueryOp = "create" - PreparedQueryUpdate = "update" - PreparedQueryDelete = "delete" + PreparedQueryUpdate PreparedQueryOp = "update" + PreparedQueryDelete PreparedQueryOp = "delete" ) // QueryRequest is used to create or change prepared queries. From 13351bd18c004ed3b01d25fa351929638d3df605 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 11:42:24 -0800 Subject: [PATCH 068/123] Updates change log pre-RC2. --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4105902b5..567cb2bfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,10 +76,12 @@ IMPROVEMENTS: lock [GH-1291] * Improved an O(n^2) algorithm in the agent's catalog sync code [GH-1296] * Switched to net-rpc-msgpackrpc to reduce RPC overhead [GH-1307] -* Removes all uses of the http package's default client and transport in +* Removed all uses of the http package's default client and transport in Consul to avoid conflicts with other packages [GH-1310] [GH-1327] -* Adds new `X-Consul-Token` HTTP header option to avoid passing tokens +* Added new `X-Consul-Token` HTTP header option to avoid passing tokens in the query string [GH-1318] +* Increased session TTL max to 24 hours (use with caution, see note added + to the Session HTTP endpoint documentation) [GH-1412] MISC: From 46159efdece196a66cba7461f44ff19ed4005581 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 11:43:22 -0800 Subject: [PATCH 069/123] Bumps the pre-release version for RC2. --- version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.go b/version.go index f7aa280c84..4a1f0bbb9a 100644 --- a/version.go +++ b/version.go @@ -12,4 +12,4 @@ const Version = "0.6.0" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "rc1" +const VersionPrerelease = "rc2" From e4d7bfee6a9554c1fbfbd90fd90a5d8c4a979300 Mon Sep 17 00:00:00 2001 From: Sean Chittenden Date: Tue, 17 Nov 2015 15:21:47 -0800 Subject: [PATCH 070/123] Fix markdown's markup: 2^(64-1) != 2^(64)-1 --- website/source/docs/agent/http/kv.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/agent/http/kv.html.markdown b/website/source/docs/agent/http/kv.html.markdown index 30abd31c08..d3cde07e3c 100644 --- a/website/source/docs/agent/http/kv.html.markdown +++ b/website/source/docs/agent/http/kv.html.markdown @@ -103,7 +103,7 @@ value corresponding to the key. There are a number of query parameters that can be used with a PUT request: * ?flags=\ : This can be used to specify an unsigned value between - 0 and 2^64-1. Clients can choose to use this however makes sense for their application. + 0 and (2^64)-1. Clients can choose to use this however makes sense for their application. * ?cas=\ : This flag is used to turn the `PUT` into a Check-And-Set operation. This is very useful as a building block for more complex From 402a3666769ea796d3b5b779ae343bb8c59abc50 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 17 Nov 2015 19:24:56 -0800 Subject: [PATCH 072/123] Adds a deps file for v0.6.0-rc2. --- deps/v0-6-0-rc2.json | 134 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 deps/v0-6-0-rc2.json diff --git a/deps/v0-6-0-rc2.json b/deps/v0-6-0-rc2.json new file mode 100644 index 0000000000..8952cca37c --- /dev/null +++ b/deps/v0-6-0-rc2.json @@ -0,0 +1,134 @@ +{ + "ImportPath": "github.com/hashicorp/consul", + "GoVersion": "go1.5.1", + "Deps": [ + { + "ImportPath": "github.com/DataDog/datadog-go/statsd", + "Rev": "b050cd8f4d7c394545fd7d966c8e2909ce89d552" + }, + { + "ImportPath": "github.com/armon/circbuf", + "Rev": "bbbad097214e2918d8543d5201d12bfd7bca254d" + }, + { + "ImportPath": "github.com/armon/go-metrics", + "Rev": "6c5fa0d8f48f4661c9ba8709799c88d425ad20f0" + }, + { + "ImportPath": "github.com/armon/go-radix", + "Rev": "fbd82e84e2b13651f3abc5ffd26b65ba71bc8f93" + }, + { + "ImportPath": "github.com/boltdb/bolt", + "Comment": "v1.1.0-19-g0b00eff", + "Rev": "0b00effdd7a8270ebd91c24297e51643e370dd52" + }, + { + "ImportPath": "github.com/fsouza/go-dockerclient", + "Rev": "2350d7bc12bb04f2d7d6824c7718012b1397b760" + }, + { + "ImportPath": "github.com/hashicorp/errwrap", + "Rev": "7554cd9344cec97297fa6649b055a8c98c2a1e55" + }, + { + "ImportPath": "github.com/hashicorp/go-checkpoint", + "Rev": "e4b2dc34c0f698ee04750bf2035d8b9384233e1b" + }, + { + "ImportPath": "github.com/hashicorp/go-cleanhttp", + "Rev": "5df5ddc69534f1a4697289f1dca2193fbb40213f" + }, + { + "ImportPath": "github.com/hashicorp/go-immutable-radix", + "Rev": "aca1bd0689e10884f20d114aff148ddb849ece80" + }, + { + "ImportPath": "github.com/hashicorp/go-memdb", + "Rev": "9ea975be0e31ada034a5760340d4892f3f543d20" + }, + { + "ImportPath": "github.com/hashicorp/go-msgpack/codec", + "Rev": "fa3f63826f7c23912c15263591e65d54d080b458" + }, + { + "ImportPath": "github.com/hashicorp/go-multierror", + "Rev": "d30f09973e19c1dfcd120b2d9c4f168e68d6b5d5" + }, + { + "ImportPath": "github.com/hashicorp/go-syslog", + "Rev": "42a2b573b664dbf281bd48c3cc12c086b17a39ba" + }, + { + "ImportPath": "github.com/hashicorp/golang-lru", + "Rev": "a6091bb5d00e2e9c4a16a0e739e306f8a3071a3c" + }, + { + "ImportPath": "github.com/hashicorp/hcl", + "Rev": "2deb1d1db27ed473f38fe65a16044572b9ff9d30" + }, + { + "ImportPath": "github.com/hashicorp/logutils", + "Rev": "0dc08b1671f34c4250ce212759ebd880f743d883" + }, + { + "ImportPath": "github.com/hashicorp/memberlist", + "Rev": "28424fb38c7c3e30f366b72b1a55f690d318d8f3" + }, + { + "ImportPath": "github.com/hashicorp/net-rpc-msgpackrpc", + "Rev": "a14192a58a694c123d8fe5481d4a4727d6ae82f3" + }, + { + "ImportPath": "github.com/hashicorp/raft", + "Rev": "d136cd15dfb7876fd7c89cad1995bc4f19ceb294" + }, + { + "ImportPath": "github.com/hashicorp/raft-boltdb", + "Rev": "d1e82c1ec3f15ee991f7cc7ffd5b67ff6f5bbaee" + }, + { + "ImportPath": "github.com/hashicorp/scada-client", + "Rev": "84989fd23ad4cc0e7ad44d6a871fd793eb9beb0a" + }, + { + "ImportPath": "github.com/hashicorp/serf/coordinate", + "Comment": "v0.6.4-145-ga72c045", + "Rev": "a72c0453da2ba628a013e98bf323a76be4aa1443" + }, + { + "ImportPath": "github.com/hashicorp/serf/serf", + "Comment": "v0.6.4-145-ga72c045", + "Rev": "a72c0453da2ba628a013e98bf323a76be4aa1443" + }, + { + "ImportPath": "github.com/hashicorp/yamux", + "Rev": "ddcd0a6ec7c55e29f235e27935bf98d302281bd3" + }, + { + "ImportPath": "github.com/inconshreveable/muxado", + "Rev": "f693c7e88ba316d1a0ae3e205e22a01aa3ec2848" + }, + { + "ImportPath": "github.com/miekg/dns", + "Rev": "d27455715200c7d3e321a1e5cadb27c9ee0b0f02" + }, + { + "ImportPath": "github.com/mitchellh/cli", + "Rev": "8102d0ed5ea2709ade1243798785888175f6e415" + }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "281073eb9eb092240d33ef253c404f1cca550309" + }, + { + "ImportPath": "github.com/ryanuber/columnize", + "Comment": "v2.0.1-8-g983d3a5", + "Rev": "983d3a5fab1bf04d1b412465d2d9f8430e2e917e" + }, + { + "ImportPath": "golang.org/x/crypto/ssh/terminal", + "Rev": "346896d57731cb5670b36c6178fc5519f3225980" + } + ] +} From 95c708f65e69562b1aa22e78d130a532c7f3f85c Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 18 Nov 2015 07:40:02 -0800 Subject: [PATCH 073/123] Adds Docker checks support to client API. Also changed `DockerContainerId` to `DockerContainerID`, and updated the agent API docs to reflect their support for Docker checks. --- api/agent.go | 16 ++++--- api/agent_test.go | 44 +++++++++++++++++++ command/agent/agent.go | 2 +- command/agent/check.go | 12 ++--- command/agent/check_test.go | 8 ++-- command/agent/config.go | 2 +- command/agent/config_test.go | 2 +- .../docs/agent/http/agent.html.markdown | 6 +++ 8 files changed, 72 insertions(+), 20 deletions(-) diff --git a/api/agent.go b/api/agent.go index 2b950d0a3e..e4466a6511 100644 --- a/api/agent.go +++ b/api/agent.go @@ -63,13 +63,15 @@ type AgentCheckRegistration struct { // AgentServiceCheck is used to create an associated // check for a service type AgentServiceCheck struct { - Script string `json:",omitempty"` - Interval string `json:",omitempty"` - Timeout string `json:",omitempty"` - TTL string `json:",omitempty"` - HTTP string `json:",omitempty"` - TCP string `json:",omitempty"` - Status string `json:",omitempty"` + Script string `json:",omitempty"` + DockerContainerID string `json:",omitempty"` + Shell string `json:",omitempty"` // Only supported for Docker. + Interval string `json:",omitempty"` + Timeout string `json:",omitempty"` + TTL string `json:",omitempty"` + HTTP string `json:",omitempty"` + TCP string `json:",omitempty"` + Status string `json:",omitempty"` } type AgentServiceChecks []*AgentServiceCheck diff --git a/api/agent_test.go b/api/agent_test.go index 358c12a6c2..c49696aeba 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -387,6 +387,50 @@ func TestAgent_Checks_serviceBound(t *testing.T) { } } +func TestAgent_Checks_Docker(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + + // First register a service + serviceReg := &AgentServiceRegistration{ + Name: "redis", + } + if err := agent.ServiceRegister(serviceReg); err != nil { + t.Fatalf("err: %v", err) + } + + // Register a check bound to the service + reg := &AgentCheckRegistration{ + Name: "redischeck", + ServiceID: "redis", + AgentServiceCheck: AgentServiceCheck{ + DockerContainerID: "f972c95ebf0e", + Script: "/bin/true", + Shell: "/bin/bash", + Interval: "10s", + }, + } + if err := agent.CheckRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + checks, err := agent.Checks() + if err != nil { + t.Fatalf("err: %v", err) + } + + check, ok := checks["redischeck"] + if !ok { + t.Fatalf("missing check: %v", checks) + } + if check.ServiceID != "redis" { + t.Fatalf("missing service association for check: %v", check) + } +} + func TestAgent_Join(t *testing.T) { t.Parallel() c, s := makeClient(t) diff --git a/command/agent/agent.go b/command/agent/agent.go index a9e3363285..afe247e1d0 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -919,7 +919,7 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType, persist dockerCheck := &CheckDocker{ Notify: &a.state, CheckID: check.CheckID, - DockerContainerId: chkType.DockerContainerId, + DockerContainerID: chkType.DockerContainerID, Shell: chkType.Shell, Script: chkType.Script, Interval: chkType.Interval, diff --git a/command/agent/check.go b/command/agent/check.go index bdc45c53b7..c658bb0170 100644 --- a/command/agent/check.go +++ b/command/agent/check.go @@ -44,7 +44,7 @@ type CheckType struct { HTTP string TCP string Interval time.Duration - DockerContainerId string + DockerContainerID string Shell string Timeout time.Duration @@ -68,7 +68,7 @@ func (c *CheckType) IsTTL() bool { // IsMonitor checks if this is a Monitor type func (c *CheckType) IsMonitor() bool { - return c.Script != "" && c.DockerContainerId == "" && c.Interval != 0 + return c.Script != "" && c.DockerContainerID == "" && c.Interval != 0 } // IsHTTP checks if this is a HTTP type @@ -82,7 +82,7 @@ func (c *CheckType) IsTCP() bool { } func (c *CheckType) IsDocker() bool { - return c.DockerContainerId != "" && c.Script != "" && c.Interval != 0 + return c.DockerContainerID != "" && c.Script != "" && c.Interval != 0 } // CheckNotifier interface is used by the CheckMonitor @@ -518,7 +518,7 @@ type CheckDocker struct { Notify CheckNotifier CheckID string Script string - DockerContainerId string + DockerContainerID string Shell string Interval time.Duration Logger *log.Logger @@ -574,7 +574,7 @@ func (c *CheckDocker) Stop() { func (c *CheckDocker) run() { // Get the randomized initial pause time initialPauseTime := randomStagger(c.Interval) - c.Logger.Printf("[DEBUG] agent: pausing %v before first invocation of %s -c %s in container %s", initialPauseTime, c.Shell, c.Script, c.DockerContainerId) + c.Logger.Printf("[DEBUG] agent: pausing %v before first invocation of %s -c %s in container %s", initialPauseTime, c.Shell, c.Script, c.DockerContainerID) next := time.After(initialPauseTime) for { select { @@ -595,7 +595,7 @@ func (c *CheckDocker) check() { AttachStderr: true, Tty: false, Cmd: c.cmd, - Container: c.DockerContainerId, + Container: c.DockerContainerID, } var ( exec *docker.Exec diff --git a/command/agent/check_test.go b/command/agent/check_test.go index 3736f8d3ac..95045d9cff 100644 --- a/command/agent/check_test.go +++ b/command/agent/check_test.go @@ -535,7 +535,7 @@ func expectDockerCheckStatus(t *testing.T, dockerClient DockerClient, status str Notify: mock, CheckID: "foo", Script: "/health.sh", - DockerContainerId: "54432bad1fc7", + DockerContainerID: "54432bad1fc7", Shell: "/bin/sh", Interval: 10 * time.Millisecond, Logger: log.New(os.Stderr, "", log.LstdFlags), @@ -595,7 +595,7 @@ func TestDockerCheckDefaultToSh(t *testing.T) { Notify: mock, CheckID: "foo", Script: "/health.sh", - DockerContainerId: "54432bad1fc7", + DockerContainerID: "54432bad1fc7", Interval: 10 * time.Millisecond, Logger: log.New(os.Stderr, "", log.LstdFlags), dockerClient: &fakeDockerClientWithNoErrors{}, @@ -620,7 +620,7 @@ func TestDockerCheckUseShellFromEnv(t *testing.T) { Notify: mock, CheckID: "foo", Script: "/health.sh", - DockerContainerId: "54432bad1fc7", + DockerContainerID: "54432bad1fc7", Interval: 10 * time.Millisecond, Logger: log.New(os.Stderr, "", log.LstdFlags), dockerClient: &fakeDockerClientWithNoErrors{}, @@ -645,7 +645,7 @@ func TestDockerCheckTruncateOutput(t *testing.T) { Notify: mock, CheckID: "foo", Script: "/health.sh", - DockerContainerId: "54432bad1fc7", + DockerContainerID: "54432bad1fc7", Shell: "/bin/sh", Interval: 10 * time.Millisecond, Logger: log.New(os.Stderr, "", log.LstdFlags), diff --git a/command/agent/config.go b/command/agent/config.go index 3577a18c30..c03659116f 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -782,7 +782,7 @@ func FixupCheckType(raw interface{}) error { rawMap["serviceid"] = v delete(rawMap, "service_id") case "docker_container_id": - rawMap["DockerContainerId"] = v + rawMap["DockerContainerID"] = v delete(rawMap, "docker_container_id") } } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 8fb1ae370e..000567b265 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -1133,7 +1133,7 @@ func TestDecodeConfig_Check(t *testing.T) { t.Fatalf("bad: %v", chk) } - if chk.DockerContainerId != "redis" { + if chk.DockerContainerID != "redis" { t.Fatalf("bad: %v", chk) } } diff --git a/website/source/docs/agent/http/agent.html.markdown b/website/source/docs/agent/http/agent.html.markdown index d12e5a7aa9..5a7b85c52f 100644 --- a/website/source/docs/agent/http/agent.html.markdown +++ b/website/source/docs/agent/http/agent.html.markdown @@ -241,6 +241,8 @@ body must look like: "Name": "Memory utilization", "Notes": "Ensure we don't oversubscribe memory", "Script": "/usr/local/bin/check_mem.py", + "DockerContainerID": "f972c95ebf0e", + "Shell": "/bin/bash", "HTTP": "http://example.com", "TCP": "example.com:22", "Interval": "10s", @@ -259,6 +261,10 @@ The `Notes` field is not used internally by Consul and is meant to be human-read If a `Script` is provided, the check type is a script, and Consul will evaluate the script every `Interval` to update the status. +If a `DockerContainerID` is provided, the check is a Docker check, and Consul will +evaluate the script every `Interval` in the given container using the specified +`Shell`. Note that `Shell` is currently only supported for Docker checks. + An `HTTP` check will perform an HTTP GET request against the value of `HTTP` (expected to be a URL) every `Interval`. If the response is any `2xx` code, the check is `passing`. If the response is `429 Too Many Requests`, the check is `warning`. Otherwise, the check From ab63122a320928b290d681aaac1145b19a69cc0d Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 18 Nov 2015 14:41:50 -0800 Subject: [PATCH 074/123] Removes old logging doc which was empty. --- website/source/docs/agent/logging.html.markdown | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 website/source/docs/agent/logging.html.markdown diff --git a/website/source/docs/agent/logging.html.markdown b/website/source/docs/agent/logging.html.markdown deleted file mode 100644 index f7aedcf07c..0000000000 --- a/website/source/docs/agent/logging.html.markdown +++ /dev/null @@ -1,11 +0,0 @@ ---- -layout: "docs" -page_title: "Logging" -sidebar_current: "docs-agent" -description: |- - TODO ---- - -# Logging - -TODO From d861f87851057522e70ee9800538ecefb6018bf3 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Fri, 20 Nov 2015 21:44:24 -0800 Subject: [PATCH 075/123] Fixes a typo in the prepared queries doc. --- website/source/docs/agent/http/query.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/agent/http/query.html.markdown b/website/source/docs/agent/http/query.html.markdown index 4f352435e0..ad6d65838c 100644 --- a/website/source/docs/agent/http/query.html.markdown +++ b/website/source/docs/agent/http/query.html.markdown @@ -313,7 +313,7 @@ for context in case an empty list of nodes is returned. copy of the structure given when the prepared query was created. `Datacenter` has the datacenter that ultimately provided the list of nodes -and `Failvovers` has the number of remote datacenters that were queried +and `Failovers` has the number of remote datacenters that were queried while executing the query. This provides some insight into where the data came from. This will be zero during non-failover operations where there were healthy nodes found in the local datacenter. From afc77a46340025baf61d61ea9990eb2b199de69e Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Mon, 23 Nov 2015 15:01:24 +0100 Subject: [PATCH 076/123] Include 'Service'/'Address' field in example Add the `Service`/`Address` field to the example output for the `/v1/health/service/\` endpoint. Even though it's an optional value, this is probably the one consumers are looking for (rather than the `Node` address) --- website/source/docs/agent/http/health.html.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/website/source/docs/agent/http/health.html.markdown b/website/source/docs/agent/http/health.html.markdown index 6e8ba7b1a6..7cfb769dd1 100644 --- a/website/source/docs/agent/http/health.html.markdown +++ b/website/source/docs/agent/http/health.html.markdown @@ -133,6 +133,7 @@ It returns a JSON body like this: "ID": "redis", "Service": "redis", "Tags": null, + "Address": "10.1.10.12" "Port": 8000 }, "Checks": [ From 4d42ff66e304e3f09eaae621ea4b0792e435064a Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Mon, 23 Nov 2015 09:39:05 -0500 Subject: [PATCH 077/123] Update health.html.markdown Correct json syntax in example. --- website/source/docs/agent/http/health.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/agent/http/health.html.markdown b/website/source/docs/agent/http/health.html.markdown index 7cfb769dd1..7aee45933b 100644 --- a/website/source/docs/agent/http/health.html.markdown +++ b/website/source/docs/agent/http/health.html.markdown @@ -133,7 +133,7 @@ It returns a JSON body like this: "ID": "redis", "Service": "redis", "Tags": null, - "Address": "10.1.10.12" + "Address": "10.1.10.12", "Port": 8000 }, "Checks": [ From 7890a3dda8ef3e2a885b2a7be9095571118f2d55 Mon Sep 17 00:00:00 2001 From: Craig Wickesser Date: Mon, 23 Nov 2015 13:03:38 -0500 Subject: [PATCH 078/123] Fix version support Updated the version that the library supports. --- api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/README.md b/api/README.md index 3b7c89ddbb..937e99d9f6 100644 --- a/api/README.md +++ b/api/README.md @@ -4,7 +4,7 @@ Consul API client This package provides the `api` package which attempts to provide programmatic access to the full Consul API. -Currently, all of the Consul APIs included in version 0.3 are supported. +Currently, all of the Consul APIs included in version 0.5.2 are supported. Documentation ============= From 58217f8f10a06b00df78c9929624636a893b8dc3 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Mon, 23 Nov 2015 10:46:14 -0800 Subject: [PATCH 079/123] Switches to curl with certificate checking. --- Vagrantfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 91e5760bda..481dd4a820 100755 --- a/Vagrantfile +++ b/Vagrantfile @@ -14,11 +14,11 @@ ARCH=`uname -m | sed 's|i686|386|' | sed 's|x86_64|amd64|'` # Install Go sudo apt-get update -sudo apt-get install -y build-essential git-core zip +sudo apt-get install -y build-essential git-core zip curl # Install Go cd /tmp -wget --quiet --no-check-certificate https://storage.googleapis.com/golang/go${GOVERSION}.linux-${ARCH}.tar.gz +curl -s -O https://storage.googleapis.com/golang/go${GOVERSION}.linux-${ARCH}.tar.gz tar -xvf go${GOVERSION}.linux-${ARCH}.tar.gz sudo mv go $SRCROOT sudo chmod 775 $SRCROOT From a61d89d0e66c6698d4b166aa457bf020d5c383cc Mon Sep 17 00:00:00 2001 From: James Phillips Date: Wed, 25 Nov 2015 17:59:16 -0800 Subject: [PATCH 080/123] Removes the GOMAXPROCS warnings which are obsolete for Go 1.5+. --- command/agent/command.go | 5 ----- command/info.go | 15 --------------- .../intro/getting-started/agent.html.markdown | 1 - 3 files changed, 21 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 165a73994a..0993ccfa15 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -565,11 +565,6 @@ func (c *Command) Run(args []string) int { return 1 } - // Check GOMAXPROCS - if runtime.GOMAXPROCS(0) == 1 { - c.Ui.Error("WARNING: It is highly recommended to set GOMAXPROCS higher than 1") - } - // Setup the log outputs logGate, logWriter, logOutput := c.setupLoggers(config) if logWriter == nil { diff --git a/command/info.go b/command/info.go index 8d9bad776d..b69f35ee8b 100644 --- a/command/info.go +++ b/command/info.go @@ -48,21 +48,6 @@ func (i *InfoCommand) Run(args []string) int { return 1 } - // Check for specific warnings - didWarn := false - runtime, ok := stats["runtime"] - if ok { - if maxprocs := runtime["max_procs"]; maxprocs == "1" { - i.Ui.Output("WARNING: It is highly recommended to set GOMAXPROCS higher than 1") - didWarn = true - } - } - - // Add a blank line if there are any warnings - if didWarn { - i.Ui.Output("") - } - // Get the keys in sorted order keys := make([]string, 0, len(stats)) for key := range stats { diff --git a/website/source/intro/getting-started/agent.html.markdown b/website/source/intro/getting-started/agent.html.markdown index e745fd3790..28ddff23db 100644 --- a/website/source/intro/getting-started/agent.html.markdown +++ b/website/source/intro/getting-started/agent.html.markdown @@ -28,7 +28,6 @@ For simplicity, we'll run a single Consul agent in server mode: $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul ==> WARNING: BootstrapExpect Mode is specified as 1; this is the same as Bootstrap mode. ==> WARNING: Bootstrap mode enabled! Do not enable unless necessary -==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1 ==> Starting Consul agent... ==> Starting Consul agent RPC... ==> Consul agent running! From 847343d801381be358d6129afcfb17d05689eb50 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 27 Nov 2015 17:20:57 -0800 Subject: [PATCH 081/123] consul: shrink yamux recv buffer on idle streams --- consul/pool.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/consul/pool.go b/consul/pool.go index 7254ded151..4abecbf4bf 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -103,6 +103,12 @@ func (c *Conn) returnClient(client *StreamClient) { if c.clients.Len() < c.pool.maxStreams && atomic.LoadInt32(&c.shouldClose) == 0 { c.clients.PushFront(client) didSave = true + + // If this is a Yamux stream, shrink the internal buffers so that + // we can GC the idle memory + if ys, ok := client.stream.(*yamux.Stream); ok { + ys.Shrink() + } } c.clientLock.Unlock() if !didSave { From bd3f7328b1e98a62fe6e061fbcbc1911c2203181 Mon Sep 17 00:00:00 2001 From: Nguyen Sy Thanh Son Date: Sat, 28 Nov 2015 22:52:40 +0700 Subject: [PATCH 082/123] Make the packages up to date --- demo/vagrant-cluster/Vagrantfile | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/vagrant-cluster/Vagrantfile b/demo/vagrant-cluster/Vagrantfile index 9aabc8c96c..1ac03068f5 100644 --- a/demo/vagrant-cluster/Vagrantfile +++ b/demo/vagrant-cluster/Vagrantfile @@ -3,6 +3,7 @@ $script = <