Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consul prepared queries #1389

Merged
merged 55 commits into from
Nov 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
ce0881a
Adds a new management ACL for prepared queries.
Nov 4, 2015
7babcef
Changes structs and state store for prepared queries.
Nov 7, 2015
989619c
Moves DNS over to new shuffle and filter functions.
Nov 7, 2015
1f87480
Adds a better shuffle test (similar to DNS).
Nov 7, 2015
1d1865d
Factors code for pulling the sorted list of DCs into a common place.
Nov 7, 2015
b736bc4
Adds basic structure for prepared queries (needs tests).
Nov 7, 2015
81bb397
Adds prefix "prepared" to everything prepared query-related.
Nov 10, 2015
c41a3d6
Changes "not" prefix from "~" to "!".
Nov 10, 2015
7ca3f0a
Adds an explicit ACL check that will fail vs. trying other DCs.
Nov 10, 2015
2f34b51
Moves sort to a query-time decision and adds back the limit.
Nov 10, 2015
666619d
Skips unknown DCs during queries and chugs along in the face of errors.
Nov 10, 2015
0bd7e82
Clarifies comment about name vs. ID.
Nov 10, 2015
58bb6e8
Checks for valid UUIDs before calling in to index function.
Nov 10, 2015
333da2a
Adds lookup and list endpoints and basic end-to-end apply test.
Nov 10, 2015
8222d3f
Completes non-ACL version of apply test.
Nov 10, 2015
d4d866c
Adds ACL cases for apply.
Nov 10, 2015
519666a
Adds query parsing unit tests.
Nov 10, 2015
fa414a2
Adds tests for query lookup and list endpoints.
Nov 10, 2015
7ded6c7
Adds a leader forwarding case for prepared queries.
Nov 10, 2015
a9e9d5e
Adds execute leader forward test for prepared queries.
Nov 10, 2015
86ead89
Removes unused ACL filter.
Nov 11, 2015
30a1822
Adds status information about failovers to query results.
Nov 11, 2015
1417053
Adds execute tests for prepared queries.
Nov 11, 2015
eefdb56
Adds tag filter tests.
Nov 11, 2015
bbc5185
Adds a test for the server wrapper.
Nov 11, 2015
d06e2a5
Adds test for remote datacenter selection and query logic.
Nov 11, 2015
a57d642
Always increments the failovers counter, even for error-ed DCs.
Nov 11, 2015
7af41ed
Changes Lookup to Get since we don't need it (only Execute does).
Nov 11, 2015
5d06a87
Adds an RPC endpoint injection method for testing.
Nov 12, 2015
57be551
Adds an HTTP endpoint for prepared queries.
Nov 12, 2015
8e1bea0
Completes FSM support for prepared queries.
Nov 12, 2015
34b685c
Adds a unit test for the new RTT getDatacentersByDistance fn.
Nov 12, 2015
c955799
Makes an empty prepared query list an empty slice, not a nil one.
Nov 12, 2015
6634cd6
Adds query metadata to prepared query execute response.
Nov 12, 2015
da20e66
Adds a note about obfuscating query name/ID from the logs.
Nov 12, 2015
5e7523e
Adds a slightly more flexible mock system so we can test DNS.
Nov 12, 2015
4a0a60a
Adds DNS support for prepared queries (needs tests).
Nov 12, 2015
81b4313
Adds unit tests for prepared queries and DNS, using existing tests fo…
Nov 13, 2015
e9480ec
Plumbs the service name back and uses agent-specific TTL settings as …
Nov 13, 2015
67fd4fa
Returns a 404 from a get or execute of a nonexistent query.
Nov 13, 2015
4715c04
Adds a test to make sure a stale retry terminates.
Nov 14, 2015
f60fc87
Gets rid of some unused constants.
Nov 14, 2015
712a3db
Adds API client support for prepared queries.
Nov 14, 2015
800e946
Adds and updates docs for prepared queries.
Nov 14, 2015
162c6ba
Updates the changelog.
Nov 14, 2015
46d5afa
Adds a test to ensure we don't return a nil slice.
Nov 15, 2015
b8ddb21
Adds a paranoia set of the nodes slice to nil.
Nov 15, 2015
e1ce1a3
Moves conversion of nil slices up to HTTP layer for prepared queries.
Nov 15, 2015
06b918e
Makes UUID regex case-insensitive.
Nov 17, 2015
8fc6a6a
Switches to helpers for prepared query API wrappers.
Nov 17, 2015
1059a8b
Removes a useless empty import and fixes some stale comments.
Nov 17, 2015
cd6be4a
Avoids taking the length again when parsing DNS queries.
Nov 17, 2015
049da2c
Breaks up huge HTTP endpoint functions.
Nov 17, 2015
533e6fc
Returns a zero index for a lookup error case.
Nov 17, 2015
e3ef204
Makes all the query ops the correct type.
Nov 17, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
24 changes: 24 additions & 0 deletions acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
32 changes: 32 additions & 0 deletions acl/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
173 changes: 173 additions & 0 deletions api/prepared_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package api

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may find the answer to this as I get further in, but the user should be required to pass a valid token with privileges to modify at modification/removal time as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - this should be enforced down in PreparedQuery.Apply().


// 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) {
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) {
var out []*PreparedQueryDefinition
qm, err := c.c.query("/v1/query", &out, q)
if 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) {
var out []*PreparedQueryDefinition
qm, err := c.c.query("/v1/query/"+queryID, &out, q)
if 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) {
var out *PreparedQueryExecuteResponse
qm, err := c.c.query("/v1/query/"+queryIDOrName+"/execute", &out, q)
if err != nil {
return nil, nil, err
}
return out, qm, nil
}
Loading