Merge branch 'master' into f-cli-rework

This commit is contained in:
Kyle Havlovitz 2017-02-06 13:46:44 -05:00
commit 3d09fb880f
No known key found for this signature in database
GPG Key ID: 8A5E6B173056AD6C
852 changed files with 304878 additions and 9655 deletions

View File

@ -1,14 +1,13 @@
language: go
go:
- 1.6.3
- 1.7.5
branches:
only:
- master
install: make
script:
- make test
- make ci
sudo: false

View File

@ -1,23 +1,115 @@
## 0.7.1 (UNRELEASED)
## 0.7.4 (February 6, 2017)
BACKWARDS INCOMPATIBILITIES:
IMPROVEMENTS:
* agent: Integrated gopsutil library to use built in host UUID as node ID, if available, instead of a randomly generated UUID. This makes it easier for other applications on the same host to generate the same node ID without coordinating with Consul. [GH-2697]
* agent: Added a configuration option, `tls_min_version`, for setting the minimum allowed TLS version used for the HTTP API and RPC. [GH-2699]
* agent: Added a `relay-factor` option to keyring operations to allow nodes to relay their response through N randomly-chosen other nodes in the cluster. [GH-2704]
* build: Consul is now built with Go 1.7.5. [GH-2682]
* dns: Add ability to lookup Consul agents by either their Node ID or Node Name through the node interface (e.g. DNS `(node-id|node-name).node.consul`). [GH-2702]
BUG FIXES:
* dns: Fixed an issue where SRV lookups for services on a node registered with non-IP addresses were missing the CNAME record in the additional section of the response. [GH-2695]
## 0.7.3 (January 26, 2017)
FEATURES:
* **KV Import/Export CLI:** `consul kv export` and `consul kv import` can be used to move parts of the KV tree between disconnected consul clusters, using JSON as the intermediate representation. [GH-2633]
* **Node Metadata:** Support for assigning user-defined metadata key/value pairs to nodes has been added. This can be viewed when looking up node info, and can be used to filter the results of various catalog and health endpoints. For more information, see the [Catalog](https://www.consul.io/docs/agent/http/catalog.html), [Health](https://www.consul.io/docs/agent/http/health.html), and [Prepared Query](https://www.consul.io/docs/agent/http/query.html) endpoint documentation, as well as the [Node Meta](https://www.consul.io/docs/agent/options.html#_node_meta) section of the agent configuration. [GH-2654]
* **Node Identifiers:** Consul agents can now be configured with a unique identifier, or they will generate one at startup that will persist across agent restarts. This identifier is designed to represent a node across all time, even if the name or address of the node changes. Identifiers are currently only exposed in node-related endpoints, but they will be used in future versions of Consul to help manage Consul servers and the Raft quorum in a more robust manner, as the quorum is currently tracked via addresses, which can change. [GH-2661]
* **Improved Blocking Queries:** Consul's [blocking query](https://www.consul.io/docs/agent/http.html#blocking-queries) implementation was improved to provide a much more fine-grained mechanism for detecting changes. For example, in previous versions of Consul blocking to wait on a change to a specific service would result in a wake up if any service changed. Now, wake ups are scoped to the specific service being watched, if possible. This support has been added to all endpoints that support blocking queries, nothing new is required to take advantage of this feature. [GH-2671]
* **GCE auto-discovery:** New `-retry-join-gce` configuration options added to allow bootstrapping by automatically discovering Google Cloud instances with a given tag at startup. [GH-2570]
IMPROVEMENTS:
* build: Consul is now built with Go 1.7.4. [GH-2676]
* cli: `consul kv get` now has a `-base64` flag to base 64 encode the value. [GH-2631]
* cli: `consul kv put` now has a `-base64` flag for setting values which are base 64 encoded. [GH-2632]
* ui: Added a notice that JS is required when viewing the web UI with JS disabled. [GH-2636]
BUG FIXES:
* agent: Redacted the AWS access key and secret key ID from the /v1/agent/self output so they are not disclosed. [GH-2677]
* agent: Fixed a rare startup panic due to a Raft/Serf race condition. [GH-1899]
* cli: Fixed a panic when an empty quoted argument was given to `consul kv put`. [GH-2635]
* tests: Fixed a race condition with check mock's map usage. [GH-2578]
## 0.7.2 (December 19, 2016)
FEATURES:
* **Keyring API:** A new `/v1/operator/keyring` HTTP endpoint was added that allows for performing operations such as list, install, use, and remove on the encryption keys in the gossip keyring. See the [Keyring Endpoint](https://www.consul.io/docs/agent/http/operator.html#keyring) for more details. [GH-2509]
* **Monitor API:** A new `/v1/agent/monitor` HTTP endpoint was added to allow for viewing streaming log output from the agent, similar to the `consul monitor` command. See the [Monitor Endpoint](https://www.consul.io/docs/agent/http/agent.html#agent_monitor) for more details. [GH-2511]
* **Reload API:** A new `/v1/agent/reload` HTTP endpoint was added for triggering a reload of the agent's configuration. See the [Reload Endpoint](https://www.consul.io/docs/agent/http/agent.html#agent_reload) for more details. [GH-2516]
* **Leave API:** A new `/v1/agent/leave` HTTP endpoint was added for causing an agent to gracefully shutdown and leave the cluster (previously, only `force-leave` was present in the HTTP API). See the [Leave Endpoint](https://www.consul.io/docs/agent/http/agent.html#agent_leave) for more details. [GH-2516]
* **Bind Address Templates (beta):** Consul agents now allow [go-sockaddr/template](https://godoc.org/github.com/hashicorp/go-sockaddr/template) syntax to be used for any bind address configuration (`advertise_addr`, `bind_addr`, `client_addr`, and others). This allows for easy creation of immutable images for Consul that can fetch their own address based on an interface name, network CIDR, address family from an actual RFC number, and many other possible schemes. This feature is in beta and we may tweak the template syntax before final release, but we encourage the community to try this and provide feedback. [GH-2563]
* **Complete ACL Coverage (beta):** Consul 0.8 will feature complete ACL coverage for all of Consul. To ease the transition to the new policies, a beta version of complete ACL support was added to help with testing and migration to the new features. Please see the [ACLs Internals Guide](https://www.consul.io/docs/internals/acl.html#version_8_acls) for more details. [GH-2594, GH-2592, GH-2590]
IMPROVEMENTS:
* agent: Defaults to `?pretty` JSON for HTTP API requests when in `-dev` mode. [GH-2518]
* agent: Updated Circonus metrics library and added new Circonus configration options for Consul for customizing check display name and tags. [GH-2555]
* agent: Added a checksum to UDP gossip messages to guard against packet corruption. [GH-2574]
* agent: Check whether a snapshot needs to be taken more often (every 5 seconds instead of 2 minutes) to keep the raft file smaller and to avoid doing huge truncations when writing lots of entries very quickly. [GH-2591]
* agent: Allow gossiping to suspected/recently dead nodes. [GH-2593]
* agent: Changed the gossip suspicion timeout to grow smoothly as the number of nodes grows. [GH-2593]
* agent: Added a deprecation notice for Atlas features to the CLI and docs. [GH-2597]
* agent: Give a better error message when the given data-dir is not a directory. [GH-2529]
BUG FIXES:
* agent: Fixed a panic when SIGPIPE signal was received. [GH-2404]
* api: Added missing Raft index fields to `CatalogService` structure. [GH-2366]
* api: Added missing notes field to `AgentServiceCheck` structure. [GH-2336]
* api: Changed type of `AgentServiceCheck.TLSSkipVerify` from `string` to `bool`. [GH-2530]
* api: Added new `HealthChecks.AggregatedStatus()` method that makes it easy get an overall health status from a list of checks. [GH-2544]
* api: Changed type of `KVTxnOp.Verb` from `string` to `KVOp`. [GH-2531]
* cli: Fixed an issue with the `consul kv put` command where a negative value would be interpreted as an argument to read from standard input. [GH-2526]
* ui: Fixed an issue where extra commas would be shown around service tags. [GH-2340]
* ui: Customized Bootstrap config to avoid missing font file references. [GH-2485]
* ui: Removed "Deregister" button as removing nodes from the catalog isn't a common operation and leads to lots of user confusion. [GH-2541]
## 0.7.1 (November 10, 2016)
BREAKING CHANGES:
* Child process reaping support has been removed, along with the `reap` configuration option. Reaping is also done via [dumb-init](https://github.com/Yelp/dumb-init) in the [Consul Docker image](https://github.com/hashicorp/docker-consul), so removing it from Consul itself simplifies the code and eases future maintainence for Consul. If you are running Consul as PID 1 in a container you will need to arrange for a wrapper process to reap child processes. [GH-1988]
* The default for `max_stale` has been increased to a near-indefinite threshold (10 years) to allow DNS queries to continue to be served in the event of a long outage with no leader. A new telemetry counter has also been added at `consul.dns.stale_queries` to track when agents serve DNS queries that are over a certain staleness (>5 seconds). [GH-2481]
* The api package's `PreparedQuery.Delete()` method now takes `WriteOptions` instead of `QueryOptions`. [GH-2417]
FEATURES:
* **Key/Value Store Command Line Interface:** New `consul kv` commands were added for easy access to all basic key/value store operations. [GH-2360]
* **Snapshot/Restore:** A new /v1/snapshot HTTP endpoint and corresponding set of `consul snapshot` commands were added for easy point-in-time snapshots for disaster recovery. Snapshots include all state managed by Consul's Raft [consensus protocol](/docs/internals/consensus.html), including Key/Value Entries, Service Catalog, Prepared Queries, Sessions, and ACLs. Snapshots can be restored on the fly into a completely fresh cluster. [GH-2396]
* **AWS auto-discovery:** New `-retry-join-ec2` configuration options added to allow bootstrapping by automatically discovering AWS instances with a given tag key/value at startup. [GH-2459]
IMPROVEMENTS:
* api: All session options can now be set when using `api.Lock()`. [GH-2372]
* agent: Added the ability to bind Serf WAN and LAN to different interfaces than the general bind address. [GH-2007]
* agent: Added a new `tls_skip_verify` configuration option for HTTP checks. [GH-1984]
* build: Consul is now built with Go 1.7.3. [GH-2281]
BUG FIXES:
* agent: Fixed a Go race issue with log buffering at startup. [GH-2262]
* agent: Fixed a panic during anti-entropy sync for services and checks. [GH-2125]
* agent: Fixed an issue on Windows where "wsarecv" errors were logged when CLI commands accessed the RPC interface. [GH-2356]
* agent: Syslog initialization will now retry on errors for up to 60 seconds to avoid a race condition at system startup. [GH-1610]
* agent: Fixed a panic when both -dev and -bootstrap-expect flags were provided. [GH-2464]
* agent: Added a retry with backoff when a session fails to invalidate after expiring. [GH-2435]
* agent: Fixed an issue where Consul would fail to start because of leftover malformed check/service state files. [GH-1221]
* agent: Fixed agent crashes on macOS Sierra by upgrading Go. [GH-2407, GH-2281]
* agent: Log a warning instead of success when attempting to deregister a nonexistent service. [GH-2492]
* api: Trim leading slashes from keys/prefixes when querying KV endpoints to avoid a bug with redirects in Go 1.7 (golang/go#4800). [GH-2403]
* dns: Fixed external services that pointed to consul addresses (CNAME records) not resolving to A-records. [GH-1228]
* dns: Fixed an issue with SRV lookups where the service address was different from the node's. [GH-832]
* dns: Fixed an issue where truncated records from a recursor query were improperly reported as errors. [GH-2384]
* server: Fixed the port numbers in the sample JSON inside peers.info. [GH-2391]
* server: Squashes ACL datacenter name to lower case and checks for proper formatting at startup. [GH-2059, GH-1778, GH-2478]
* ui: Fixed an XSS issue with the display of sessions and ACLs in the web UI. [GH-2456]
## 0.7.0 (September 14, 2016)

View File

@ -4,23 +4,31 @@ GOTOOLS = \
github.com/mitchellh/gox \
golang.org/x/tools/cmd/cover \
golang.org/x/tools/cmd/stringer
PACKAGES=$(shell go list ./... | grep -v '^github.com/hashicorp/consul/vendor/')
PACKAGES=$(shell go list ./... | grep -v '/vendor/')
VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods \
-nilfunc -printf -rangeloops -shift -structtags -unsafeptr
VERSION?=$(shell awk -F\" '/^const Version/ { print $$2; exit }' version.go)
BUILD_TAGS?=consul
# all builds binaries for all targets
all: tools
all: bin
ci:
if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then \
$(MAKE) bin ;\
fi
@$(MAKE) test
bin: tools
@mkdir -p bin/
@sh -c "'$(CURDIR)/scripts/build.sh'"
@BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'"
# dev creates binaries for testing locally - these are put into ./bin and $GOPATH
dev: format
@CONSUL_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'"
@CONSUL_DEV=1 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'"
# dist builds binaries for all platforms and packages them for distribution
dist:
@sh -c "'$(CURDIR)/scripts/dist.sh' $(VERSION)"
@BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/dist.sh'"
cov:
gocov test ./... | gocov-html > /tmp/coverage.html
@ -29,7 +37,7 @@ cov:
test: format
@$(MAKE) vet
@./scripts/verify_no_uuid.sh
@./scripts/test.sh
@BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/test.sh'"
cover:
go list ./... | xargs -n1 go test --cover
@ -41,7 +49,7 @@ format:
vet:
@echo "--> Running go tool vet $(VETARGS) ."
@go list ./... \
| grep -v ^github.com/hashicorp/consul/vendor/ \
| grep -v /vendor/ \
| cut -d '/' -f 4- \
| xargs -n1 \
go tool vet $(VETARGS) ;\
@ -51,6 +59,11 @@ vet:
echo "and fix them if necessary before submitting the code for reviewal."; \
fi
# build the static web ui and build static assets inside a Docker container, the
# same way a release build works
ui:
@sh -c "'$(CURDIR)/scripts/ui.sh'"
# generates the static web ui that's compiled into the binary
static-assets:
@echo "--> Generating static assets"
@ -61,4 +74,4 @@ static-assets:
tools:
go get -u -v $(GOTOOLS)
.PHONY: all bin dev dist cov test cover format vet static-assets tools
.PHONY: all ci bin dev dist cov test cover format vet ui static-assets tools

View File

@ -1,4 +1,4 @@
# Consul [![Build Status](https://travis-ci.org/hashicorp/consul.png)](https://travis-ci.org/hashicorp/consul) [![Join the chat at https://gitter.im/hashicorp-consul/Lobby](https://badges.gitter.im/hashicorp-consul/Lobby.svg)](https://gitter.im/hashicorp-consul/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# Consul [![Build Status](https://travis-ci.org/hashicorp/consul.svg?branch=master)](https://travis-ci.org/hashicorp/consul) [![Join the chat at https://gitter.im/hashicorp-consul/Lobby](https://badges.gitter.im/hashicorp-consul/Lobby.svg)](https://gitter.im/hashicorp-consul/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
* Website: https://www.consul.io
* Chat: [Gitter](https://gitter.im/hashicorp-consul/Lobby)
@ -42,7 +42,7 @@ https://www.consul.io/docs
## Developing Consul
If you wish to work on Consul itself, you'll first need [Go](https://golang.org)
installed (version 1.6+ is _required_). Make sure you have Go properly installed,
installed (version 1.7+ is _required_). Make sure you have Go properly installed,
including setting up your [GOPATH](https://golang.org/doc/code.html#GOPATH).
Next, clone this repository into `$GOPATH/src/github.com/hashicorp/consul` and
@ -64,7 +64,7 @@ format the code according to Go standards.
### Building Consul on Windows
Make sure Go 1.6+ is installed on your system and that the Go command is in your
Make sure Go 1.7+ is installed on your system and that the Go command is in your
%PATH%.
For building Consul on Windows, you also need to have MinGW installed.

View File

@ -35,6 +35,26 @@ func init() {
// ACL is the interface for policy enforcement.
type ACL interface {
// ACLList checks for permission to list all the ACLs
ACLList() bool
// ACLModify checks for permission to manipulate ACLs
ACLModify() bool
// AgentRead checks for permission to read from agent endpoints for a
// given node.
AgentRead(string) bool
// AgentWrite checks for permission to make changes via agent endpoints
// for a given node.
AgentWrite(string) bool
// EventRead determines if a specific event can be queried.
EventRead(string) bool
// EventWrite determines if a specific event may be fired.
EventWrite(string) bool
// KeyRead checks for permission to read a given key
KeyRead(string) bool
@ -46,26 +66,6 @@ type ACL interface {
// that deny a write.
KeyWritePrefix(string) bool
// ServiceWrite checks for permission to read a given service
ServiceWrite(string) bool
// ServiceRead checks for permission to read a given service
ServiceRead(string) bool
// EventRead determines if a specific event can be queried.
EventRead(string) bool
// EventWrite determines if a specific event may be fired.
EventWrite(string) bool
// PrepardQueryRead determines if a specific prepared query can be read
// to show its contents (this is not used for execution).
PreparedQueryRead(string) bool
// PreparedQueryWrite determines if a specific prepared query can be
// created, modified, or deleted.
PreparedQueryWrite(string) bool
// KeyringRead determines if the encryption keyring used in
// the gossip layer can be read.
KeyringRead() bool
@ -73,6 +73,13 @@ type ACL interface {
// KeyringWrite determines if the keyring can be manipulated
KeyringWrite() bool
// NodeRead checks for permission to read (discover) a given node.
NodeRead(string) bool
// NodeWrite checks for permission to create or update (register) a
// given node.
NodeWrite(string) bool
// OperatorRead determines if the read-only Consul operator functions
// can be used.
OperatorRead() bool
@ -81,11 +88,30 @@ type ACL interface {
// functions can be used.
OperatorWrite() bool
// ACLList checks for permission to list all the ACLs
ACLList() bool
// PrepardQueryRead determines if a specific prepared query can be read
// to show its contents (this is not used for execution).
PreparedQueryRead(string) bool
// ACLModify checks for permission to manipulate ACLs
ACLModify() bool
// PreparedQueryWrite determines if a specific prepared query can be
// created, modified, or deleted.
PreparedQueryWrite(string) bool
// ServiceRead checks for permission to read a given service
ServiceRead(string) bool
// ServiceWrite checks for permission to create or update a given
// service
ServiceWrite(string) bool
// SessionRead checks for permission to read sessions for a given node.
SessionRead(string) bool
// SessionWrite checks for permission to create sessions for a given
// node.
SessionWrite(string) bool
// Snapshot checks for permission to take and restore snapshots.
Snapshot() bool
}
// StaticACL is used to implement a base ACL policy. It either
@ -96,6 +122,30 @@ type StaticACL struct {
defaultAllow bool
}
func (s *StaticACL) ACLList() bool {
return s.allowManage
}
func (s *StaticACL) ACLModify() bool {
return s.allowManage
}
func (s *StaticACL) AgentRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) AgentWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) EventRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) EventWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) KeyRead(string) bool {
return s.defaultAllow
}
@ -108,30 +158,6 @@ func (s *StaticACL) KeyWritePrefix(string) bool {
return s.defaultAllow
}
func (s *StaticACL) ServiceRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) ServiceWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) EventRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) EventWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) PreparedQueryRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) PreparedQueryWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) KeyringRead() bool {
return s.defaultAllow
}
@ -140,6 +166,14 @@ func (s *StaticACL) KeyringWrite() bool {
return s.defaultAllow
}
func (s *StaticACL) NodeRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) NodeWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) OperatorRead() bool {
return s.defaultAllow
}
@ -148,11 +182,31 @@ func (s *StaticACL) OperatorWrite() bool {
return s.defaultAllow
}
func (s *StaticACL) ACLList() bool {
return s.allowManage
func (s *StaticACL) PreparedQueryRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) ACLModify() bool {
func (s *StaticACL) PreparedQueryWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) ServiceRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) ServiceWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) SessionRead(string) bool {
return s.defaultAllow
}
func (s *StaticACL) SessionWrite(string) bool {
return s.defaultAllow
}
func (s *StaticACL) Snapshot() bool {
return s.allowManage
}
@ -192,12 +246,21 @@ type PolicyACL struct {
// no matching rule.
parent ACL
// agentRules contains the agent policies
agentRules *radix.Tree
// keyRules contains the key policies
keyRules *radix.Tree
// nodeRules contains the node policies
nodeRules *radix.Tree
// serviceRules contains the service policies
serviceRules *radix.Tree
// sessionRules contains the session policies
sessionRules *radix.Tree
// eventRules contains the user event policies
eventRules *radix.Tree
@ -218,22 +281,40 @@ type PolicyACL struct {
func New(parent ACL, policy *Policy) (*PolicyACL, error) {
p := &PolicyACL{
parent: parent,
agentRules: radix.New(),
keyRules: radix.New(),
nodeRules: radix.New(),
serviceRules: radix.New(),
sessionRules: radix.New(),
eventRules: radix.New(),
preparedQueryRules: radix.New(),
}
// Load the agent policy
for _, ap := range policy.Agents {
p.agentRules.Insert(ap.Node, ap.Policy)
}
// Load the key policy
for _, kp := range policy.Keys {
p.keyRules.Insert(kp.Prefix, kp.Policy)
}
// Load the node policy
for _, np := range policy.Nodes {
p.nodeRules.Insert(np.Name, np.Policy)
}
// Load the service policy
for _, sp := range policy.Services {
p.serviceRules.Insert(sp.Name, sp.Policy)
}
// Load the session policy
for _, sp := range policy.Sessions {
p.sessionRules.Insert(sp.Node, sp.Policy)
}
// Load the event policy
for _, ep := range policy.Events {
p.eventRules.Insert(ep.Event, ep.Policy)
@ -253,6 +334,88 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) {
return p, nil
}
// ACLList checks if listing of ACLs is allowed
func (p *PolicyACL) ACLList() bool {
return p.parent.ACLList()
}
// ACLModify checks if modification of ACLs is allowed
func (p *PolicyACL) ACLModify() bool {
return p.parent.ACLModify()
}
// AgentRead checks for permission to read from agent endpoints for a given
// node.
func (p *PolicyACL) AgentRead(node string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.agentRules.LongestPrefix(node)
if ok {
switch rule {
case PolicyRead, PolicyWrite:
return true
default:
return false
}
}
// No matching rule, use the parent.
return p.parent.AgentRead(node)
}
// AgentWrite checks for permission to make changes via agent endpoints for a
// given node.
func (p *PolicyACL) AgentWrite(node string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.agentRules.LongestPrefix(node)
if ok {
switch rule {
case PolicyWrite:
return true
default:
return false
}
}
// No matching rule, use the parent.
return p.parent.AgentWrite(node)
}
// Snapshot checks if taking and restoring snapshots is allowed.
func (p *PolicyACL) Snapshot() bool {
return p.parent.Snapshot()
}
// EventRead is used to determine if the policy allows for a
// specific user event to be read.
func (p *PolicyACL) EventRead(name string) bool {
// Longest-prefix match on event names
if _, rule, ok := p.eventRules.LongestPrefix(name); ok {
switch rule {
case PolicyRead, PolicyWrite:
return true
default:
return false
}
}
// Nothing matched, use parent
return p.parent.EventRead(name)
}
// EventWrite is used to determine if new events can be created
// (fired) by the policy.
func (p *PolicyACL) EventWrite(name string) bool {
// Longest-prefix match event names
if _, rule, ok := p.eventRules.LongestPrefix(name); ok {
return rule == PolicyWrite
}
// No match, use parent
return p.parent.EventWrite(name)
}
// KeyRead returns if a key is allowed to be read
func (p *PolicyACL) KeyRead(key string) bool {
// Look for a matching rule
@ -320,10 +483,43 @@ func (p *PolicyACL) KeyWritePrefix(prefix string) bool {
return p.parent.KeyWritePrefix(prefix)
}
// ServiceRead checks if reading (discovery) of a service is allowed
func (p *PolicyACL) ServiceRead(name string) bool {
// KeyringRead is used to determine if the keyring can be
// read by the current ACL token.
func (p *PolicyACL) KeyringRead() bool {
switch p.keyringRule {
case PolicyRead, PolicyWrite:
return true
case PolicyDeny:
return false
default:
return p.parent.KeyringRead()
}
}
// KeyringWrite determines if the keyring can be manipulated.
func (p *PolicyACL) KeyringWrite() bool {
if p.keyringRule == PolicyWrite {
return true
}
return p.parent.KeyringWrite()
}
// OperatorRead determines if the read-only operator functions are allowed.
func (p *PolicyACL) OperatorRead() bool {
switch p.operatorRule {
case PolicyRead, PolicyWrite:
return true
case PolicyDeny:
return false
default:
return p.parent.OperatorRead()
}
}
// NodeRead checks if reading (discovery) of a node is allowed
func (p *PolicyACL) NodeRead(name string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name)
_, rule, ok := p.nodeRules.LongestPrefix(name)
if ok {
switch rule {
@ -335,13 +531,13 @@ func (p *PolicyACL) ServiceRead(name string) bool {
}
// No matching rule, use the parent.
return p.parent.ServiceRead(name)
return p.parent.NodeRead(name)
}
// ServiceWrite checks if writing (registering) a service is allowed
func (p *PolicyACL) ServiceWrite(name string) bool {
// NodeWrite checks if writing (registering) a node is allowed
func (p *PolicyACL) NodeWrite(name string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name)
_, rule, ok := p.nodeRules.LongestPrefix(name)
if ok {
switch rule {
@ -353,36 +549,16 @@ func (p *PolicyACL) ServiceWrite(name string) bool {
}
// No matching rule, use the parent.
return p.parent.ServiceWrite(name)
return p.parent.NodeWrite(name)
}
// EventRead is used to determine if the policy allows for a
// specific user event to be read.
func (p *PolicyACL) EventRead(name string) bool {
// Longest-prefix match on event names
if _, rule, ok := p.eventRules.LongestPrefix(name); ok {
switch rule {
case PolicyRead, PolicyWrite:
return true
default:
return false
}
// OperatorWrite determines if the state-changing operator functions are
// allowed.
func (p *PolicyACL) OperatorWrite() bool {
if p.operatorRule == PolicyWrite {
return true
}
// Nothing matched, use parent
return p.parent.EventRead(name)
}
// EventWrite is used to determine if new events can be created
// (fired) by the policy.
func (p *PolicyACL) EventWrite(name string) bool {
// Longest-prefix match event names
if _, rule, ok := p.eventRules.LongestPrefix(name); ok {
return rule == PolicyWrite
}
// No match, use parent
return p.parent.EventWrite(name)
return p.parent.OperatorWrite()
}
// PreparedQueryRead checks if reading (listing) of a prepared query is
@ -423,54 +599,74 @@ func (p *PolicyACL) PreparedQueryWrite(prefix string) bool {
return p.parent.PreparedQueryWrite(prefix)
}
// KeyringRead is used to determine if the keyring can be
// read by the current ACL token.
func (p *PolicyACL) KeyringRead() bool {
switch p.keyringRule {
case PolicyRead, PolicyWrite:
return true
case PolicyDeny:
return false
default:
return p.parent.KeyringRead()
// ServiceRead checks if reading (discovery) of a service is allowed
func (p *PolicyACL) ServiceRead(name string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name)
if ok {
switch rule {
case PolicyRead, PolicyWrite:
return true
default:
return false
}
}
// No matching rule, use the parent.
return p.parent.ServiceRead(name)
}
// KeyringWrite determines if the keyring can be manipulated.
func (p *PolicyACL) KeyringWrite() bool {
if p.keyringRule == PolicyWrite {
return true
// ServiceWrite checks if writing (registering) a service is allowed
func (p *PolicyACL) ServiceWrite(name string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.serviceRules.LongestPrefix(name)
if ok {
switch rule {
case PolicyWrite:
return true
default:
return false
}
}
return p.parent.KeyringWrite()
// No matching rule, use the parent.
return p.parent.ServiceWrite(name)
}
// OperatorRead determines if the read-only operator functions are allowed.
func (p *PolicyACL) OperatorRead() bool {
switch p.operatorRule {
case PolicyRead, PolicyWrite:
return true
case PolicyDeny:
return false
default:
return p.parent.OperatorRead()
// SessionRead checks for permission to read sessions for a given node.
func (p *PolicyACL) SessionRead(node string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.sessionRules.LongestPrefix(node)
if ok {
switch rule {
case PolicyRead, PolicyWrite:
return true
default:
return false
}
}
// No matching rule, use the parent.
return p.parent.SessionRead(node)
}
// OperatorWrite determines if the state-changing operator functions are
// allowed.
func (p *PolicyACL) OperatorWrite() bool {
if p.operatorRule == PolicyWrite {
return true
// SessionWrite checks for permission to create sessions for a given node.
func (p *PolicyACL) SessionWrite(node string) bool {
// Check for an exact rule or catch-all
_, rule, ok := p.sessionRules.LongestPrefix(node)
if ok {
switch rule {
case PolicyWrite:
return true
default:
return false
}
}
return p.parent.OperatorWrite()
}
// ACLList checks if listing of ACLs is allowed
func (p *PolicyACL) ACLList() bool {
return p.parent.ACLList()
}
// ACLModify checks if modification of ACLs is allowed
func (p *PolicyACL) ACLModify() bool {
return p.parent.ACLModify()
// No matching rule, use the parent.
return p.parent.SessionWrite(node)
}

View File

@ -35,16 +35,16 @@ func TestStaticACL(t *testing.T) {
t.Fatalf("expected static")
}
if !all.KeyRead("foobar") {
if all.ACLList() {
t.Fatalf("should not allow")
}
if all.ACLModify() {
t.Fatalf("should not allow")
}
if !all.AgentRead("foobar") {
t.Fatalf("should allow")
}
if !all.KeyWrite("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceRead("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceWrite("foobar") {
if !all.AgentWrite("foobar") {
t.Fatalf("should allow")
}
if !all.EventRead("foobar") {
@ -53,10 +53,10 @@ func TestStaticACL(t *testing.T) {
if !all.EventWrite("foobar") {
t.Fatalf("should allow")
}
if !all.PreparedQueryRead("foobar") {
if !all.KeyRead("foobar") {
t.Fatalf("should allow")
}
if !all.PreparedQueryWrite("foobar") {
if !all.KeyWrite("foobar") {
t.Fatalf("should allow")
}
if !all.KeyringRead() {
@ -65,29 +65,50 @@ func TestStaticACL(t *testing.T) {
if !all.KeyringWrite() {
t.Fatalf("should allow")
}
if !all.NodeRead("foobar") {
t.Fatalf("should allow")
}
if !all.NodeWrite("foobar") {
t.Fatalf("should allow")
}
if !all.OperatorRead() {
t.Fatalf("should allow")
}
if !all.OperatorWrite() {
t.Fatalf("should allow")
}
if all.ACLList() {
t.Fatalf("should not allow")
if !all.PreparedQueryRead("foobar") {
t.Fatalf("should allow")
}
if all.ACLModify() {
if !all.PreparedQueryWrite("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceRead("foobar") {
t.Fatalf("should allow")
}
if !all.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if !all.SessionRead("foobar") {
t.Fatalf("should allow")
}
if !all.SessionWrite("foobar") {
t.Fatalf("should allow")
}
if all.Snapshot() {
t.Fatalf("should not allow")
}
if none.KeyRead("foobar") {
if none.ACLList() {
t.Fatalf("should not allow")
}
if none.KeyWrite("foobar") {
if none.ACLModify() {
t.Fatalf("should not allow")
}
if none.ServiceRead("foobar") {
if none.AgentRead("foobar") {
t.Fatalf("should not allow")
}
if none.ServiceWrite("foobar") {
if none.AgentWrite("foobar") {
t.Fatalf("should not allow")
}
if none.EventRead("foobar") {
@ -102,10 +123,10 @@ func TestStaticACL(t *testing.T) {
if none.EventWrite("") {
t.Fatalf("should not allow")
}
if none.PreparedQueryRead("foobar") {
if none.KeyRead("foobar") {
t.Fatalf("should not allow")
}
if none.PreparedQueryWrite("foobar") {
if none.KeyWrite("foobar") {
t.Fatalf("should not allow")
}
if none.KeyringRead() {
@ -114,29 +135,50 @@ func TestStaticACL(t *testing.T) {
if none.KeyringWrite() {
t.Fatalf("should not allow")
}
if none.NodeRead("foobar") {
t.Fatalf("should not allow")
}
if none.NodeWrite("foobar") {
t.Fatalf("should not allow")
}
if none.OperatorRead() {
t.Fatalf("should now allow")
}
if none.OperatorWrite() {
t.Fatalf("should not allow")
}
if none.ACLList() {
if none.PreparedQueryRead("foobar") {
t.Fatalf("should not allow")
}
if none.ACLModify() {
if none.PreparedQueryWrite("foobar") {
t.Fatalf("should not allow")
}
if none.ServiceRead("foobar") {
t.Fatalf("should not allow")
}
if none.ServiceWrite("foobar") {
t.Fatalf("should not allow")
}
if none.SessionRead("foobar") {
t.Fatalf("should not allow")
}
if none.SessionWrite("foobar") {
t.Fatalf("should not allow")
}
if none.Snapshot() {
t.Fatalf("should not allow")
}
if !manage.KeyRead("foobar") {
if !manage.ACLList() {
t.Fatalf("should allow")
}
if !manage.KeyWrite("foobar") {
if !manage.ACLModify() {
t.Fatalf("should allow")
}
if !manage.ServiceRead("foobar") {
if !manage.AgentRead("foobar") {
t.Fatalf("should allow")
}
if !manage.ServiceWrite("foobar") {
if !manage.AgentWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.EventRead("foobar") {
@ -145,10 +187,10 @@ func TestStaticACL(t *testing.T) {
if !manage.EventWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.PreparedQueryRead("foobar") {
if !manage.KeyRead("foobar") {
t.Fatalf("should allow")
}
if !manage.PreparedQueryWrite("foobar") {
if !manage.KeyWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.KeyringRead() {
@ -157,16 +199,37 @@ func TestStaticACL(t *testing.T) {
if !manage.KeyringWrite() {
t.Fatalf("should allow")
}
if !manage.NodeRead("foobar") {
t.Fatalf("should allow")
}
if !manage.NodeWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.OperatorRead() {
t.Fatalf("should allow")
}
if !manage.OperatorWrite() {
t.Fatalf("should allow")
}
if !manage.ACLList() {
if !manage.PreparedQueryRead("foobar") {
t.Fatalf("should allow")
}
if !manage.ACLModify() {
if !manage.PreparedQueryWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.ServiceRead("foobar") {
t.Fatalf("should allow")
}
if !manage.ServiceWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.SessionRead("foobar") {
t.Fatalf("should allow")
}
if !manage.SessionWrite("foobar") {
t.Fatalf("should allow")
}
if !manage.Snapshot() {
t.Fatalf("should allow")
}
}
@ -174,6 +237,20 @@ func TestStaticACL(t *testing.T) {
func TestPolicyACL(t *testing.T) {
all := AllowAll()
policy := &Policy{
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
Policy: PolicyDeny,
},
},
Keys: []*KeyPolicy{
&KeyPolicy{
Prefix: "foo/",
@ -192,38 +269,6 @@ func TestPolicyACL(t *testing.T) {
Policy: PolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
&ServicePolicy{
Name: "bar",
Policy: PolicyDeny,
},
&ServicePolicy{
Name: "barfoo",
Policy: PolicyWrite,
},
},
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
Policy: PolicyDeny,
},
},
PreparedQueries: []*PreparedQueryPolicy{
&PreparedQueryPolicy{
Prefix: "",
@ -242,6 +287,24 @@ func TestPolicyACL(t *testing.T) {
Policy: PolicyWrite,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
&ServicePolicy{
Name: "bar",
Policy: PolicyDeny,
},
&ServicePolicy{
Name: "barfoo",
Policy: PolicyWrite,
},
},
}
acl, err := New(all, policy)
if err != nil {
@ -360,16 +423,6 @@ func TestPolicyACL_Parent(t *testing.T) {
Policy: PolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "other",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
PreparedQueries: []*PreparedQueryPolicy{
&PreparedQueryPolicy{
Prefix: "other",
@ -380,6 +433,16 @@ func TestPolicyACL_Parent(t *testing.T) {
Policy: PolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "other",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
}
root, err := New(deny, policyRoot)
if err != nil {
@ -401,18 +464,18 @@ func TestPolicyACL_Parent(t *testing.T) {
Policy: PolicyRead,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "bar",
Policy: PolicyDeny,
},
},
PreparedQueries: []*PreparedQueryPolicy{
&PreparedQueryPolicy{
Prefix: "bar",
Policy: PolicyDeny,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Name: "bar",
Policy: PolicyDeny,
},
},
}
acl, err := New(root, policy)
if err != nil {
@ -495,6 +558,92 @@ func TestPolicyACL_Parent(t *testing.T) {
if acl.ACLModify() {
t.Fatalf("should not allow")
}
if acl.Snapshot() {
t.Fatalf("should not allow")
}
}
func TestPolicyACL_Agent(t *testing.T) {
deny := DenyAll()
policyRoot := &Policy{
Agents: []*AgentPolicy{
&AgentPolicy{
Node: "root-nope",
Policy: PolicyDeny,
},
&AgentPolicy{
Node: "root-ro",
Policy: PolicyRead,
},
&AgentPolicy{
Node: "root-rw",
Policy: PolicyWrite,
},
&AgentPolicy{
Node: "override",
Policy: PolicyDeny,
},
},
}
root, err := New(deny, policyRoot)
if err != nil {
t.Fatalf("err: %v", err)
}
policy := &Policy{
Agents: []*AgentPolicy{
&AgentPolicy{
Node: "child-nope",
Policy: PolicyDeny,
},
&AgentPolicy{
Node: "child-ro",
Policy: PolicyRead,
},
&AgentPolicy{
Node: "child-rw",
Policy: PolicyWrite,
},
&AgentPolicy{
Node: "override",
Policy: PolicyWrite,
},
},
}
acl, err := New(root, policy)
if err != nil {
t.Fatalf("err: %v", err)
}
type agentcase struct {
inp string
read bool
write bool
}
cases := []agentcase{
{"nope", false, false},
{"root-nope", false, false},
{"root-ro", true, false},
{"root-rw", true, true},
{"root-nope-prefix", false, false},
{"root-ro-prefix", true, false},
{"root-rw-prefix", true, true},
{"child-nope", false, false},
{"child-ro", true, false},
{"child-rw", true, true},
{"child-nope-prefix", false, false},
{"child-ro-prefix", true, false},
{"child-rw-prefix", true, true},
{"override", true, true},
}
for _, c := range cases {
if c.read != acl.AgentRead(c.inp) {
t.Fatalf("Read fail: %#v", c)
}
if c.write != acl.AgentWrite(c.inp) {
t.Fatalf("Write fail: %#v", c)
}
}
}
func TestPolicyACL_Keyring(t *testing.T) {
@ -548,3 +697,169 @@ func TestPolicyACL_Operator(t *testing.T) {
}
}
}
func TestPolicyACL_Node(t *testing.T) {
deny := DenyAll()
policyRoot := &Policy{
Nodes: []*NodePolicy{
&NodePolicy{
Name: "root-nope",
Policy: PolicyDeny,
},
&NodePolicy{
Name: "root-ro",
Policy: PolicyRead,
},
&NodePolicy{
Name: "root-rw",
Policy: PolicyWrite,
},
&NodePolicy{
Name: "override",
Policy: PolicyDeny,
},
},
}
root, err := New(deny, policyRoot)
if err != nil {
t.Fatalf("err: %v", err)
}
policy := &Policy{
Nodes: []*NodePolicy{
&NodePolicy{
Name: "child-nope",
Policy: PolicyDeny,
},
&NodePolicy{
Name: "child-ro",
Policy: PolicyRead,
},
&NodePolicy{
Name: "child-rw",
Policy: PolicyWrite,
},
&NodePolicy{
Name: "override",
Policy: PolicyWrite,
},
},
}
acl, err := New(root, policy)
if err != nil {
t.Fatalf("err: %v", err)
}
type nodecase struct {
inp string
read bool
write bool
}
cases := []nodecase{
{"nope", false, false},
{"root-nope", false, false},
{"root-ro", true, false},
{"root-rw", true, true},
{"root-nope-prefix", false, false},
{"root-ro-prefix", true, false},
{"root-rw-prefix", true, true},
{"child-nope", false, false},
{"child-ro", true, false},
{"child-rw", true, true},
{"child-nope-prefix", false, false},
{"child-ro-prefix", true, false},
{"child-rw-prefix", true, true},
{"override", true, true},
}
for _, c := range cases {
if c.read != acl.NodeRead(c.inp) {
t.Fatalf("Read fail: %#v", c)
}
if c.write != acl.NodeWrite(c.inp) {
t.Fatalf("Write fail: %#v", c)
}
}
}
func TestPolicyACL_Session(t *testing.T) {
deny := DenyAll()
policyRoot := &Policy{
Sessions: []*SessionPolicy{
&SessionPolicy{
Node: "root-nope",
Policy: PolicyDeny,
},
&SessionPolicy{
Node: "root-ro",
Policy: PolicyRead,
},
&SessionPolicy{
Node: "root-rw",
Policy: PolicyWrite,
},
&SessionPolicy{
Node: "override",
Policy: PolicyDeny,
},
},
}
root, err := New(deny, policyRoot)
if err != nil {
t.Fatalf("err: %v", err)
}
policy := &Policy{
Sessions: []*SessionPolicy{
&SessionPolicy{
Node: "child-nope",
Policy: PolicyDeny,
},
&SessionPolicy{
Node: "child-ro",
Policy: PolicyRead,
},
&SessionPolicy{
Node: "child-rw",
Policy: PolicyWrite,
},
&SessionPolicy{
Node: "override",
Policy: PolicyWrite,
},
},
}
acl, err := New(root, policy)
if err != nil {
t.Fatalf("err: %v", err)
}
type sessioncase struct {
inp string
read bool
write bool
}
cases := []sessioncase{
{"nope", false, false},
{"root-nope", false, false},
{"root-ro", true, false},
{"root-rw", true, true},
{"root-nope-prefix", false, false},
{"root-ro-prefix", true, false},
{"root-rw-prefix", true, true},
{"child-nope", false, false},
{"child-ro", true, false},
{"child-rw", true, true},
{"child-nope-prefix", false, false},
{"child-ro-prefix", true, false},
{"child-rw-prefix", true, true},
{"override", true, true},
}
for _, c := range cases {
if c.read != acl.SessionRead(c.inp) {
t.Fatalf("Read fail: %#v", c)
}
if c.write != acl.SessionWrite(c.inp) {
t.Fatalf("Write fail: %#v", c)
}
}
}

View File

@ -16,14 +16,28 @@ const (
// an ACL configuration.
type Policy struct {
ID string `hcl:"-"`
Agents []*AgentPolicy `hcl:"agent,expand"`
Keys []*KeyPolicy `hcl:"key,expand"`
Nodes []*NodePolicy `hcl:"node,expand"`
Services []*ServicePolicy `hcl:"service,expand"`
Sessions []*SessionPolicy `hcl:"session,expand"`
Events []*EventPolicy `hcl:"event,expand"`
PreparedQueries []*PreparedQueryPolicy `hcl:"query,expand"`
Keyring string `hcl:"keyring"`
Operator string `hcl:"operator"`
}
// AgentPolicy represents a policy for working with agent endpoints on nodes
// with specific name prefixes.
type AgentPolicy struct {
Node string `hcl:",key"`
Policy string
}
func (a *AgentPolicy) GoString() string {
return fmt.Sprintf("%#v", *a)
}
// KeyPolicy represents a policy for a key
type KeyPolicy struct {
Prefix string `hcl:",key"`
@ -34,14 +48,35 @@ func (k *KeyPolicy) GoString() string {
return fmt.Sprintf("%#v", *k)
}
// NodePolicy represents a policy for a node
type NodePolicy struct {
Name string `hcl:",key"`
Policy string
}
func (n *NodePolicy) GoString() string {
return fmt.Sprintf("%#v", *n)
}
// ServicePolicy represents a policy for a service
type ServicePolicy struct {
Name string `hcl:",key"`
Policy string
}
func (k *ServicePolicy) GoString() string {
return fmt.Sprintf("%#v", *k)
func (s *ServicePolicy) GoString() string {
return fmt.Sprintf("%#v", *s)
}
// SessionPolicy represents a policy for making sessions tied to specific node
// name prefixes.
type SessionPolicy struct {
Node string `hcl:",key"`
Policy string
}
func (s *SessionPolicy) GoString() string {
return fmt.Sprintf("%#v", *s)
}
// EventPolicy represents a user event policy.
@ -60,8 +95,8 @@ type PreparedQueryPolicy struct {
Policy string
}
func (e *PreparedQueryPolicy) GoString() string {
return fmt.Sprintf("%#v", *e)
func (p *PreparedQueryPolicy) GoString() string {
return fmt.Sprintf("%#v", *p)
}
// isPolicyValid makes sure the given string matches one of the valid policies.
@ -93,6 +128,13 @@ func Parse(rules string) (*Policy, error) {
return nil, fmt.Errorf("Failed to parse ACL rules: %v", err)
}
// Validate the agent policy
for _, ap := range p.Agents {
if !isPolicyValid(ap.Policy) {
return nil, fmt.Errorf("Invalid agent policy: %#v", ap)
}
}
// Validate the key policy
for _, kp := range p.Keys {
if !isPolicyValid(kp.Policy) {
@ -100,13 +142,27 @@ func Parse(rules string) (*Policy, error) {
}
}
// Validate the service policy
// Validate the node policies
for _, np := range p.Nodes {
if !isPolicyValid(np.Policy) {
return nil, fmt.Errorf("Invalid node policy: %#v", np)
}
}
// Validate the service policies
for _, sp := range p.Services {
if !isPolicyValid(sp.Policy) {
return nil, fmt.Errorf("Invalid service policy: %#v", sp)
}
}
// Validate the session policies
for _, sp := range p.Sessions {
if !isPolicyValid(sp.Policy) {
return nil, fmt.Errorf("Invalid session policy: %#v", sp)
}
}
// Validate the user event policies
for _, ep := range p.Events {
if !isPolicyValid(ep.Policy) {

View File

@ -8,6 +8,21 @@ import (
func TestACLPolicy_Parse_HCL(t *testing.T) {
inp := `
agent "foo" {
policy = "read"
}
agent "bar" {
policy = "write"
}
event "" {
policy = "read"
}
event "foo" {
policy = "write"
}
event "bar" {
policy = "deny"
}
key "" {
policy = "read"
}
@ -20,19 +35,27 @@ key "foo/bar/" {
key "foo/bar/baz" {
policy = "deny"
}
keyring = "deny"
node "" {
policy = "read"
}
node "foo" {
policy = "write"
}
node "bar" {
policy = "deny"
}
operator = "deny"
service "" {
policy = "write"
}
service "foo" {
policy = "read"
}
event "" {
policy = "read"
}
event "foo" {
session "foo" {
policy = "write"
}
event "bar" {
session "bar" {
policy = "deny"
}
query "" {
@ -44,10 +67,33 @@ query "foo" {
query "bar" {
policy = "deny"
}
keyring = "deny"
operator = "deny"
`
exp := &Policy{
Agents: []*AgentPolicy{
&AgentPolicy{
Node: "foo",
Policy: PolicyRead,
},
&AgentPolicy{
Node: "bar",
Policy: PolicyWrite,
},
},
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Keys: []*KeyPolicy{
&KeyPolicy{
Prefix: "",
@ -66,30 +112,21 @@ operator = "deny"
Policy: PolicyDeny,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Nodes: []*NodePolicy{
&NodePolicy{
Name: "",
Policy: PolicyWrite,
Policy: PolicyRead,
},
&ServicePolicy{
&NodePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
&NodePolicy{
Name: "bar",
Policy: PolicyDeny,
},
},
Operator: PolicyDeny,
PreparedQueries: []*PreparedQueryPolicy{
&PreparedQueryPolicy{
Prefix: "",
@ -104,8 +141,26 @@ operator = "deny"
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Operator: PolicyDeny,
Services: []*ServicePolicy{
&ServicePolicy{
Name: "",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
Sessions: []*SessionPolicy{
&SessionPolicy{
Node: "foo",
Policy: PolicyWrite,
},
&SessionPolicy{
Node: "bar",
Policy: PolicyDeny,
},
},
}
out, err := Parse(inp)
@ -120,6 +175,25 @@ operator = "deny"
func TestACLPolicy_Parse_JSON(t *testing.T) {
inp := `{
"agent": {
"foo": {
"policy": "write"
},
"bar": {
"policy": "deny"
}
},
"event": {
"": {
"policy": "read"
},
"foo": {
"policy": "write"
},
"bar": {
"policy": "deny"
}
},
"key": {
"": {
"policy": "read"
@ -134,15 +208,8 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
"policy": "deny"
}
},
"service": {
"": {
"policy": "write"
},
"foo": {
"policy": "read"
}
},
"event": {
"keyring": "deny",
"node": {
"": {
"policy": "read"
},
@ -153,6 +220,7 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
"policy": "deny"
}
},
"operator": "deny",
"query": {
"": {
"policy": "read"
@ -164,10 +232,49 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
"policy": "deny"
}
},
"keyring": "deny",
"operator": "deny"
"service": {
"": {
"policy": "write"
},
"foo": {
"policy": "read"
}
},
"session": {
"foo": {
"policy": "write"
},
"bar": {
"policy": "deny"
}
}
}`
exp := &Policy{
Agents: []*AgentPolicy{
&AgentPolicy{
Node: "foo",
Policy: PolicyWrite,
},
&AgentPolicy{
Node: "bar",
Policy: PolicyDeny,
},
},
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Keys: []*KeyPolicy{
&KeyPolicy{
Prefix: "",
@ -186,30 +293,21 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
Policy: PolicyDeny,
},
},
Services: []*ServicePolicy{
&ServicePolicy{
Nodes: []*NodePolicy{
&NodePolicy{
Name: "",
Policy: PolicyWrite,
Policy: PolicyRead,
},
&ServicePolicy{
&NodePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
Events: []*EventPolicy{
&EventPolicy{
Event: "",
Policy: PolicyRead,
},
&EventPolicy{
Event: "foo",
Policy: PolicyWrite,
},
&EventPolicy{
Event: "bar",
&NodePolicy{
Name: "bar",
Policy: PolicyDeny,
},
},
Operator: PolicyDeny,
PreparedQueries: []*PreparedQueryPolicy{
&PreparedQueryPolicy{
Prefix: "",
@ -224,8 +322,26 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Operator: PolicyDeny,
Services: []*ServicePolicy{
&ServicePolicy{
Name: "",
Policy: PolicyWrite,
},
&ServicePolicy{
Name: "foo",
Policy: PolicyRead,
},
},
Sessions: []*SessionPolicy{
&SessionPolicy{
Node: "foo",
Policy: PolicyWrite,
},
&SessionPolicy{
Node: "bar",
Policy: PolicyDeny,
},
},
}
out, err := Parse(inp)
@ -276,12 +392,15 @@ operator = ""
func TestACLPolicy_Bad_Policy(t *testing.T) {
cases := []string{
`key "" { policy = "nope" }`,
`service "" { policy = "nope" }`,
`agent "" { policy = "nope" }`,
`event "" { policy = "nope" }`,
`query "" { policy = "nope" }`,
`key "" { policy = "nope" }`,
`keyring = "nope"`,
`node "" { policy = "nope" }`,
`operator = "nope"`,
`query "" { policy = "nope" }`,
`service "" { policy = "nope" }`,
`session "" { policy = "nope" }`,
}
for _, c := range cases {
_, err := Parse(c)

View File

@ -1,6 +1,7 @@
package api
import (
"bufio"
"fmt"
)
@ -73,6 +74,8 @@ type AgentServiceCheck struct {
HTTP string `json:",omitempty"`
TCP string `json:",omitempty"`
Status string `json:",omitempty"`
Notes string `json:",omitempty"`
TLSSkipVerify bool `json:",omitempty"`
// In Consul 0.7 and later, checks that are associated with a service
// may also contain this optional DeregisterCriticalServiceAfter field,
@ -114,6 +117,17 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) {
return out, nil
}
// Reload triggers a configuration reload for the agent we are connected to.
func (a *Agent) Reload() error {
r := a.c.newRequest("PUT", "/v1/agent/reload")
_, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// NodeName is used to get the node name of the agent
func (a *Agent) NodeName() (string, error) {
if a.nodeName != "" {
@ -345,6 +359,17 @@ func (a *Agent) Join(addr string, wan bool) error {
return nil
}
// Leave is used to have the agent gracefully leave the cluster and shutdown
func (a *Agent) Leave() error {
r := a.c.newRequest("PUT", "/v1/agent/leave")
_, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// ForceLeave is used to have the agent eject a failed node
func (a *Agent) ForceLeave(node string) error {
r := a.c.newRequest("PUT", "/v1/agent/force-leave/"+node)
@ -409,3 +434,38 @@ func (a *Agent) DisableNodeMaintenance() error {
resp.Body.Close()
return nil
}
// Monitor returns a channel which will receive streaming logs from the agent
// Providing a non-nil stopCh can be used to close the connection and stop the
// log stream
func (a *Agent) Monitor(loglevel string, stopCh chan struct{}, q *QueryOptions) (chan string, error) {
r := a.c.newRequest("GET", "/v1/agent/monitor")
r.setQueryOptions(q)
if loglevel != "" {
r.params.Add("loglevel", loglevel)
}
_, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, err
}
logCh := make(chan string, 64)
go func() {
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for {
select {
case <-stopCh:
close(logCh)
return
default:
}
if scanner.Scan() {
logCh <- scanner.Text()
}
}
}()
return logCh, nil
}

View File

@ -1,8 +1,15 @@
package api
import (
"io"
"io/ioutil"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/serf/serf"
)
func TestAgent_Self(t *testing.T) {
@ -23,6 +30,51 @@ func TestAgent_Self(t *testing.T) {
}
}
func TestAgent_Reload(t *testing.T) {
t.Parallel()
// Create our initial empty config file, to be overwritten later
configFile, err := ioutil.TempFile("", "reload")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := configFile.Write([]byte("{}")); err != nil {
t.Fatalf("err: %s", err)
}
configFile.Close()
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.Args = []string{"-config-file", configFile.Name()}
})
defer s.Stop()
agent := c.Agent()
// Update the config file with a service definition
config := `{"service":{"name":"redis", "port":1234}}`
err = ioutil.WriteFile(configFile.Name(), []byte(config), 0644)
if err != nil {
t.Fatalf("err: %v", err)
}
if err = agent.Reload(); err != nil {
t.Fatalf("err: %v", err)
}
services, err := agent.Services()
if err != nil {
t.Fatalf("err: %v", err)
}
service, ok := services["redis"]
if !ok {
t.Fatalf("bad: %v", ok)
}
if service.Port != 1234 {
t.Fatalf("bad: %v", service.Port)
}
}
func TestAgent_Members(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
@ -544,6 +596,41 @@ func TestAgent_Join(t *testing.T) {
}
}
func TestAgent_Leave(t *testing.T) {
t.Parallel()
c1, s1 := makeClient(t)
defer s1.Stop()
c2, s2 := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.Server = false
conf.Bootstrap = false
})
defer s2.Stop()
if err := c2.Agent().Join(s1.LANAddr, false); err != nil {
t.Fatalf("err: %v", err)
}
// We sometimes see an EOF response to this one, depending on timing.
err := c2.Agent().Leave()
if err != nil && err != io.EOF {
t.Fatalf("err: %v", err)
}
// Make sure the second agent's status is 'Left'
members, err := c1.Agent().Members(false)
if err != nil {
t.Fatalf("err: %v", err)
}
member := members[0]
if member.Name == s1.Config.NodeName {
member = members[1]
}
if member.Status != int(serf.StatusLeft) {
t.Fatalf("bad: %v", *member)
}
}
func TestAgent_ForceLeave(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
@ -558,6 +645,29 @@ func TestAgent_ForceLeave(t *testing.T) {
}
}
func TestAgent_Monitor(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()
agent := c.Agent()
logCh, err := agent.Monitor("info", nil, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
// Wait for the first log message and validate it
select {
case log := <-logCh:
if !strings.Contains(log, "[INFO]") {
t.Fatalf("bad: %q", log)
}
case <-time.After(10 * time.Second):
t.Fatalf("failed to get a log message")
}
}
func TestServiceMaintenance(t *testing.T) {
t.Parallel()
c, s := makeClient(t)

View File

@ -20,6 +20,28 @@ import (
"github.com/hashicorp/go-cleanhttp"
)
const (
// HTTPAddrEnvName defines an environment variable name which sets
// the HTTP address if there is no -http-addr specified.
HTTPAddrEnvName = "CONSUL_HTTP_ADDR"
// HTTPTokenEnvName defines an environment variable name which sets
// the HTTP token.
HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"
// HTTPAuthEnvName defines an environment variable name which sets
// the HTTP authentication header.
HTTPAuthEnvName = "CONSUL_HTTP_AUTH"
// HTTPSSLEnvName defines an environment variable name which sets
// whether or not to use HTTPS.
HTTPSSLEnvName = "CONSUL_HTTP_SSL"
// HTTPSSLVerifyEnvName defines an environment variable name which sets
// whether or not to disable certificate checking.
HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"
)
// QueryOptions are used to parameterize a query
type QueryOptions struct {
// Providing a datacenter overwrites the DC provided
@ -52,6 +74,11 @@ type QueryOptions struct {
// that node. Setting this to "_agent" will use the agent's node
// for the sort.
Near string
// NodeMeta is used to filter results by nodes with the given
// metadata key/value pairs. Currently, only one key/value pair can
// be provided for filtering.
NodeMeta map[string]string
}
// WriteOptions are used to parameterize a write
@ -181,15 +208,15 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
},
}
if addr := os.Getenv("CONSUL_HTTP_ADDR"); addr != "" {
if addr := os.Getenv(HTTPAddrEnvName); addr != "" {
config.Address = addr
}
if token := os.Getenv("CONSUL_HTTP_TOKEN"); token != "" {
if token := os.Getenv(HTTPTokenEnvName); token != "" {
config.Token = token
}
if auth := os.Getenv("CONSUL_HTTP_AUTH"); auth != "" {
if auth := os.Getenv(HTTPAuthEnvName); auth != "" {
var username, password string
if strings.Contains(auth, ":") {
split := strings.SplitN(auth, ":", 2)
@ -205,10 +232,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
}
}
if ssl := os.Getenv("CONSUL_HTTP_SSL"); ssl != "" {
if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" {
enabled, err := strconv.ParseBool(ssl)
if err != nil {
log.Printf("[WARN] client: could not parse CONSUL_HTTP_SSL: %s", err)
log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLEnvName, err)
}
if enabled {
@ -216,10 +243,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
}
}
if verify := os.Getenv("CONSUL_HTTP_SSL_VERIFY"); verify != "" {
if verify := os.Getenv(HTTPSSLVerifyEnvName); verify != "" {
doVerify, err := strconv.ParseBool(verify)
if err != nil {
log.Printf("[WARN] client: could not parse CONSUL_HTTP_SSL_VERIFY: %s", err)
log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err)
}
if !doVerify {
@ -364,6 +391,11 @@ func (r *request) setQueryOptions(q *QueryOptions) {
if q.Near != "" {
r.params.Set("near", q.Near)
}
if len(q.NodeMeta) > 0 {
for key, value := range q.NodeMeta {
r.params.Add("node-meta", key+":"+value)
}
}
}
// durToMsec converts a duration to a millisecond specified string. If the

View File

@ -76,16 +76,16 @@ func TestDefaultConfig_env(t *testing.T) {
token := "abcd1234"
auth := "username:password"
os.Setenv("CONSUL_HTTP_ADDR", addr)
defer os.Setenv("CONSUL_HTTP_ADDR", "")
os.Setenv("CONSUL_HTTP_TOKEN", token)
defer os.Setenv("CONSUL_HTTP_TOKEN", "")
os.Setenv("CONSUL_HTTP_AUTH", auth)
defer os.Setenv("CONSUL_HTTP_AUTH", "")
os.Setenv("CONSUL_HTTP_SSL", "1")
defer os.Setenv("CONSUL_HTTP_SSL", "")
os.Setenv("CONSUL_HTTP_SSL_VERIFY", "0")
defer os.Setenv("CONSUL_HTTP_SSL_VERIFY", "")
os.Setenv(HTTPAddrEnvName, addr)
defer os.Setenv(HTTPAddrEnvName, "")
os.Setenv(HTTPTokenEnvName, token)
defer os.Setenv(HTTPTokenEnvName, "")
os.Setenv(HTTPAuthEnvName, auth)
defer os.Setenv(HTTPAuthEnvName, "")
os.Setenv(HTTPSSLEnvName, "1")
defer os.Setenv(HTTPSSLEnvName, "")
os.Setenv(HTTPSSLVerifyEnvName, "0")
defer os.Setenv(HTTPSSLVerifyEnvName, "")
for i, config := range []*Config{DefaultConfig(), DefaultNonPooledConfig()} {
if config.Address != addr {

View File

@ -1,21 +1,27 @@
package api
type Node struct {
ID string
Node string
Address string
TaggedAddresses map[string]string
Meta map[string]string
}
type CatalogService struct {
ID string
Node string
Address string
TaggedAddresses map[string]string
NodeMeta map[string]string
ServiceID string
ServiceName string
ServiceAddress string
ServiceTags []string
ServicePort int
ServiceEnableTagOverride bool
CreateIndex uint64
ModifyIndex uint64
}
type CatalogNode struct {
@ -24,9 +30,11 @@ type CatalogNode struct {
}
type CatalogRegistration struct {
ID string
Node string
Address string
TaggedAddresses map[string]string
NodeMeta map[string]string
Datacenter string
Service *AgentService
Check *AgentCheck
@ -34,7 +42,7 @@ type CatalogRegistration struct {
type CatalogDeregistration struct {
Node string
Address string
Address string // Obsolete.
Datacenter string
ServiceID string
CheckID string

View File

@ -31,7 +31,6 @@ func TestCatalog_Datacenters(t *testing.T) {
}
func TestCatalog_Nodes(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()
@ -52,6 +51,64 @@ func TestCatalog_Nodes(t *testing.T) {
}
if _, ok := nodes[0].TaggedAddresses["wan"]; !ok {
return false, fmt.Errorf("Bad: %v", nodes[0])
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCatalog_Nodes_MetaFilter(t *testing.T) {
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
catalog := c.Catalog()
// Make sure we get the node back when filtering by its metadata
testutil.WaitForResult(func() (bool, error) {
nodes, meta, err := catalog.Nodes(&QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("Bad: %v", meta)
}
if len(nodes) == 0 {
return false, fmt.Errorf("Bad: %v", nodes)
}
if _, ok := nodes[0].TaggedAddresses["wan"]; !ok {
return false, fmt.Errorf("Bad: %v", nodes[0])
}
if v, ok := nodes[0].Meta["somekey"]; !ok || v != "somevalue" {
return false, fmt.Errorf("Bad: %v", nodes[0].Meta)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Get nothing back when we use an invalid filter
testutil.WaitForResult(func() (bool, error) {
nodes, meta, err := catalog.Nodes(&QueryOptions{NodeMeta: map[string]string{"nope": "nope"}})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("Bad: %v", meta)
}
if len(nodes) != 0 {
return false, fmt.Errorf("Bad: %v", nodes)
}
@ -88,6 +145,56 @@ func TestCatalog_Services(t *testing.T) {
})
}
func TestCatalog_Services_NodeMetaFilter(t *testing.T) {
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
catalog := c.Catalog()
// Make sure we get the service back when filtering by the node's metadata
testutil.WaitForResult(func() (bool, error) {
services, meta, err := catalog.Services(&QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("Bad: %v", meta)
}
if len(services) == 0 {
return false, fmt.Errorf("Bad: %v", services)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Get nothing back when using an invalid filter
testutil.WaitForResult(func() (bool, error) {
services, meta, err := catalog.Services(&QueryOptions{NodeMeta: map[string]string{"nope": "nope"}})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("Bad: %v", meta)
}
if len(services) != 0 {
return false, fmt.Errorf("Bad: %v", services)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCatalog_Service(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
@ -115,6 +222,36 @@ func TestCatalog_Service(t *testing.T) {
})
}
func TestCatalog_Service_NodeMetaFilter(t *testing.T) {
t.Parallel()
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
catalog := c.Catalog()
testutil.WaitForResult(func() (bool, error) {
services, meta, err := catalog.Service("consul", "", &QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("Bad: %v", meta)
}
if len(services) == 0 {
return false, fmt.Errorf("Bad: %v", services)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCatalog_Node(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
@ -174,6 +311,7 @@ func TestCatalog_Registration(t *testing.T) {
Datacenter: "dc1",
Node: "foobar",
Address: "192.168.10.10",
NodeMeta: map[string]string{"somekey": "somevalue"},
Service: service,
Check: check,
}
@ -201,6 +339,10 @@ func TestCatalog_Registration(t *testing.T) {
return false, fmt.Errorf("missing checkid service:redis1")
}
if v, ok := node.Node.Meta["somekey"]; !ok || v != "somevalue" {
return false, fmt.Errorf("missing node meta pair somekey:somevalue")
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)

View File

@ -2,6 +2,7 @@ package api
import (
"fmt"
"strings"
)
const (
@ -11,6 +12,15 @@ const (
HealthPassing = "passing"
HealthWarning = "warning"
HealthCritical = "critical"
HealthMaint = "maintenance"
)
const (
// NodeMaint is the special key set by a node in maintenance mode.
NodeMaint = "_node_maintenance"
// ServiceMaintPrefix is the prefix for a service in maintenance mode.
ServiceMaintPrefix = "_service_maintenance:"
)
// HealthCheck is used to represent a single check
@ -25,11 +35,56 @@ type HealthCheck struct {
ServiceName string
}
// HealthChecks is a collection of HealthCheck structs.
type HealthChecks []*HealthCheck
// AggregatedStatus returns the "best" status for the list of health checks.
// Because a given entry may have many service and node-level health checks
// attached, this function determines the best representative of the status as
// as single string using the following heuristic:
//
// maintenance > critical > warning > passing
//
func (c HealthChecks) AggregatedStatus() string {
var passing, warning, critical, maintenance bool
for _, check := range c {
id := string(check.CheckID)
if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) {
maintenance = true
continue
}
switch check.Status {
case HealthPassing:
passing = true
case HealthWarning:
warning = true
case HealthCritical:
critical = true
default:
return ""
}
}
switch {
case maintenance:
return HealthMaint
case critical:
return HealthCritical
case warning:
return HealthWarning
case passing:
return HealthPassing
default:
return HealthPassing
}
}
// ServiceEntry is used for the health service endpoint
type ServiceEntry struct {
Node *Node
Service *AgentService
Checks []*HealthCheck
Checks HealthChecks
}
// Health can be used to query the Health endpoints
@ -43,7 +98,7 @@ func (c *Client) Health() *Health {
}
// Node is used to query for checks belonging to a given node
func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) {
func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
r := h.c.newRequest("GET", "/v1/health/node/"+node)
r.setQueryOptions(q)
rtt, resp, err := requireOK(h.c.doRequest(r))
@ -56,7 +111,7 @@ func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta,
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var out []*HealthCheck
var out HealthChecks
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
@ -64,7 +119,7 @@ func (h *Health) Node(node string, q *QueryOptions) ([]*HealthCheck, *QueryMeta,
}
// Checks is used to return the checks associated with a service
func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) {
func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
r := h.c.newRequest("GET", "/v1/health/checks/"+service)
r.setQueryOptions(q)
rtt, resp, err := requireOK(h.c.doRequest(r))
@ -77,7 +132,7 @@ func (h *Health) Checks(service string, q *QueryOptions) ([]*HealthCheck, *Query
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var out []*HealthCheck
var out HealthChecks
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
@ -115,7 +170,7 @@ func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions)
// State is used to retrieve all the checks in a given state.
// The wildcard "any" state can also be used for all checks.
func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMeta, error) {
func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
switch state {
case HealthAny:
case HealthWarning:
@ -136,7 +191,7 @@ func (h *Health) State(state string, q *QueryOptions) ([]*HealthCheck, *QueryMet
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var out []*HealthCheck
var out HealthChecks
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}

View File

@ -38,6 +38,139 @@ func TestHealth_Node(t *testing.T) {
})
}
func TestHealthChecks_AggregatedStatus(t *testing.T) {
t.Parallel()
cases := []struct {
name string
checks HealthChecks
exp string
}{
{
"empty",
nil,
HealthPassing,
},
{
"passing",
HealthChecks{
&HealthCheck{
Status: HealthPassing,
},
},
HealthPassing,
},
{
"warning",
HealthChecks{
&HealthCheck{
Status: HealthWarning,
},
},
HealthWarning,
},
{
"critical",
HealthChecks{
&HealthCheck{
Status: HealthCritical,
},
},
HealthCritical,
},
{
"node_maintenance",
HealthChecks{
&HealthCheck{
CheckID: NodeMaint,
},
},
HealthMaint,
},
{
"service_maintenance",
HealthChecks{
&HealthCheck{
CheckID: ServiceMaintPrefix + "service",
},
},
HealthMaint,
},
{
"unknown",
HealthChecks{
&HealthCheck{
Status: "nope-nope-noper",
},
},
"",
},
{
"maintenance_over_critical",
HealthChecks{
&HealthCheck{
CheckID: NodeMaint,
},
&HealthCheck{
Status: HealthCritical,
},
},
HealthMaint,
},
{
"critical_over_warning",
HealthChecks{
&HealthCheck{
Status: HealthCritical,
},
&HealthCheck{
Status: HealthWarning,
},
},
HealthCritical,
},
{
"warning_over_passing",
HealthChecks{
&HealthCheck{
Status: HealthWarning,
},
&HealthCheck{
Status: HealthPassing,
},
},
HealthWarning,
},
{
"lots",
HealthChecks{
&HealthCheck{
Status: HealthPassing,
},
&HealthCheck{
Status: HealthPassing,
},
&HealthCheck{
Status: HealthPassing,
},
&HealthCheck{
Status: HealthWarning,
},
},
HealthWarning,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) {
act := tc.checks.AggregatedStatus()
if tc.exp != act {
t.Errorf("\nexp: %#v\nact: %#v", tc.exp, act)
}
})
}
}
func TestHealth_Checks(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
@ -75,8 +208,47 @@ func TestHealth_Checks(t *testing.T) {
})
}
func TestHealth_Service(t *testing.T) {
func TestHealth_Checks_NodeMetaFilter(t *testing.T) {
t.Parallel()
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
agent := c.Agent()
health := c.Health()
// Make a service with a check
reg := &AgentServiceRegistration{
Name: "foo",
Check: &AgentServiceCheck{
TTL: "15s",
},
}
if err := agent.ServiceRegister(reg); err != nil {
t.Fatalf("err: %v", err)
}
defer agent.ServiceDeregister("foo")
testutil.WaitForResult(func() (bool, error) {
checks, meta, err := health.Checks("foo", &QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("bad: %v", meta)
}
if len(checks) == 0 {
return false, fmt.Errorf("Bad: %v", checks)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestHealth_Service(t *testing.T) {
c, s := makeClient(t)
defer s.Stop()
@ -95,8 +267,38 @@ func TestHealth_Service(t *testing.T) {
return false, fmt.Errorf("Bad: %v", checks)
}
if _, ok := checks[0].Node.TaggedAddresses["wan"]; !ok {
return false, fmt.Errorf("Bad: %v", checks[0].Node)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestHealth_Service_NodeMetaFilter(t *testing.T) {
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
health := c.Health()
testutil.WaitForResult(func() (bool, error) {
// consul service should always exist...
checks, meta, err := health.Service("consul", "", true, &QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("bad: %v", meta)
}
if len(checks) == 0 {
return false, fmt.Errorf("Bad: %v", checks)
}
if _, ok := checks[0].Node.TaggedAddresses["wan"]; !ok {
return false, fmt.Errorf("Bad: %v", checks[0].Node)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
@ -126,3 +328,30 @@ func TestHealth_State(t *testing.T) {
t.Fatalf("err: %s", err)
})
}
func TestHealth_State_NodeMetaFilter(t *testing.T) {
t.Parallel()
meta := map[string]string{"somekey": "somevalue"}
c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) {
conf.NodeMeta = meta
})
defer s.Stop()
health := c.Health()
testutil.WaitForResult(func() (bool, error) {
checks, meta, err := health.State("any", &QueryOptions{NodeMeta: meta})
if err != nil {
return false, err
}
if meta.LastIndex == 0 {
return false, fmt.Errorf("bad: %v", meta)
}
if len(checks) == 0 {
return false, fmt.Errorf("Bad: %v", checks)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}

View File

@ -50,21 +50,21 @@ type KVOp string
const (
KVSet KVOp = "set"
KVDelete = "delete"
KVDeleteCAS = "delete-cas"
KVDeleteTree = "delete-tree"
KVCAS = "cas"
KVLock = "lock"
KVUnlock = "unlock"
KVGet = "get"
KVGetTree = "get-tree"
KVCheckSession = "check-session"
KVCheckIndex = "check-index"
KVDelete KVOp = "delete"
KVDeleteCAS KVOp = "delete-cas"
KVDeleteTree KVOp = "delete-tree"
KVCAS KVOp = "cas"
KVLock KVOp = "lock"
KVUnlock KVOp = "unlock"
KVGet KVOp = "get"
KVGetTree KVOp = "get-tree"
KVCheckSession KVOp = "check-session"
KVCheckIndex KVOp = "check-index"
)
// KVTxnOp defines a single operation inside a transaction.
type KVTxnOp struct {
Verb string
Verb KVOp
Key string
Value []byte
Flags uint64
@ -92,7 +92,8 @@ func (c *Client) KV() *KV {
return &KV{c}
}
// Get is used to lookup a single key
// Get is used to lookup a single key. The returned pointer
// to the KVPair will be nil if the key does not exist.
func (k *KV) Get(key string, q *QueryOptions) (*KVPair, *QueryMeta, error) {
resp, qm, err := k.getInternal(key, nil, q)
if err != nil {
@ -155,7 +156,7 @@ func (k *KV) Keys(prefix, separator string, q *QueryOptions) ([]string, *QueryMe
}
func (k *KV) getInternal(key string, params map[string]string, q *QueryOptions) (*http.Response, *QueryMeta, error) {
r := k.c.newRequest("GET", "/v1/kv/"+key)
r := k.c.newRequest("GET", "/v1/kv/"+strings.TrimPrefix(key, "/"))
r.setQueryOptions(q)
for param, val := range params {
r.params.Set(param, val)
@ -276,7 +277,7 @@ func (k *KV) DeleteTree(prefix string, w *WriteOptions) (*WriteMeta, error) {
}
func (k *KV) deleteInternal(key string, params map[string]string, q *WriteOptions) (bool, *WriteMeta, error) {
r := k.c.newRequest("DELETE", "/v1/kv/"+key)
r := k.c.newRequest("DELETE", "/v1/kv/"+strings.TrimPrefix(key, "/"))
r.setWriteOptions(q)
for param, val := range params {
r.params.Set(param, val)

View File

@ -244,6 +244,7 @@ func TestClient_WatchGet(t *testing.T) {
// Put the key
value := []byte("test")
doneCh := make(chan struct{})
go func() {
kv := c.KV()
@ -252,6 +253,7 @@ func TestClient_WatchGet(t *testing.T) {
if _, err := kv.Put(p, nil); err != nil {
t.Fatalf("err: %v", err)
}
doneCh <- struct{}{}
}()
// Get should work
@ -272,6 +274,9 @@ func TestClient_WatchGet(t *testing.T) {
if meta2.LastIndex <= meta.LastIndex {
t.Fatalf("unexpected value: %#v", meta2)
}
// Block until put finishes to avoid a race between it and deferred s.Stop()
<-doneCh
}
func TestClient_WatchList(t *testing.T) {
@ -297,6 +302,7 @@ func TestClient_WatchList(t *testing.T) {
// Put the key
value := []byte("test")
doneCh := make(chan struct{})
go func() {
kv := c.KV()
@ -305,6 +311,7 @@ func TestClient_WatchList(t *testing.T) {
if _, err := kv.Put(p, nil); err != nil {
t.Fatalf("err: %v", err)
}
doneCh <- struct{}{}
}()
// Get should work
@ -326,6 +333,8 @@ func TestClient_WatchList(t *testing.T) {
t.Fatalf("unexpected value: %#v", meta2)
}
// Block until put finishes to avoid a race between it and deferred s.Stop()
<-doneCh
}
func TestClient_Keys_DeleteRecurse(t *testing.T) {

View File

@ -139,7 +139,7 @@ func TestLock_DeleteKey(t *testing.T) {
// Should loose leadership
select {
case <-leaderCh:
case <-time.After(time.Second):
case <-time.After(10 * time.Second):
t.Fatalf("should not be leader")
}
}()

View File

@ -43,6 +43,26 @@ type RaftConfiguration struct {
Index uint64
}
// keyringRequest is used for performing Keyring operations
type keyringRequest struct {
Key string
}
// KeyringResponse is returned when listing the gossip encryption keys
type KeyringResponse struct {
// Whether this response is for a WAN ring
WAN bool
// The datacenter name this request corresponds to
Datacenter string
// A map of the encryption keys to the number of nodes they're installed on
Keys map[string]int
// The total number of nodes in this ring
NumNodes int
}
// RaftGetConfiguration is used to query the current Raft peer set.
func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) {
r := op.c.newRequest("GET", "/v1/operator/raft/configuration")
@ -79,3 +99,65 @@ func (op *Operator) RaftRemovePeerByAddress(address string, q *WriteOptions) err
resp.Body.Close()
return nil
}
// KeyringInstall is used to install a new gossip encryption key into the cluster
func (op *Operator) KeyringInstall(key string, q *WriteOptions) error {
r := op.c.newRequest("POST", "/v1/operator/keyring")
r.setWriteOptions(q)
r.obj = keyringRequest{
Key: key,
}
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// KeyringList is used to list the gossip keys installed in the cluster
func (op *Operator) KeyringList(q *QueryOptions) ([]*KeyringResponse, error) {
r := op.c.newRequest("GET", "/v1/operator/keyring")
r.setQueryOptions(q)
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out []*KeyringResponse
if err := decodeBody(resp, &out); err != nil {
return nil, err
}
return out, nil
}
// KeyringRemove is used to remove a gossip encryption key from the cluster
func (op *Operator) KeyringRemove(key string, q *WriteOptions) error {
r := op.c.newRequest("DELETE", "/v1/operator/keyring")
r.setWriteOptions(q)
r.obj = keyringRequest{
Key: key,
}
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}
// KeyringUse is used to change the active gossip encryption key
func (op *Operator) KeyringUse(key string, q *WriteOptions) error {
r := op.c.newRequest("PUT", "/v1/operator/keyring")
r.setWriteOptions(q)
r.obj = keyringRequest{
Key: key,
}
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return err
}
resp.Body.Close()
return nil
}

View File

@ -3,6 +3,8 @@ package api
import (
"strings"
"testing"
"github.com/hashicorp/consul/testutil"
)
func TestOperator_RaftGetConfiguration(t *testing.T) {
@ -36,3 +38,69 @@ func TestOperator_RaftRemovePeerByAddress(t *testing.T) {
t.Fatalf("err: %v", err)
}
}
func TestOperator_KeyringInstallListPutRemove(t *testing.T) {
oldKey := "d8wu8CSUrqgtjVsvcBPmhQ=="
newKey := "qxycTi/SsePj/TZzCBmNXw=="
t.Parallel()
c, s := makeClientWithConfig(t, nil, func(c *testutil.TestServerConfig) {
c.Encrypt = oldKey
})
defer s.Stop()
operator := c.Operator()
if err := operator.KeyringInstall(newKey, nil); err != nil {
t.Fatalf("err: %v", err)
}
listResponses, err := operator.KeyringList(nil)
if err != nil {
t.Fatalf("err %v", err)
}
// Make sure the new key is installed
if len(listResponses) != 2 {
t.Fatalf("bad: %v", len(listResponses))
}
for _, response := range listResponses {
if len(response.Keys) != 2 {
t.Fatalf("bad: %v", len(response.Keys))
}
if _, ok := response.Keys[oldKey]; !ok {
t.Fatalf("bad: %v", ok)
}
if _, ok := response.Keys[newKey]; !ok {
t.Fatalf("bad: %v", ok)
}
}
// Switch the primary to the new key
if err := operator.KeyringUse(newKey, nil); err != nil {
t.Fatalf("err: %v", err)
}
if err := operator.KeyringRemove(oldKey, nil); err != nil {
t.Fatalf("err: %v", err)
}
listResponses, err = operator.KeyringList(nil)
if err != nil {
t.Fatalf("err %v", err)
}
// Make sure the old key is removed
if len(listResponses) != 2 {
t.Fatalf("bad: %v", len(listResponses))
}
for _, response := range listResponses {
if len(response.Keys) != 1 {
t.Fatalf("bad: %v", len(response.Keys))
}
if _, ok := response.Keys[oldKey]; ok {
t.Fatalf("bad: %v", ok)
}
if _, ok := response.Keys[newKey]; !ok {
t.Fatalf("bad: %v", ok)
}
}
}

View File

@ -43,6 +43,11 @@ type ServiceQuery struct {
// this list it must be present. If the tag is preceded with "!" then
// it is disallowed.
Tags []string
// NodeMeta is a map of required node metadata fields. If a key/value
// pair is in this map it must be present on the node in order for the
// service entry to be returned.
NodeMeta map[string]string
}
// QueryTemplate carries the arguments for creating a templated query.
@ -167,19 +172,18 @@ func (c *PreparedQuery) Get(queryID string, q *QueryOptions) ([]*PreparedQueryDe
}
// Delete is used to delete a specific prepared query.
func (c *PreparedQuery) Delete(queryID string, q *QueryOptions) (*QueryMeta, error) {
func (c *PreparedQuery) Delete(queryID string, q *WriteOptions) (*WriteMeta, error) {
r := c.c.newRequest("DELETE", "/v1/query/"+queryID)
r.setQueryOptions(q)
r.setWriteOptions(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
wm := &WriteMeta{}
wm.RequestTime = rtt
return wm, nil
}
// Execute is used to execute a specific prepared query. You can execute using

View File

@ -20,6 +20,7 @@ func TestPreparedQuery(t *testing.T) {
TaggedAddresses: map[string]string{
"wan": "127.0.0.1",
},
NodeMeta: map[string]string{"somekey": "somevalue"},
Service: &AgentService{
ID: "redis1",
Service: "redis",
@ -45,8 +46,10 @@ func TestPreparedQuery(t *testing.T) {
// Create a simple prepared query.
def := &PreparedQueryDefinition{
Name: "test",
Service: ServiceQuery{
Service: "redis",
Service: "redis",
NodeMeta: map[string]string{"somekey": "somevalue"},
},
}

47
api/snapshot.go Normal file
View File

@ -0,0 +1,47 @@
package api
import (
"io"
)
// Snapshot can be used to query the /v1/snapshot endpoint to take snapshots of
// Consul's internal state and restore snapshots for disaster recovery.
type Snapshot struct {
c *Client
}
// Snapshot returns a handle that exposes the snapshot endpoints.
func (c *Client) Snapshot() *Snapshot {
return &Snapshot{c}
}
// Save requests a new snapshot and provides an io.ReadCloser with the snapshot
// data to save. If this doesn't return an error, then it's the responsibility
// of the caller to close it. Only a subset of the QueryOptions are supported:
// Datacenter, AllowStale, and Token.
func (s *Snapshot) Save(q *QueryOptions) (io.ReadCloser, *QueryMeta, error) {
r := s.c.newRequest("GET", "/v1/snapshot")
r.setQueryOptions(q)
rtt, resp, err := requireOK(s.c.doRequest(r))
if err != nil {
return nil, nil, err
}
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
return resp.Body, qm, nil
}
// Restore streams in an existing snapshot and attempts to restore it.
func (s *Snapshot) Restore(q *WriteOptions, in io.Reader) error {
r := s.c.newRequest("PUT", "/v1/snapshot")
r.body = in
r.setWriteOptions(q)
_, _, err := requireOK(s.c.doRequest(r))
if err != nil {
return err
}
return nil
}

134
api/snapshot_test.go Normal file
View File

@ -0,0 +1,134 @@
package api
import (
"bytes"
"strings"
"testing"
)
func TestSnapshot(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()
// Place an initial key into the store.
kv := c.KV()
key := &KVPair{Key: testKey(), Value: []byte("hello")}
if _, err := kv.Put(key, nil); err != nil {
t.Fatalf("err: %v", err)
}
// Make sure it reads back.
pair, _, err := kv.Get(key.Key, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if pair == nil {
t.Fatalf("expected value: %#v", pair)
}
if !bytes.Equal(pair.Value, []byte("hello")) {
t.Fatalf("unexpected value: %#v", pair)
}
// Take a snapshot.
snapshot := c.Snapshot()
snap, qm, err := snapshot.Save(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
defer snap.Close()
// Sanity check th query metadata.
if qm.LastIndex == 0 || !qm.KnownLeader ||
qm.RequestTime == 0 {
t.Fatalf("bad: %v", qm)
}
// Overwrite the key's value.
key.Value = []byte("goodbye")
if _, err := kv.Put(key, nil); err != nil {
t.Fatalf("err: %v", err)
}
// Read the key back and look for the new value.
pair, _, err = kv.Get(key.Key, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if pair == nil {
t.Fatalf("expected value: %#v", pair)
}
if !bytes.Equal(pair.Value, []byte("goodbye")) {
t.Fatalf("unexpected value: %#v", pair)
}
// Restore the snapshot.
if err := snapshot.Restore(nil, snap); err != nil {
t.Fatalf("err: %v", err)
}
// Read the key back and look for the original value.
pair, _, err = kv.Get(key.Key, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if pair == nil {
t.Fatalf("expected value: %#v", pair)
}
if !bytes.Equal(pair.Value, []byte("hello")) {
t.Fatalf("unexpected value: %#v", pair)
}
}
func TestSnapshot_Options(t *testing.T) {
t.Parallel()
c, s := makeACLClient(t)
defer s.Stop()
// Try to take a snapshot with a bad token.
snapshot := c.Snapshot()
_, _, err := snapshot.Save(&QueryOptions{Token: "anonymous"})
if err == nil || !strings.Contains(err.Error(), "Permission denied") {
t.Fatalf("err: %v", err)
}
// Now try an unknown DC.
_, _, err = snapshot.Save(&QueryOptions{Datacenter: "nope"})
if err == nil || !strings.Contains(err.Error(), "No path to datacenter") {
t.Fatalf("err: %v", err)
}
// This should work with a valid token.
snap, _, err := snapshot.Save(&QueryOptions{Token: "root"})
if err != nil {
t.Fatalf("err: %v", err)
}
defer snap.Close()
// This should work with a stale snapshot. This doesn't have good feedback
// that the stale option was sent, but it makes sure nothing bad happens.
snap, _, err = snapshot.Save(&QueryOptions{Token: "root", AllowStale: true})
if err != nil {
t.Fatalf("err: %v", err)
}
defer snap.Close()
// Try to restore a snapshot with a bad token.
null := bytes.NewReader([]byte(""))
err = snapshot.Restore(&WriteOptions{Token: "anonymous"}, null)
if err == nil || !strings.Contains(err.Error(), "Permission denied") {
t.Fatalf("err: %v", err)
}
// Now try an unknown DC.
null = bytes.NewReader([]byte(""))
err = snapshot.Restore(&WriteOptions{Datacenter: "nope"}, null)
if err == nil || !strings.Contains(err.Error(), "No path to datacenter") {
t.Fatalf("err: %v", err)
}
// This should work.
if err := snapshot.Restore(&WriteOptions{Token: "root"}, snap); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -1,4 +1,4 @@
REQ=20480
REQ=262144
CLIENTS=64
ADDR=http://127.0.0.1:8500/v1/kv/bench
DATA="74a31e96-1d0f-4fa7-aa14-7212a326986e"

View File

@ -1,13 +1,14 @@
Consul Benchmark
================
This repo contains the Packer automation necessary for the Consul benchmarks.
This repo contains the automation necessary for the Consul benchmarks.
There is a single main Packer file `bench.json`. To use it, the variables
for `do_client_id` and `do_api_key` must be provided. There correspond to
your DigitalOcean client id and API key.
for `do_client_id` and `do_api_key` must be provided. These correspond to
your DigitalOcean client ID and API key.
When Packer runs, it will generate 3 images:
* bench-bootstrap - Consul server in bootstrap mode
* bench-server - Consul server
* bench-worker - Worker node

View File

@ -1,86 +1,81 @@
{
"variables": {
"do_client_id": "",
"do_api_key": ""
},
"builders": [
{
"type": "digitalocean",
"api_key": "{{ user `do_api_key` }}",
"client_id": "{{ user `do_client_id` }}",
"region_id": "1",
"size_id": "66",
"image_id": "3101045",
"snapshot_name": "bench-bootstrap-{{ isotime }}",
"name": "bootstrap"
"variables": {
"do_size": "16gb",
"do_image": "ubuntu-14-04-x64",
"do_region": "nyc3"
},
"builders": [
{
"type": "digitalocean",
"region": "{{ user `do_region` }}",
"size": "{{ user `do_size` }}",
"image": "{{ user `do_image` }}",
"snapshot_name": "bench-bootstrap-{{ isotime }}",
"name": "bootstrap"
},
{
"type": "digitalocean",
"api_key": "{{ user `do_api_key` }}",
"client_id": "{{ user `do_client_id` }}",
"region_id": "1",
"size_id": "66",
"image_id": "3101045",
"snapshot_name": "bench-server-{{ isotime }}",
"name": "server"
"type": "digitalocean",
"region": "{{ user `do_region` }}",
"size": "{{ user `do_size` }}",
"image": "{{ user `do_image` }}",
"snapshot_name": "bench-server-{{ isotime }}",
"name": "server"
},
{
"type": "digitalocean",
"api_key": "{{ user `do_api_key` }}",
"client_id": "{{ user `do_client_id` }}",
"region_id": "1",
"size_id": "66",
"image_id": "3101045",
"snapshot_name": "bench-worker-{{ isotime }}",
"name": "worker"
"type": "digitalocean",
"region": "{{ user `do_region` }}",
"size": "{{ user `do_size` }}",
"image": "{{ user `do_image` }}",
"snapshot_name": "bench-worker-{{ isotime }}",
"name": "worker"
}
],
"provisioners":[
],
"provisioners":[
{
"type": "file",
"source": "conf/upstart.conf",
"destination": "/etc/init/consul.conf"
"type": "file",
"source": "conf/upstart.conf",
"destination": "/etc/init/consul.conf"
},
{
"type": "shell",
"inline": [
"mkdir /etc/consul.d",
"apt-get update",
"apt-get install unzip make",
"wget https://releases.hashicorp.com/consul/0.5.2/consul_0.5.2_linux_amd64.zip",
"unzip 0.5.2_linux_amd64.zip",
"mv consul /usr/local/bin/consul",
"chmod +x /usr/local/bin/consul"
]
"type": "shell",
"inline": [
"mkdir /etc/consul.d",
"apt-get update",
"apt-get install -y unzip make",
"wget https://releases.hashicorp.com/consul/0.7.1/consul_0.7.1_linux_amd64.zip",
"unzip consul_*_linux_amd64.zip",
"mv consul /usr/local/bin/consul",
"chmod +x /usr/local/bin/consul"
]
},
{
"type": "file",
"source": "conf/common.json",
"destination": "/etc/consul.d/common.json"
"type": "file",
"source": "conf/common.json",
"destination": "/etc/consul.d/common.json"
},
{
"type": "file",
"source": "conf/bootstrap.json",
"destination": "/etc/consul.d/bootstrap.json",
"only": ["bootstrap"]
"type": "file",
"source": "conf/bootstrap.json",
"destination": "/etc/consul.d/bootstrap.json",
"only": ["bootstrap"]
},
{
"type": "file",
"source": "conf/server.json",
"destination": "/etc/consul.d/server.json",
"only": ["server"]
"type": "file",
"source": "conf/server.json",
"destination": "/etc/consul.d/server.json",
"only": ["server"]
},
{
"type": "shell",
"inline": [
"curl https://s3.amazonaws.com/hc-ops/boom_linux_amd64 -o /usr/local/bin/boom",
"chmod +x /usr/local/bin/boom"
]
"type": "shell",
"inline": [
"curl https://s3.amazonaws.com/hc-ops/boom_linux_amd64 -o /usr/local/bin/boom",
"chmod +x /usr/local/bin/boom"
]
},
{
"type": "file",
"source": "Makefile",
"destination": "/Makefile"
"type": "file",
"source": "Makefile",
"destination": "/Makefile"
}
]
]
}

223
bench/results-0.7.1.md Normal file
View File

@ -0,0 +1,223 @@
# Consul Benchmark Results
As part of a benchmark, we started a 4 node DigitalOcean cluster to benchmark.
There are 3 servers, meaning writes must commit to at least 2 servers.
The cluster uses the 16GB DigitalOcean droplet which has the following specs:
* 8 CPU Cores, 2Ghz
* 16GB RAM
* 160GB SSD disk
* 1Gbps NIC
# Output
Below is the output for a test run on a benchmark cluster. We ran the benchmark
several times to warm up the nodes, and this is just a single representative sample.
Note, that a single worker was running the benchmark. This means the "stale" test
is not representative of total throughput, as the client was only routing to a
single server.
We also did an initial run where we got lots of noise in the results, so we
increased the number of requests to try to get a better sample.
```
===== PUT test =====
GOMAXPROCS=4 boom -m PUT -d "74a31e96-1d0f-4fa7-aa14-7212a326986e" -n 262144 -c 64 http://127.0.0.1:8500/v1/kv/bench
262144 / 262144 Booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! 100.00 %
Summary:
Total: 69.3512 secs.
Slowest: 0.0966 secs.
Fastest: 0.0026 secs.
Average: 0.0169 secs.
Requests/sec: 3779.9491
Total Data Received: 1048576 bytes.
Response Size per Request: 4 bytes.
Status code distribution:
[200] 262144 responses
Response time histogram:
0.003 [1] |
0.012 [66586] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.021 [146064] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.031 [34189] |∎∎∎∎∎∎∎∎∎
0.040 [9178] |∎∎
0.050 [3682] |∎
0.059 [1773] |
0.068 [464] |
0.078 [124] |
0.087 [63] |
0.097 [20] |
Latency distribution:
10% in 0.0095 secs.
25% in 0.0119 secs.
50% in 0.0151 secs.
75% in 0.0195 secs.
90% in 0.0260 secs.
95% in 0.0323 secs.
99% in 0.0489 secs.
===== GET default test =====
GOMAXPROCS=4 boom -n 262144 -c 64 http://127.0.0.1:8500/v1/kv/bench
262144 / 262144 Booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! 100.00 %
Summary:
Total: 34.8371 secs.
Slowest: 0.9568 secs.
Fastest: 0.0014 secs.
Average: 0.0085 secs.
Requests/sec: 7524.8570
Total Data Received: 36175872 bytes.
Response Size per Request: 138 bytes.
Status code distribution:
[200] 262144 responses
Response time histogram:
0.001 [1] |
0.097 [261977] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.192 [38] |
0.288 [64] |
0.384 [0] |
0.479 [0] |
0.575 [0] |
0.670 [0] |
0.766 [0] |
0.861 [38] |
0.957 [26] |
Latency distribution:
10% in 0.0044 secs.
25% in 0.0055 secs.
50% in 0.0072 secs.
75% in 0.0098 secs.
90% in 0.0130 secs.
95% in 0.0157 secs.
99% in 0.0228 secs.
===== GET stale test =====
GOMAXPROCS=4 boom -n 262144 -c 64 http://127.0.0.1:8500/v1/kv/bench?stale
262144 / 262144 Booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! 100.00 %
Summary:
Total: 26.8200 secs.
Slowest: 0.0838 secs.
Fastest: 0.0005 secs.
Average: 0.0065 secs.
Requests/sec: 9774.1922
Total Data Received: 36175872 bytes.
Response Size per Request: 138 bytes.
Status code distribution:
[200] 262144 responses
Response time histogram:
0.001 [1] |
0.009 [214210] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.017 [42999] |∎∎∎∎∎∎∎∎
0.026 [3709] |
0.034 [589] |
0.042 [313] |
0.050 [166] |
0.059 [102] |
0.067 [42] |
0.075 [11] |
0.084 [2] |
Latency distribution:
10% in 0.0031 secs.
25% in 0.0041 secs.
50% in 0.0056 secs.
75% in 0.0079 secs.
90% in 0.0109 secs.
95% in 0.0134 secs.
99% in 0.0203 secs.
===== GET consistent test =====
GOMAXPROCS=4 boom -n 262144 -c 64 http://127.0.0.1:8500/v1/kv/bench?consistent
262144 / 262144 Booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! 100.00 %
Summary:
Total: 35.6962 secs.
Slowest: 0.0826 secs.
Fastest: 0.0016 secs.
Average: 0.0087 secs.
Requests/sec: 7343.7475
Total Data Received: 36175872 bytes.
Response Size per Request: 138 bytes.
Status code distribution:
[200] 262144 responses
Response time histogram:
0.002 [1] |
0.010 [183123] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.018 [70460] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.026 [6955] |∎
0.034 [657] |
0.042 [391] |
0.050 [229] |
0.058 [120] |
0.066 [121] |
0.074 [68] |
0.083 [19] |
Latency distribution:
10% in 0.0047 secs.
25% in 0.0059 secs.
50% in 0.0077 secs.
75% in 0.0104 secs.
90% in 0.0137 secs.
95% in 0.0162 secs.
99% in 0.0227 secs.
```
# Profile
In order to probe performance a bit, we ran the get-stale benchmark on the
leader itself and collected pprof data. Here's the output of the benchmark:
```
===== GET stale test =====
GOMAXPROCS=4 boom -n 262144 -c 64 http://127.0.0.1:8500/v1/kv/bench?stale
262144 / 262144 Booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! 100.00 %
Summary:
Total: 16.3139 secs.
Slowest: 0.0815 secs.
Fastest: 0.0001 secs.
Average: 0.0040 secs.
Requests/sec: 16068.7946
Total Data Received: 36175872 bytes.
Response Size per Request: 138 bytes.
Status code distribution:
[200] 262144 responses
Response time histogram:
0.000 [1] |
0.008 [240221] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
0.016 [18761] |∎∎∎
0.025 [1937] |
0.033 [496] |
0.041 [293] |
0.049 [131] |
0.057 [162] |
0.065 [127] |
0.073 [10] |
0.081 [5] |
Latency distribution:
10% in 0.0013 secs.
25% in 0.0019 secs.
50% in 0.0030 secs.
75% in 0.0046 secs.
90% in 0.0074 secs.
95% in 0.0109 secs.
99% in 0.0174 secs.
```
And here's the [resulting flame graph](results-0.7.1.svg).

8395
bench/results-0.7.1.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 678 KiB

452
command/agent/acl.go Normal file
View File

@ -0,0 +1,452 @@
package agent
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/golang-lru"
"github.com/hashicorp/serf/serf"
)
// There's enough behavior difference with client-side ACLs that we've
// intentionally kept this code separate from the server-side ACL code in
// consul/acl.go. We may refactor some of the caching logic in the future,
// but for now we are developing this separately to see how things shake out.
// These must be kept in sync with the constants in consul/acl.go.
const (
// aclNotFound indicates there is no matching ACL.
aclNotFound = "ACL not found"
// rootDenied is returned when attempting to resolve a root ACL.
rootDenied = "Cannot resolve root ACL"
// permissionDenied is returned when an ACL based rejection happens.
permissionDenied = "Permission denied"
// aclDisabled is returned when ACL changes are not permitted since they
// are disabled.
aclDisabled = "ACL support disabled"
// anonymousToken is the token ID we re-write to if there is no token ID
// provided.
anonymousToken = "anonymous"
// Maximum number of cached ACL entries.
aclCacheSize = 10 * 1024
)
var (
permissionDeniedErr = errors.New(permissionDenied)
)
// aclCacheEntry is used to cache ACL tokens.
type aclCacheEntry struct {
// ACL is the cached ACL.
ACL acl.ACL
// Expires is set based on the TTL for the ACL.
Expires time.Time
// ETag is used as an optimization when fetching ACLs from servers to
// avoid transmitting data back when the agent has a good copy, which is
// usually the case when refreshing a TTL.
ETag string
}
// aclManager is used by the agent to keep track of state related to ACLs,
// including caching tokens from the servers. This has some internal state that
// we don't want to dump into the agent itself.
type aclManager struct {
// acls is a cache mapping ACL tokens to compiled policies.
acls *lru.TwoQueueCache
// master is the ACL to use when the agent master token is supplied.
// This may be nil if that option isn't set in the agent config.
master acl.ACL
// down is the ACL to use when the servers are down. This may be nil
// which means to try and use the cached policy if there is one (or
// deny if there isn't a policy in the cache).
down acl.ACL
// disabled is used to keep track of feedback from the servers that ACLs
// are disabled. If the manager discovers that ACLs are disabled, this
// will be set to the next time we should check to see if they have been
// enabled. This helps cut useless traffic, but allows us to turn on ACL
// support at the servers without having to restart the whole cluster.
disabled time.Time
disabledLock sync.RWMutex
}
// newACLManager returns an ACL manager based on the given config.
func newACLManager(config *Config) (*aclManager, error) {
// Set up the cache from ID to ACL (we don't cache policies like the
// servers; only one level).
acls, err := lru.New2Q(aclCacheSize)
if err != nil {
return nil, err
}
// If an agent master token is configured, build a policy and ACL for
// it, otherwise leave it nil.
var master acl.ACL
if len(config.ACLAgentMasterToken) > 0 {
policy := &acl.Policy{
Agents: []*acl.AgentPolicy{
&acl.AgentPolicy{
Node: config.NodeName,
Policy: acl.PolicyWrite,
},
},
}
acl, err := acl.New(acl.DenyAll(), policy)
if err != nil {
return nil, err
}
master = acl
}
var down acl.ACL
switch config.ACLDownPolicy {
case "allow":
down = acl.AllowAll()
case "deny":
down = acl.DenyAll()
case "extend-cache":
// Leave the down policy as nil to signal this.
default:
return nil, fmt.Errorf("invalid ACL down policy %q", config.ACLDownPolicy)
}
// Give back a manager.
return &aclManager{
acls: acls,
master: master,
down: down,
}, nil
}
// isDisabled returns true if the manager has discovered that ACLs are disabled
// on the servers.
func (m *aclManager) isDisabled() bool {
m.disabledLock.RLock()
defer m.disabledLock.RUnlock()
return time.Now().Before(m.disabled)
}
// lookupACL attempts to locate the compiled policy associated with the given
// token. The agent may be used to perform RPC calls to the servers to fetch
// policies that aren't in the cache.
func (m *aclManager) lookupACL(agent *Agent, id string) (acl.ACL, error) {
// Handle some special cases for the ID.
if len(id) == 0 {
id = anonymousToken
} else if acl.RootACL(id) != nil {
return nil, errors.New(rootDenied)
} else if m.master != nil && id == agent.config.ACLAgentMasterToken {
return m.master, nil
}
// Try the cache first.
var cached *aclCacheEntry
if raw, ok := m.acls.Get(id); ok {
cached = raw.(*aclCacheEntry)
}
if cached != nil && time.Now().Before(cached.Expires) {
metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1)
return cached.ACL, nil
} else {
metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1)
}
// At this point we might have a stale cached ACL, or none at all, so
// try to contact the servers.
args := structs.ACLPolicyRequest{
Datacenter: agent.config.Datacenter,
ACL: id,
}
if cached != nil {
args.ETag = cached.ETag
}
var reply structs.ACLPolicy
err := agent.RPC(agent.getEndpoint("ACL")+".GetPolicy", &args, &reply)
if err != nil {
if strings.Contains(err.Error(), aclDisabled) {
agent.logger.Printf("[DEBUG] agent: ACLs disabled on servers, will check again after %s", agent.config.ACLDisabledTTL)
m.disabledLock.Lock()
m.disabled = time.Now().Add(agent.config.ACLDisabledTTL)
m.disabledLock.Unlock()
return nil, nil
} else if strings.Contains(err.Error(), aclNotFound) {
return nil, errors.New(aclNotFound)
} else {
agent.logger.Printf("[DEBUG] agent: Failed to get policy for ACL from servers: %v", err)
if m.down != nil {
return m.down, nil
} else if cached != nil {
return cached.ACL, nil
} else {
return acl.DenyAll(), nil
}
}
}
// Use the old cached compiled ACL if we can, otherwise compile it and
// resolve any parents.
var compiled acl.ACL
if cached != nil && cached.ETag == reply.ETag {
compiled = cached.ACL
} else {
parent := acl.RootACL(reply.Parent)
if parent == nil {
parent, err = m.lookupACL(agent, reply.Parent)
if err != nil {
return nil, err
}
}
acl, err := acl.New(parent, reply.Policy)
if err != nil {
return nil, err
}
compiled = acl
}
// Update the cache.
cached = &aclCacheEntry{
ACL: compiled,
ETag: reply.ETag,
}
if reply.TTL > 0 {
cached.Expires = time.Now().Add(reply.TTL)
}
m.acls.Add(id, cached)
return compiled, nil
}
// resolveToken is the primary interface used by ACL-checkers in the agent
// endpoints, which is the one place where we do some ACL enforcement on
// clients. Some of the enforcement is normative (e.g. self and monitor)
// and some is informative (e.g. catalog and health).
func (a *Agent) resolveToken(id string) (acl.ACL, error) {
// Disable ACLs if version 8 enforcement isn't enabled.
if !(*a.config.ACLEnforceVersion8) {
return nil, nil
}
// Bail if the ACL manager is disabled. This happens if it gets feedback
// from the servers that ACLs are disabled.
if a.acls.isDisabled() {
return nil, nil
}
// This will look in the cache and fetch from the servers if necessary.
return a.acls.lookupACL(a, id)
}
// vetServiceRegister makes sure the service registration action is allowed by
// the given token.
func (a *Agent) vetServiceRegister(token string, service *structs.NodeService) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet the service itself.
if !acl.ServiceWrite(service.Service) {
return permissionDeniedErr
}
// Vet any service that might be getting overwritten.
services := a.state.Services()
if existing, ok := services[service.ID]; ok {
if !acl.ServiceWrite(existing.Service) {
return permissionDeniedErr
}
}
return nil
}
// vetServiceUpdate makes sure the service update action is allowed by the given
// token.
func (a *Agent) vetServiceUpdate(token string, serviceID string) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet any changes based on the existing services's info.
services := a.state.Services()
if existing, ok := services[serviceID]; ok {
if !acl.ServiceWrite(existing.Service) {
return permissionDeniedErr
}
} else {
return fmt.Errorf("Unknown service %q", serviceID)
}
return nil
}
// vetCheckRegister makes sure the check registration action is allowed by the
// given token.
func (a *Agent) vetCheckRegister(token string, check *structs.HealthCheck) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet the check itself.
if len(check.ServiceName) > 0 {
if !acl.ServiceWrite(check.ServiceName) {
return permissionDeniedErr
}
} else {
if !acl.NodeWrite(a.config.NodeName) {
return permissionDeniedErr
}
}
// Vet any check that might be getting overwritten.
checks := a.state.Checks()
if existing, ok := checks[check.CheckID]; ok {
if len(existing.ServiceName) > 0 {
if !acl.ServiceWrite(existing.ServiceName) {
return permissionDeniedErr
}
} else {
if !acl.NodeWrite(a.config.NodeName) {
return permissionDeniedErr
}
}
}
return nil
}
// vetCheckUpdate makes sure that a check update is allowed by the given token.
func (a *Agent) vetCheckUpdate(token string, checkID types.CheckID) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Vet any changes based on the existing check's info.
checks := a.state.Checks()
if existing, ok := checks[checkID]; ok {
if len(existing.ServiceName) > 0 {
if !acl.ServiceWrite(existing.ServiceName) {
return permissionDeniedErr
}
} else {
if !acl.NodeWrite(a.config.NodeName) {
return permissionDeniedErr
}
}
} else {
return fmt.Errorf("Unknown check %q", checkID)
}
return nil
}
// filterMembers redacts members that the token doesn't have access to.
func (a *Agent) filterMembers(token string, members *[]serf.Member) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out members based on the node policy.
m := *members
for i := 0; i < len(m); i++ {
node := m[i].Name
if acl.NodeRead(node) {
continue
}
a.logger.Printf("[DEBUG] agent: dropping node %q from result due to ACLs", node)
m = append(m[:i], m[i+1:]...)
i--
}
*members = m
return nil
}
// filterServices redacts services that the token doesn't have access to.
func (a *Agent) filterServices(token string, services *map[string]*structs.NodeService) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out services based on the service policy.
for id, service := range *services {
if acl.ServiceRead(service.Service) {
continue
}
a.logger.Printf("[DEBUG] agent: dropping service %q from result due to ACLs", id)
delete(*services, id)
}
return nil
}
// filterChecks redacts checks that the token doesn't have access to.
func (a *Agent) filterChecks(token string, checks *map[types.CheckID]*structs.HealthCheck) error {
// Resolve the token and bail if ACLs aren't enabled.
acl, err := a.resolveToken(token)
if err != nil {
return err
}
if acl == nil {
return nil
}
// Filter out checks based on the node or service policy.
for id, check := range *checks {
if len(check.ServiceName) > 0 {
if acl.ServiceRead(check.ServiceName) {
continue
}
} else {
if acl.NodeRead(a.config.NodeName) {
continue
}
}
a.logger.Printf("[DEBUG] agent: dropping check %q from result due to ACLs", id)
delete(*checks, id)
}
return nil
}

View File

@ -13,8 +13,8 @@ type aclCreateResponse struct {
ID string
}
// aclDisabled handles if ACL datacenter is not configured
func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// ACLDisabled handles if ACL datacenter is not configured
func ACLDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
resp.WriteHeader(401)
resp.Write([]byte("ACL support disabled"))
return nil, nil

863
command/agent/acl_test.go Normal file
View File

@ -0,0 +1,863 @@
package agent
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"time"
rawacl "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/serf/serf"
)
func TestACL_Bad_Config(t *testing.T) {
config := nextConfig()
config.ACLDownPolicy = "nope"
var err error
config.DataDir, err = ioutil.TempDir("", "agent")
if err != nil {
t.Fatalf("err: %v", err)
}
defer os.RemoveAll(config.DataDir)
_, err = Create(config, nil, nil, nil)
if err == nil || !strings.Contains(err.Error(), "invalid ACL down policy") {
t.Fatalf("err: %v", err)
}
}
type MockServer struct {
getPolicyFn func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error
}
func (m *MockServer) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
if m.getPolicyFn != nil {
return m.getPolicyFn(args, reply)
} else {
return fmt.Errorf("should not have called GetPolicy")
}
}
func TestACL_Version8(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(false)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// With version 8 enforcement off, this should not get called.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
t.Fatalf("should not have called to server")
return nil
}
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
t.Fatalf("bad: %v err: %v", token, err)
}
}
func TestACL_Disabled(t *testing.T) {
config := nextConfig()
config.ACLDisabledTTL = 10 * time.Millisecond
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Fetch a token without ACLs enabled and make sure the manager sees it.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
return errors.New(aclDisabled)
}
if agent.acls.isDisabled() {
t.Fatalf("should not be disabled yet")
}
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
t.Fatalf("bad: %v err: %v", token, err)
}
if !agent.acls.isDisabled() {
t.Fatalf("should be disabled")
}
// Now turn on ACLs and check right away, it should still think ACLs are
// disabled since we don't check again right away.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
return errors.New(aclNotFound)
}
if token, err := agent.resolveToken("nope"); token != nil || err != nil {
t.Fatalf("bad: %v err: %v", token, err)
}
if !agent.acls.isDisabled() {
t.Fatalf("should be disabled")
}
// Wait the waiting period and make sure it checks again. Do a few tries
// to make sure we don't think it's disabled.
time.Sleep(2 * config.ACLDisabledTTL)
for i := 0; i < 10; i++ {
_, err := agent.resolveToken("nope")
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
t.Fatalf("err: %v", err)
}
if agent.acls.isDisabled() {
t.Fatalf("should not be disabled")
}
}
}
func TestACL_Special_IDs(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
config.ACLAgentMasterToken = "towel"
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// An empty ID should get mapped to the anonymous token.
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
if req.ACL != "anonymous" {
t.Fatalf("bad: %#v", *req)
}
return errors.New(aclNotFound)
}
_, err := agent.resolveToken("")
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
t.Fatalf("err: %v", err)
}
// A root ACL request should get rejected and not call the server.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
t.Fatalf("should not have called to server")
return nil
}
_, err = agent.resolveToken("deny")
if err == nil || !strings.Contains(err.Error(), rootDenied) {
t.Fatalf("err: %v", err)
}
// The ACL master token should also not call the server, but should give
// us a working agent token.
acl, err := agent.resolveToken("towel")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should be able to read agent")
}
if !acl.AgentWrite(config.NodeName) {
t.Fatalf("should be able to write agent")
}
}
func TestACL_Down_Deny(t *testing.T) {
config := nextConfig()
config.ACLDownPolicy = "deny"
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Resolve with ACLs down.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
return fmt.Errorf("ACLs are broken")
}
acl, err := agent.resolveToken("nope")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if acl.AgentRead(config.NodeName) {
t.Fatalf("should deny")
}
}
func TestACL_Down_Allow(t *testing.T) {
config := nextConfig()
config.ACLDownPolicy = "allow"
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Resolve with ACLs down.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
return fmt.Errorf("ACLs are broken")
}
acl, err := agent.resolveToken("nope")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
}
func TestACL_Down_Extend(t *testing.T) {
config := nextConfig()
config.ACLDownPolicy = "extend-cache"
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Populate the cache for one of the tokens.
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
*reply = structs.ACLPolicy{
Parent: "allow",
Policy: &rawacl.Policy{
Agents: []*rawacl.AgentPolicy{
&rawacl.AgentPolicy{
Node: config.NodeName,
Policy: "read",
},
},
},
}
return nil
}
acl, err := agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if acl.AgentWrite(config.NodeName) {
t.Fatalf("should deny")
}
// Now take down ACLs and make sure a new token fails to resolve.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
return fmt.Errorf("ACLs are broken")
}
acl, err = agent.resolveToken("nope")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if acl.AgentRead(config.NodeName) {
t.Fatalf("should deny")
}
if acl.AgentWrite(config.NodeName) {
t.Fatalf("should deny")
}
// Read the token from the cache while ACLs are broken, which should
// extend.
acl, err = agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if acl.AgentWrite(config.NodeName) {
t.Fatalf("should deny")
}
}
func TestACL_Cache(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Populate the cache for one of the tokens.
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
*reply = structs.ACLPolicy{
ETag: "hash1",
Parent: "deny",
Policy: &rawacl.Policy{
Agents: []*rawacl.AgentPolicy{
&rawacl.AgentPolicy{
Node: config.NodeName,
Policy: "read",
},
},
},
TTL: 10 * time.Millisecond,
}
return nil
}
acl, err := agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if acl.AgentWrite(config.NodeName) {
t.Fatalf("should deny")
}
if acl.NodeRead("nope") {
t.Fatalf("should deny")
}
// Fetch right away and make sure it uses the cache.
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
t.Fatalf("should not have called to server")
return nil
}
acl, err = agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if acl.AgentWrite(config.NodeName) {
t.Fatalf("should deny")
}
if acl.NodeRead("nope") {
t.Fatalf("should deny")
}
// Wait for the TTL to expire and try again. This time the token will be
// gone.
time.Sleep(20 * time.Millisecond)
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
return errors.New(aclNotFound)
}
_, err = agent.resolveToken("yep")
if err == nil || !strings.Contains(err.Error(), aclNotFound) {
t.Fatalf("err: %v", err)
}
// Page it back in with a new tag and different policy
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
*reply = structs.ACLPolicy{
ETag: "hash2",
Parent: "deny",
Policy: &rawacl.Policy{
Agents: []*rawacl.AgentPolicy{
&rawacl.AgentPolicy{
Node: config.NodeName,
Policy: "write",
},
},
},
TTL: 10 * time.Millisecond,
}
return nil
}
acl, err = agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if !acl.AgentWrite(config.NodeName) {
t.Fatalf("should allow")
}
if acl.NodeRead("nope") {
t.Fatalf("should deny")
}
// Wait for the TTL to expire and try again. This will match the tag
// and not send the policy back, but we should have the old token
// behavior.
time.Sleep(20 * time.Millisecond)
var didRefresh bool
m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
*reply = structs.ACLPolicy{
ETag: "hash2",
TTL: 10 * time.Millisecond,
}
didRefresh = true
return nil
}
acl, err = agent.resolveToken("yep")
if err != nil {
t.Fatalf("err: %v", err)
}
if acl == nil {
t.Fatalf("should not be nil")
}
if !acl.AgentRead(config.NodeName) {
t.Fatalf("should allow")
}
if !acl.AgentWrite(config.NodeName) {
t.Fatalf("should allow")
}
if acl.NodeRead("nope") {
t.Fatalf("should deny")
}
if !didRefresh {
t.Fatalf("should refresh")
}
}
// catalogPolicy supplies some standard policies to help with testing the
// catalog-related vet and filter functions.
func catalogPolicy(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
reply.Policy = &rawacl.Policy{}
switch req.ACL {
case "node-ro":
reply.Policy.Nodes = append(reply.Policy.Nodes,
&rawacl.NodePolicy{Name: "Node", Policy: "read"})
case "node-rw":
reply.Policy.Nodes = append(reply.Policy.Nodes,
&rawacl.NodePolicy{Name: "Node", Policy: "write"})
case "service-ro":
reply.Policy.Services = append(reply.Policy.Services,
&rawacl.ServicePolicy{Name: "service", Policy: "read"})
case "service-rw":
reply.Policy.Services = append(reply.Policy.Services,
&rawacl.ServicePolicy{Name: "service", Policy: "write"})
case "other-rw":
reply.Policy.Services = append(reply.Policy.Services,
&rawacl.ServicePolicy{Name: "other", Policy: "write"})
default:
return fmt.Errorf("unknown token %q", req.ACL)
}
return nil
}
func TestACL_vetServiceRegister(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Register a new service, with permission.
err := agent.vetServiceRegister("service-rw", &structs.NodeService{
ID: "my-service",
Service: "service",
})
if err != nil {
t.Fatalf("err: %v", err)
}
// Register a new service without write privs.
err = agent.vetServiceRegister("service-ro", &structs.NodeService{
ID: "my-service",
Service: "service",
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Try to register over a service without write privs to the existing
// service.
agent.state.AddService(&structs.NodeService{
ID: "my-service",
Service: "other",
}, "")
err = agent.vetServiceRegister("service-rw", &structs.NodeService{
ID: "my-service",
Service: "service",
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestACL_vetServiceUpdate(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Update a service that doesn't exist.
err := agent.vetServiceUpdate("service-rw", "my-service")
if err == nil || !strings.Contains(err.Error(), "Unknown service") {
t.Fatalf("err: %v", err)
}
// Update with write privs.
agent.state.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
err = agent.vetServiceUpdate("service-rw", "my-service")
if err != nil {
t.Fatalf("err: %v", err)
}
// Update without write privs.
err = agent.vetServiceUpdate("service-ro", "my-service")
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestACL_vetCheckRegister(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Register a new service check with write privs.
err := agent.vetCheckRegister("service-rw", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
if err != nil {
t.Fatalf("err: %v", err)
}
// Register a new service check without write privs.
err = agent.vetCheckRegister("service-ro", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Register a new node check with write privs.
err = agent.vetCheckRegister("node-rw", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
})
if err != nil {
t.Fatalf("err: %v", err)
}
// Register a new node check without write privs.
err = agent.vetCheckRegister("node-ro", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Try to register over a service check without write privs to the
// existing service.
agent.state.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
agent.state.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "other",
}, "")
err = agent.vetCheckRegister("service-rw", &structs.HealthCheck{
CheckID: types.CheckID("my-check"),
ServiceID: "my-service",
ServiceName: "service",
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Try to register over a node check without write privs to the node.
agent.state.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
}, "")
err = agent.vetCheckRegister("service-rw", &structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
ServiceID: "my-service",
ServiceName: "service",
})
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestACL_vetCheckUpdate(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
// Update a check that doesn't exist.
err := agent.vetCheckUpdate("node-rw", "my-check")
if err == nil || !strings.Contains(err.Error(), "Unknown check") {
t.Fatalf("err: %v", err)
}
// Update service check with write privs.
agent.state.AddService(&structs.NodeService{
ID: "my-service",
Service: "service",
}, "")
agent.state.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-service-check"),
ServiceID: "my-service",
ServiceName: "service",
}, "")
err = agent.vetCheckUpdate("service-rw", "my-service-check")
if err != nil {
t.Fatalf("err: %v", err)
}
// Update service check without write privs.
err = agent.vetCheckUpdate("service-ro", "my-service-check")
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
// Update node check with write privs.
agent.state.AddCheck(&structs.HealthCheck{
CheckID: types.CheckID("my-node-check"),
}, "")
err = agent.vetCheckUpdate("node-rw", "my-node-check")
if err != nil {
t.Fatalf("err: %v", err)
}
// Update without write privs.
err = agent.vetCheckUpdate("node-ro", "my-node-check")
if err == nil || !strings.Contains(err.Error(), permissionDenied) {
t.Fatalf("err: %v", err)
}
}
func TestACL_filterMembers(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
var members []serf.Member
if err := agent.filterMembers("node-ro", &members); err != nil {
t.Fatalf("err: %v", err)
}
if len(members) != 0 {
t.Fatalf("bad: %#v", members)
}
members = []serf.Member{
serf.Member{Name: "Node 1"},
serf.Member{Name: "Nope"},
serf.Member{Name: "Node 2"},
}
if err := agent.filterMembers("node-ro", &members); err != nil {
t.Fatalf("err: %v", err)
}
if len(members) != 2 ||
members[0].Name != "Node 1" ||
members[1].Name != "Node 2" {
t.Fatalf("bad: %#v", members)
}
}
func TestACL_filterServices(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
services := make(map[string]*structs.NodeService)
if err := agent.filterServices("node-ro", &services); err != nil {
t.Fatalf("err: %v", err)
}
services["my-service"] = &structs.NodeService{ID: "my-service", Service: "service"}
services["my-other"] = &structs.NodeService{ID: "my-other", Service: "other"}
if err := agent.filterServices("service-ro", &services); err != nil {
t.Fatalf("err: %v", err)
}
if _, ok := services["my-service"]; !ok {
t.Fatalf("bad: %#v", services)
}
if _, ok := services["my-other"]; ok {
t.Fatalf("bad: %#v", services)
}
}
func TestACL_filterChecks(t *testing.T) {
config := nextConfig()
config.ACLEnforceVersion8 = Bool(true)
dir, agent := makeAgent(t, config)
defer os.RemoveAll(dir)
defer agent.Shutdown()
testutil.WaitForLeader(t, agent.RPC, "dc1")
m := MockServer{catalogPolicy}
if err := agent.InjectEndpoint("ACL", &m); err != nil {
t.Fatalf("err: %v", err)
}
checks := make(map[types.CheckID]*structs.HealthCheck)
if err := agent.filterChecks("node-ro", &checks); err != nil {
t.Fatalf("err: %v", err)
}
checks["my-node"] = &structs.HealthCheck{}
checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
if err := agent.filterChecks("service-ro", &checks); err != nil {
t.Fatalf("err: %v", err)
}
if _, ok := checks["my-node"]; ok {
t.Fatalf("bad: %#v", checks)
}
if _, ok := checks["my-service"]; !ok {
t.Fatalf("bad: %#v", checks)
}
if _, ok := checks["my-other"]; ok {
t.Fatalf("bad: %#v", checks)
}
checks["my-node"] = &structs.HealthCheck{}
checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
if err := agent.filterChecks("node-ro", &checks); err != nil {
t.Fatalf("err: %v", err)
}
if _, ok := checks["my-node"]; !ok {
t.Fatalf("bad: %#v", checks)
}
if _, ok := checks["my-service"]; ok {
t.Fatalf("bad: %#v", checks)
}
if _, ok := checks["my-other"]; ok {
t.Fatalf("bad: %#v", checks)
}
}

View File

@ -2,6 +2,7 @@ package agent
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@ -12,6 +13,7 @@ import (
"reflect"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -19,9 +21,13 @@ import (
"github.com/hashicorp/consul/consul/state"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-sockaddr/template"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/serf/coordinate"
"github.com/hashicorp/serf/serf"
"github.com/shirou/gopsutil/host"
)
const (
@ -32,10 +38,6 @@ const (
checksDir = "checks"
checkStateDir = "checks/state"
// The ID of the faux health checks for maintenance mode
serviceMaintCheckPrefix = "_service_maintenance"
nodeMaintCheckID = "_node_maintenance"
// Default reasons for node/service maintenance mode
defaultNodeMaintReason = "Maintenance mode is enabled for this node, " +
"but no reason was provided. This is a default message."
@ -65,11 +67,17 @@ type Agent struct {
// Output sink for logs
logOutput io.Writer
// Used for streaming logs to
logWriter *logger.LogWriter
// We have one of a client or a server, depending
// on our configuration
server *consul.Server
client *consul.Client
// acls is an object that helps manage local ACL enforcement.
acls *aclManager
// state stores a local representation of the node,
// services and checks. Used for anti-entropy.
state localState
@ -108,6 +116,8 @@ type Agent struct {
eventLock sync.RWMutex
eventNotify state.NotifyGroup
reloadCh chan chan error
shutdown bool
shutdownCh chan struct{}
shutdownLock sync.Mutex
@ -120,7 +130,8 @@ type Agent struct {
// Create is used to create a new Agent. Returns
// the agent or potentially an error.
func Create(config *Config, logOutput io.Writer) (*Agent, error) {
func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter,
reloadCh chan chan error) (*Agent, error) {
// Ensure we have a log sink
if logOutput == nil {
logOutput = os.Stderr
@ -136,6 +147,12 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
// Try to get an advertise address
if config.AdvertiseAddr != "" {
ipStr, err := parseSingleIPTemplate(config.AdvertiseAddr)
if err != nil {
return nil, fmt.Errorf("Advertise address resolution failed: %v", err)
}
config.AdvertiseAddr = ipStr
if ip := net.ParseIP(config.AdvertiseAddr); ip == nil {
return nil, fmt.Errorf("Failed to parse advertise address: %v", config.AdvertiseAddr)
}
@ -157,6 +174,12 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
// Try to get an advertise address for the wan
if config.AdvertiseAddrWan != "" {
ipStr, err := parseSingleIPTemplate(config.AdvertiseAddrWan)
if err != nil {
return nil, fmt.Errorf("Advertise WAN address resolution failed: %v", err)
}
config.AdvertiseAddrWan = ipStr
if ip := net.ParseIP(config.AdvertiseAddrWan); ip == nil {
return nil, fmt.Errorf("Failed to parse advertise address for wan: %v", config.AdvertiseAddrWan)
}
@ -174,6 +197,7 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
config: config,
logger: log.New(logOutput, "", log.LstdFlags),
logOutput: logOutput,
logWriter: logWriter,
checkReapAfter: make(map[types.CheckID]time.Duration),
checkMonitors: make(map[types.CheckID]*CheckMonitor),
checkTTLs: make(map[types.CheckID]*CheckTTL),
@ -182,15 +206,31 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
checkDockers: make(map[types.CheckID]*CheckDocker),
eventCh: make(chan serf.UserEvent, 1024),
eventBuf: make([]*UserEvent, 256),
reloadCh: reloadCh,
shutdownCh: make(chan struct{}),
endpoints: make(map[string]string),
}
if err := agent.resolveTmplAddrs(); err != nil {
return nil, err
}
// Initialize the ACL manager.
acls, err := newACLManager(config)
if err != nil {
return nil, err
}
agent.acls = acls
// Retrieve or generate the node ID before setting up the rest of the
// agent, which depends on it.
if err := agent.setupNodeID(config); err != nil {
return nil, fmt.Errorf("Failed to setup node ID: %v", err)
}
// Initialize the local state.
agent.state.Init(config, agent.logger)
// Setup either the client or the server.
var err error
if config.Server {
err = agent.setupServer()
agent.state.SetIface(agent.server)
@ -202,7 +242,8 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
Port: agent.config.Ports.Server,
Tags: []string{},
}
agent.state.AddService(&consulService, "")
agent.state.AddService(&consulService, agent.config.GetTokenForAgent())
} else {
err = agent.setupClient()
agent.state.SetIface(agent.client)
@ -211,13 +252,16 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) {
return nil, err
}
// Load checks/services.
// Load checks/services/metadata.
if err := agent.loadServices(config); err != nil {
return nil, err
}
if err := agent.loadChecks(config); err != nil {
return nil, err
}
if err := agent.loadMetadata(config); err != nil {
return nil, err
}
// Start watching for critical services to deregister, based on their
// checks.
@ -250,6 +294,9 @@ func (a *Agent) consulConfig() *consul.Config {
base = consul.DefaultConfig()
}
// This is set when the agent starts up
base.NodeID = a.config.NodeID
// Apply dev mode
base.DevMode = a.config.DevMode
@ -268,10 +315,6 @@ func (a *Agent) consulConfig() *consul.Config {
if a.config.NodeName != "" {
base.NodeName = a.config.NodeName
}
if a.config.BindAddr != "" {
base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.BindAddr
base.SerfWANConfig.MemberlistConfig.BindAddr = a.config.BindAddr
}
if a.config.Ports.SerfLan != 0 {
base.SerfLANConfig.MemberlistConfig.BindPort = a.config.Ports.SerfLan
base.SerfLANConfig.MemberlistConfig.AdvertisePort = a.config.Ports.SerfLan
@ -286,6 +329,17 @@ func (a *Agent) consulConfig() *consul.Config {
Port: a.config.Ports.Server,
}
base.RPCAddr = bindAddr
// Set the Serf configs using the old default behavior, we may
// override these in the code right below.
base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.BindAddr
base.SerfWANConfig.MemberlistConfig.BindAddr = a.config.BindAddr
}
if a.config.SerfLanBindAddr != "" {
base.SerfLANConfig.MemberlistConfig.BindAddr = a.config.SerfLanBindAddr
}
if a.config.SerfWanBindAddr != "" {
base.SerfWANConfig.MemberlistConfig.BindAddr = a.config.SerfWanBindAddr
}
if a.config.AdvertiseAddr != "" {
base.SerfLANConfig.MemberlistConfig.AdvertiseAddr = a.config.AdvertiseAddr
@ -331,6 +385,9 @@ func (a *Agent) consulConfig() *consul.Config {
if a.config.ACLToken != "" {
base.ACLToken = a.config.ACLToken
}
if a.config.ACLAgentToken != "" {
base.ACLAgentToken = a.config.ACLAgentToken
}
if a.config.ACLMasterToken != "" {
base.ACLMasterToken = a.config.ACLMasterToken
}
@ -349,6 +406,9 @@ func (a *Agent) consulConfig() *consul.Config {
if a.config.ACLReplicationToken != "" {
base.ACLReplicationToken = a.config.ACLReplicationToken
}
if a.config.ACLEnforceVersion8 != nil {
base.ACLEnforceVersion8 = *a.config.ACLEnforceVersion8
}
if a.config.SessionTTLMinRaw != "" {
base.SessionTTLMin = a.config.SessionTTLMin
}
@ -370,6 +430,7 @@ func (a *Agent) consulConfig() *consul.Config {
base.KeyFile = a.config.KeyFile
base.ServerName = a.config.ServerName
base.Domain = a.config.Domain
base.TLSMinVersion = a.config.TLSMinVersion
// Setup the ServerUp callback
base.ServerUp = a.state.ConsulServerUp
@ -387,6 +448,121 @@ func (a *Agent) consulConfig() *consul.Config {
return base
}
// parseSingleIPTemplate is used as a helper function to parse out a single IP
// address from a config parameter.
func parseSingleIPTemplate(ipTmpl string) (string, error) {
out, err := template.Parse(ipTmpl)
if err != nil {
return "", fmt.Errorf("Unable to parse address template %q: %v", ipTmpl, err)
}
ips := strings.Split(out, " ")
switch len(ips) {
case 0:
return "", errors.New("No addresses found, please configure one.")
case 1:
return ips[0], nil
default:
return "", fmt.Errorf("Multiple addresses found (%q), please configure one.", out)
}
}
// resolveTmplAddrs iterates over the myriad of addresses in the agent's config
// and performs go-sockaddr/template Parse on each known address in case the
// user specified a template config for any of their values.
func (a *Agent) resolveTmplAddrs() error {
if a.config.AdvertiseAddr != "" {
ipStr, err := parseSingleIPTemplate(a.config.AdvertiseAddr)
if err != nil {
return fmt.Errorf("Advertise address resolution failed: %v", err)
}
a.config.AdvertiseAddr = ipStr
}
if a.config.Addresses.DNS != "" {
ipStr, err := parseSingleIPTemplate(a.config.Addresses.DNS)
if err != nil {
return fmt.Errorf("DNS address resolution failed: %v", err)
}
a.config.Addresses.DNS = ipStr
}
if a.config.Addresses.HTTP != "" {
ipStr, err := parseSingleIPTemplate(a.config.Addresses.HTTP)
if err != nil {
return fmt.Errorf("HTTP address resolution failed: %v", err)
}
a.config.Addresses.HTTP = ipStr
}
if a.config.Addresses.HTTPS != "" {
ipStr, err := parseSingleIPTemplate(a.config.Addresses.HTTPS)
if err != nil {
return fmt.Errorf("HTTPS address resolution failed: %v", err)
}
a.config.Addresses.HTTPS = ipStr
}
if a.config.Addresses.RPC != "" {
ipStr, err := parseSingleIPTemplate(a.config.Addresses.RPC)
if err != nil {
return fmt.Errorf("RPC address resolution failed: %v", err)
}
a.config.Addresses.RPC = ipStr
}
if a.config.AdvertiseAddrWan != "" {
ipStr, err := parseSingleIPTemplate(a.config.AdvertiseAddrWan)
if err != nil {
return fmt.Errorf("Advertise WAN address resolution failed: %v", err)
}
a.config.AdvertiseAddrWan = ipStr
}
if a.config.BindAddr != "" {
ipStr, err := parseSingleIPTemplate(a.config.BindAddr)
if err != nil {
return fmt.Errorf("Bind address resolution failed: %v", err)
}
a.config.BindAddr = ipStr
}
if a.config.ClientAddr != "" {
ipStr, err := parseSingleIPTemplate(a.config.ClientAddr)
if err != nil {
return fmt.Errorf("Client address resolution failed: %v", err)
}
a.config.ClientAddr = ipStr
}
if a.config.SerfLanBindAddr != "" {
ipStr, err := parseSingleIPTemplate(a.config.SerfLanBindAddr)
if err != nil {
return fmt.Errorf("Serf LAN Address resolution failed: %v", err)
}
a.config.SerfLanBindAddr = ipStr
}
if a.config.SerfWanBindAddr != "" {
ipStr, err := parseSingleIPTemplate(a.config.SerfWanBindAddr)
if err != nil {
return fmt.Errorf("Serf WAN Address resolution failed: %v", err)
}
a.config.SerfWanBindAddr = ipStr
}
// Parse all tagged addresses
for k, v := range a.config.TaggedAddresses {
ipStr, err := parseSingleIPTemplate(v)
if err != nil {
return fmt.Errorf("%s address resolution failed: %v", k, err)
}
a.config.TaggedAddresses[k] = ipStr
}
return nil
}
// setupServer is used to initialize the Consul server
func (a *Agent) setupServer() error {
config := a.consulConfig()
@ -419,6 +595,102 @@ func (a *Agent) setupClient() error {
return nil
}
// makeRandomID will generate a random UUID for a node.
func (a *Agent) makeRandomID() (string, error) {
id, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
a.logger.Printf("[DEBUG] Using random ID %q as node ID", id)
return id, nil
}
// makeNodeID will try to find a host-specific ID, or else will generate a
// random ID. The returned ID will always be formatted as a GUID. We don't tell
// the caller whether this ID is random or stable since the consequences are
// high for us if this changes, so we will persist it either way. This will let
// gopsutil change implementations without affecting in-place upgrades of nodes.
func (a *Agent) makeNodeID() (string, error) {
// Try to get a stable ID associated with the host itself.
info, err := host.Info()
if err != nil {
a.logger.Printf("[DEBUG] Couldn't get a unique ID from the host: %v", err)
return a.makeRandomID()
}
// Make sure the host ID parses as a UUID, since we don't have complete
// control over this process.
id := strings.ToLower(info.HostID)
if _, err := uuid.ParseUUID(id); err != nil {
a.logger.Printf("[DEBUG] Unique ID %q from host isn't formatted as a UUID: %v",
id, err)
return a.makeRandomID()
}
a.logger.Printf("[DEBUG] Using unique ID %q from host as node ID", id)
return id, nil
}
// setupNodeID will pull the persisted node ID, if any, or create a random one
// and persist it.
func (a *Agent) setupNodeID(config *Config) error {
// If they've configured a node ID manually then just use that, as
// long as it's valid.
if config.NodeID != "" {
if _, err := uuid.ParseUUID(string(config.NodeID)); err != nil {
return err
}
return nil
}
// For dev mode we have no filesystem access so just make one.
if a.config.DevMode {
id, err := a.makeNodeID()
if err != nil {
return err
}
config.NodeID = types.NodeID(id)
return nil
}
// Load saved state, if any. Since a user could edit this, we also
// validate it.
fileID := filepath.Join(config.DataDir, "node-id")
if _, err := os.Stat(fileID); err == nil {
rawID, err := ioutil.ReadFile(fileID)
if err != nil {
return err
}
nodeID := strings.TrimSpace(string(rawID))
if _, err := uuid.ParseUUID(nodeID); err != nil {
return err
}
config.NodeID = types.NodeID(nodeID)
}
// If we still don't have a valid node ID, make one.
if config.NodeID == "" {
id, err := a.makeNodeID()
if err != nil {
return err
}
if err := lib.EnsurePath(fileID, false); err != nil {
return err
}
if err := ioutil.WriteFile(fileID, []byte(id), 0600); err != nil {
return err
}
config.NodeID = types.NodeID(id)
}
return nil
}
// setupKeyrings is used to initialize and load keyrings during agent startup
func (a *Agent) setupKeyrings(config *consul.Config) error {
fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring)
@ -469,6 +741,19 @@ func (a *Agent) RPC(method string, args interface{}, reply interface{}) error {
return a.client.RPC(method, args, reply)
}
// SnapshotRPC performs the requested snapshot RPC against the Consul server in
// a streaming manner. The contents of in will be read and passed along as the
// payload, and the response message will determine the error status, and any
// return payload will be written to out.
func (a *Agent) SnapshotRPC(args *structs.SnapshotRequest, in io.Reader, out io.Writer,
replyFn consul.SnapshotReplyFn) error {
if a.server != nil {
return a.server.SnapshotRPC(args, in, out, replyFn)
}
return a.client.SnapshotRPC(args, in, out, replyFn)
}
// Leave is used to prepare the agent for a graceful shutdown
func (a *Agent) Leave() error {
if a.server != nil {
@ -651,14 +936,11 @@ func (a *Agent) sendCoordinate() {
continue
}
// TODO - Consider adding a distance check so we don't send
// an update if the position hasn't changed by more than a
// threshold.
req := structs.CoordinateUpdateRequest{
Datacenter: a.config.Datacenter,
Node: a.config.NodeName,
Coord: c,
WriteRequest: structs.WriteRequest{Token: a.config.ACLToken},
WriteRequest: structs.WriteRequest{Token: a.config.GetTokenForAgent()},
}
var reply struct{}
if err := a.RPC("Coordinate.Update", &req, &reply); err != nil {
@ -721,6 +1003,7 @@ func (a *Agent) reapServices() {
// persistService saves a service definition to a JSON file in the data dir
func (a *Agent) persistService(service *structs.NodeService) error {
svcPath := filepath.Join(a.config.DataDir, servicesDir, stringHash(service.ID))
wrapped := persistedService{
Token: a.state.ServiceToken(service.ID),
Service: service,
@ -729,18 +1012,8 @@ func (a *Agent) persistService(service *structs.NodeService) error {
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(svcPath), 0700); err != nil {
return err
}
fh, err := os.OpenFile(svcPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer fh.Close()
if _, err := fh.Write(encoded); err != nil {
return err
}
return nil
return writeFileAtomic(svcPath, encoded)
}
// purgeService removes a persisted service definition file from the data dir
@ -767,18 +1040,8 @@ func (a *Agent) persistCheck(check *structs.HealthCheck, chkType *CheckType) err
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(checkPath), 0700); err != nil {
return err
}
fh, err := os.OpenFile(checkPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer fh.Close()
if _, err := fh.Write(encoded); err != nil {
return err
}
return nil
return writeFileAtomic(checkPath, encoded)
}
// purgeCheck removes a persisted check definition file from the data dir
@ -790,6 +1053,34 @@ func (a *Agent) purgeCheck(checkID types.CheckID) error {
return nil
}
// writeFileAtomic writes the given contents to a temporary file in the same
// directory, does an fsync and then renames the file to its real path
func writeFileAtomic(path string, contents []byte) error {
uuid, err := uuid.GenerateUUID()
if err != nil {
return err
}
tempPath := fmt.Sprintf("%s-%s.tmp", path, uuid)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
fh, err := os.OpenFile(tempPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
if _, err := fh.Write(contents); err != nil {
return err
}
if err := fh.Sync(); err != nil {
return err
}
if err := fh.Close(); err != nil {
return err
}
return os.Rename(tempPath, path)
}
// AddService is used to add a service entry.
// This entry is persistent and the agent will make a best effort to
// ensure it is registered
@ -816,7 +1107,7 @@ func (a *Agent) AddService(service *structs.NodeService, chkTypes CheckTypes, pe
// Warn if any tags are incompatible with DNS
for _, tag := range service.Tags {
if !dnsNameRe.MatchString(tag) {
a.logger.Printf("[WARN] Service tag %q will not be discoverable "+
a.logger.Printf("[DEBUG] Service tag %q will not be discoverable "+
"via DNS due to invalid characters. Valid characters include "+
"all alpha-numerics and dashes.", tag)
}
@ -882,7 +1173,14 @@ func (a *Agent) RemoveService(serviceID string, persist bool) error {
}
// Remove service immediately
a.state.RemoveService(serviceID)
err := a.state.RemoveService(serviceID)
// TODO: Return the error instead of just logging here in Consul 0.8
// For now, keep the current idempotent behavior on deleting a nonexistent service
if err != nil {
a.logger.Printf("[WARN] agent: Failed to deregister service %q: %s", serviceID, err)
return nil
}
// Remove the service from the data dir
if persist {
@ -962,12 +1260,13 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType, persist
}
http := &CheckHTTP{
Notify: &a.state,
CheckID: check.CheckID,
HTTP: chkType.HTTP,
Interval: chkType.Interval,
Timeout: chkType.Timeout,
Logger: a.logger,
Notify: &a.state,
CheckID: check.CheckID,
HTTP: chkType.HTTP,
Interval: chkType.Interval,
Timeout: chkType.Timeout,
Logger: a.logger,
TLSSkipVerify: chkType.TLSSkipVerify,
}
http.Start()
a.checkHTTPs[check.CheckID] = http
@ -1163,8 +1462,16 @@ func (a *Agent) persistCheckState(check *CheckTTL, status, output string) error
// Write the state to the file
file := filepath.Join(dir, checkIDHash(check.CheckID))
if err := ioutil.WriteFile(file, buf, 0600); err != nil {
return fmt.Errorf("failed writing file %q: %s", file, err)
// Create temp file in same dir, to make more likely atomic
tempFile := file + ".tmp"
// persistCheckState is called frequently, so don't use writeFileAtomic to avoid calling fsync here
if err := ioutil.WriteFile(tempFile, buf, 0600); err != nil {
return fmt.Errorf("failed writing temp file %q: %s", tempFile, err)
}
if err := os.Rename(tempFile, file); err != nil {
return fmt.Errorf("failed to rename temp file from %q to %q: %s", tempFile, file, err)
}
return nil
@ -1185,7 +1492,8 @@ func (a *Agent) loadCheckState(check *structs.HealthCheck) error {
// Decode the state data
var p persistedCheckState
if err := json.Unmarshal(buf, &p); err != nil {
return fmt.Errorf("failed decoding check state: %s", err)
a.logger.Printf("[ERROR] agent: failed decoding check state: %s", err)
return a.purgeCheckState(check.CheckID)
}
// Check if the state has expired
@ -1478,9 +1786,42 @@ func (a *Agent) restoreCheckState(snap map[types.CheckID]*structs.HealthCheck) {
}
}
// loadMetadata loads node metadata fields from the agent config and
// updates them on the local agent.
func (a *Agent) loadMetadata(conf *Config) error {
a.state.Lock()
defer a.state.Unlock()
for key, value := range conf.Meta {
a.state.metadata[key] = value
}
a.state.changeMade()
return nil
}
// parseMetaPair parses a key/value pair of the form key:value
func parseMetaPair(raw string) (string, string) {
pair := strings.SplitN(raw, ":", 2)
if len(pair) == 2 {
return pair[0], pair[1]
} else {
return pair[0], ""
}
}
// unloadMetadata resets the local metadata state
func (a *Agent) unloadMetadata() {
a.state.Lock()
defer a.state.Unlock()
a.state.metadata = make(map[string]string)
}
// serviceMaintCheckID returns the ID of a given service's maintenance check
func serviceMaintCheckID(serviceID string) types.CheckID {
return types.CheckID(fmt.Sprintf("%s:%s", serviceMaintCheckPrefix, serviceID))
return types.CheckID(structs.ServiceMaintPrefix + serviceID)
}
// EnableServiceMaintenance will register a false health check against the given
@ -1541,7 +1882,7 @@ func (a *Agent) DisableServiceMaintenance(serviceID string) error {
// EnableNodeMaintenance places a node into maintenance mode.
func (a *Agent) EnableNodeMaintenance(reason, token string) {
// Ensure node maintenance is not already enabled
if _, ok := a.state.Checks()[nodeMaintCheckID]; ok {
if _, ok := a.state.Checks()[structs.NodeMaint]; ok {
return
}
@ -1553,7 +1894,7 @@ func (a *Agent) EnableNodeMaintenance(reason, token string) {
// Create and register the node maintenance check
check := &structs.HealthCheck{
Node: a.config.NodeName,
CheckID: nodeMaintCheckID,
CheckID: structs.NodeMaint,
Name: "Node Maintenance Mode",
Notes: reason,
Status: structs.HealthCritical,
@ -1564,10 +1905,10 @@ func (a *Agent) EnableNodeMaintenance(reason, token string) {
// DisableNodeMaintenance removes a node from maintenance mode
func (a *Agent) DisableNodeMaintenance() {
if _, ok := a.state.Checks()[nodeMaintCheckID]; !ok {
if _, ok := a.state.Checks()[structs.NodeMaint]; !ok {
return
}
a.RemoveCheck(nodeMaintCheckID, true)
a.RemoveCheck(structs.NodeMaint, true)
a.logger.Printf("[INFO] agent: Node left maintenance mode")
}

View File

@ -2,12 +2,15 @@ package agent
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/logutils"
"github.com/hashicorp/serf/coordinate"
"github.com/hashicorp/serf/serf"
)
@ -17,6 +20,7 @@ type AgentSelf struct {
Coord *coordinate.Coordinate
Member serf.Member
Stats map[string]map[string]string
Meta map[string]string
}
func (s *HTTPServer) AgentSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
@ -28,38 +32,119 @@ func (s *HTTPServer) AgentSelf(resp http.ResponseWriter, req *http.Request) (int
}
}
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentRead(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
return AgentSelf{
Config: s.agent.config,
Coord: c,
Member: s.agent.LocalMember(),
Stats: s.agent.Stats(),
Meta: s.agent.state.Metadata(),
}, nil
}
func (s *HTTPServer) AgentReload(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "PUT" {
resp.WriteHeader(http.StatusMethodNotAllowed)
return nil, nil
}
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentWrite(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
// Trigger the reload
errCh := make(chan error, 0)
select {
case <-s.agent.ShutdownCh():
return nil, fmt.Errorf("Agent was shutdown before reload could be completed")
case s.agent.reloadCh <- errCh:
}
// Wait for the result of the reload, or for the agent to shutdown
select {
case <-s.agent.ShutdownCh():
return nil, fmt.Errorf("Agent was shutdown before reload could be completed")
case err := <-errCh:
return nil, err
}
}
func (s *HTTPServer) AgentServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Fetch the ACL token, if any.
var token string
s.parseToken(req, &token)
services := s.agent.state.Services()
if err := s.agent.filterServices(token, &services); err != nil {
return nil, err
}
return services, nil
}
func (s *HTTPServer) AgentChecks(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Fetch the ACL token, if any.
var token string
s.parseToken(req, &token)
checks := s.agent.state.Checks()
if err := s.agent.filterChecks(token, &checks); err != nil {
return nil, err
}
return checks, nil
}
func (s *HTTPServer) AgentMembers(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Fetch the ACL token, if any.
var token string
s.parseToken(req, &token)
// Check if the WAN is being queried
wan := false
if other := req.URL.Query().Get("wan"); other != "" {
wan = true
}
var members []serf.Member
if wan {
return s.agent.WANMembers(), nil
members = s.agent.WANMembers()
} else {
return s.agent.LANMembers(), nil
members = s.agent.LANMembers()
}
if err := s.agent.filterMembers(token, &members); err != nil {
return nil, err
}
return members, nil
}
func (s *HTTPServer) AgentJoin(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentWrite(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
// Check if the WAN is being queried
wan := false
if other := req.URL.Query().Get("wan"); other != "" {
@ -77,16 +162,59 @@ func (s *HTTPServer) AgentJoin(resp http.ResponseWriter, req *http.Request) (int
}
}
func (s *HTTPServer) AgentLeave(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "PUT" {
resp.WriteHeader(http.StatusMethodNotAllowed)
return nil, nil
}
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentWrite(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
if err := s.agent.Leave(); err != nil {
return nil, err
}
return nil, s.agent.Shutdown()
}
func (s *HTTPServer) AgentForceLeave(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentWrite(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
addr := strings.TrimPrefix(req.URL.Path, "/v1/agent/force-leave/")
return nil, s.agent.ForceLeave(addr)
}
// syncChanges is a helper function which wraps a blocking call to sync
// services and checks to the server. If the operation fails, we only
// only warn because the write did succeed and anti-entropy will sync later.
func (s *HTTPServer) syncChanges() {
if err := s.agent.state.syncChanges(); err != nil {
s.logger.Printf("[ERR] agent: failed to sync changes: %v", err)
}
}
const invalidCheckMessage = "Must provide TTL or Script/DockerContainerID/HTTP/TCP and Interval"
func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var args CheckDefinition
// Fixup the type decode of TTL or Interval
// Fixup the type decode of TTL or Interval.
decodeCB := func(raw interface{}) error {
return FixupCheckType(raw)
}
@ -96,7 +224,7 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ
return nil, nil
}
// Verify the check has a name
// Verify the check has a name.
if args.Name == "" {
resp.WriteHeader(400)
resp.Write([]byte("Missing check name"))
@ -109,10 +237,10 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ
return nil, nil
}
// Construct the health check
// Construct the health check.
health := args.HealthCheck(s.agent.config.NodeName)
// Verify the check type
// Verify the check type.
chkType := &args.CheckType
if !chkType.Valid() {
resp.WriteHeader(400)
@ -120,11 +248,14 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ
return nil, nil
}
// Get the provided token, if any
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckRegister(token, health); err != nil {
return nil, err
}
// Add the check
// Add the check.
if err := s.agent.AddCheck(health, chkType, true, token); err != nil {
return nil, err
}
@ -134,6 +265,14 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ
func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/deregister/"))
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckUpdate(token, checkID); err != nil {
return nil, err
}
if err := s.agent.RemoveCheck(checkID, true); err != nil {
return nil, err
}
@ -144,6 +283,14 @@ func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Re
func (s *HTTPServer) AgentCheckPass(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/pass/"))
note := req.URL.Query().Get("note")
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckUpdate(token, checkID); err != nil {
return nil, err
}
if err := s.agent.updateTTLCheck(checkID, structs.HealthPassing, note); err != nil {
return nil, err
}
@ -154,6 +301,14 @@ func (s *HTTPServer) AgentCheckPass(resp http.ResponseWriter, req *http.Request)
func (s *HTTPServer) AgentCheckWarn(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/warn/"))
note := req.URL.Query().Get("note")
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckUpdate(token, checkID); err != nil {
return nil, err
}
if err := s.agent.updateTTLCheck(checkID, structs.HealthWarning, note); err != nil {
return nil, err
}
@ -164,6 +319,14 @@ func (s *HTTPServer) AgentCheckWarn(resp http.ResponseWriter, req *http.Request)
func (s *HTTPServer) AgentCheckFail(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/fail/"))
note := req.URL.Query().Get("note")
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckUpdate(token, checkID); err != nil {
return nil, err
}
if err := s.agent.updateTTLCheck(checkID, structs.HealthCritical, note); err != nil {
return nil, err
}
@ -216,6 +379,14 @@ func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Reques
}
checkID := types.CheckID(strings.TrimPrefix(req.URL.Path, "/v1/agent/check/update/"))
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetCheckUpdate(token, checkID); err != nil {
return nil, err
}
if err := s.agent.updateTTLCheck(checkID, update.Status, update.Output); err != nil {
return nil, err
}
@ -225,7 +396,7 @@ func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Reques
func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var args ServiceDefinition
// Fixup the type decode of TTL or Interval if a check if provided
// Fixup the type decode of TTL or Interval if a check if provided.
decodeCB := func(raw interface{}) error {
rawMap, ok := raw.(map[string]interface{})
if !ok {
@ -258,17 +429,17 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re
return nil, nil
}
// Verify the service has a name
// Verify the service has a name.
if args.Name == "" {
resp.WriteHeader(400)
resp.Write([]byte("Missing service name"))
return nil, nil
}
// Get the node service
// Get the node service.
ns := args.NodeService()
// Verify the check type
// Verify the check type.
chkTypes := args.CheckTypes()
for _, check := range chkTypes {
if check.Status != "" && !structs.ValidStatus(check.Status) {
@ -283,11 +454,14 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re
}
}
// Get the provided token, if any
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetServiceRegister(token, ns); err != nil {
return nil, err
}
// Add the check
// Add the service.
if err := s.agent.AddService(ns, chkTypes, true, token); err != nil {
return nil, err
}
@ -297,6 +471,14 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re
func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
serviceID := strings.TrimPrefix(req.URL.Path, "/v1/agent/service/deregister/")
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetServiceUpdate(token, serviceID); err != nil {
return nil, err
}
if err := s.agent.RemoveService(serviceID, true); err != nil {
return nil, err
}
@ -335,9 +517,12 @@ func (s *HTTPServer) AgentServiceMaintenance(resp http.ResponseWriter, req *http
return nil, nil
}
// Get the provided token, if any
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
if err := s.agent.vetServiceUpdate(token, serviceID); err != nil {
return nil, err
}
if enable {
reason := params.Get("reason")
@ -380,9 +565,16 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re
return nil, nil
}
// Get the provided token, if any
// Get the provided token, if any, and vet against any ACL policies.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.NodeWrite(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
if enable {
s.agent.EnableNodeMaintenance(params.Get("reason"), token)
@ -393,11 +585,93 @@ func (s *HTTPServer) AgentNodeMaintenance(resp http.ResponseWriter, req *http.Re
return nil, nil
}
// syncChanges is a helper function which wraps a blocking call to sync
// services and checks to the server. If the operation fails, we only
// only warn because the write did succeed and anti-entropy will sync later.
func (s *HTTPServer) syncChanges() {
if err := s.agent.state.syncChanges(); err != nil {
s.logger.Printf("[ERR] agent: failed to sync changes: %v", err)
func (s *HTTPServer) AgentMonitor(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Only GET supported.
if req.Method != "GET" {
resp.WriteHeader(405)
return nil, nil
}
// Fetch the ACL token, if any, and enforce agent policy.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
if acl != nil && !acl.AgentRead(s.agent.config.NodeName) {
return nil, permissionDeniedErr
}
// Get the provided loglevel.
logLevel := req.URL.Query().Get("loglevel")
if logLevel == "" {
logLevel = "INFO"
}
// Upper case the level since that's required by the filter.
logLevel = strings.ToUpper(logLevel)
// Create a level filter and flusher.
filter := logger.LevelFilter()
filter.MinLevel = logutils.LogLevel(logLevel)
if !logger.ValidateLevelFilter(filter.MinLevel, filter) {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("Unknown log level: %s", filter.MinLevel)))
return nil, nil
}
flusher, ok := resp.(http.Flusher)
if !ok {
return nil, fmt.Errorf("Streaming not supported")
}
// Set up a log handler.
handler := &httpLogHandler{
filter: filter,
logCh: make(chan string, 512),
logger: s.logger,
}
s.agent.logWriter.RegisterHandler(handler)
defer s.agent.logWriter.DeregisterHandler(handler)
notify := resp.(http.CloseNotifier).CloseNotify()
// Stream logs until the connection is closed.
for {
select {
case <-notify:
s.agent.logWriter.DeregisterHandler(handler)
if handler.droppedCount > 0 {
s.agent.logger.Printf("[WARN] agent: Dropped %d logs during monitor request", handler.droppedCount)
}
return nil, nil
case log := <-handler.logCh:
resp.Write([]byte(log + "\n"))
flusher.Flush()
}
}
return nil, nil
}
type httpLogHandler struct {
filter *logutils.LevelFilter
logCh chan string
logger *log.Logger
droppedCount int
}
func (h *httpLogHandler) HandleLog(log string) {
// Check the log level
if !h.filter.Check([]byte(log)) {
return
}
// Do a non-blocking send
select {
case h.logCh <- log:
default:
// Just increment a counter for dropped logs to this handler; we can't log now
// because the lock is already held by the LogWriter invoking this
h.droppedCount += 1
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,13 +10,17 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/hashicorp/consul/consul"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/raft"
)
@ -54,6 +58,7 @@ func nextConfig() *Config {
conf.Ports.SerfWan = basePortNumber + idx + portOffsetSerfWan
conf.Ports.Server = basePortNumber + idx + portOffsetServer
conf.Server = true
conf.ACLEnforceVersion8 = Bool(false)
conf.ACLDatacenter = "dc1"
conf.ACLMasterToken = "root"
@ -79,14 +84,14 @@ func nextConfig() *Config {
return conf
}
func makeAgentLog(t *testing.T, conf *Config, l io.Writer) (string, *Agent) {
func makeAgentLog(t *testing.T, conf *Config, l io.Writer, writer *logger.LogWriter) (string, *Agent) {
dir, err := ioutil.TempDir("", "agent")
if err != nil {
t.Fatalf(fmt.Sprintf("err: %v", err))
}
conf.DataDir = dir
agent, err := Create(conf, l)
agent, err := Create(conf, l, writer, nil)
if err != nil {
os.RemoveAll(dir)
t.Fatalf(fmt.Sprintf("err: %v", err))
@ -112,7 +117,7 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) {
t.Fatalf("err: %s", err)
}
agent, err := Create(conf, nil)
agent, err := Create(conf, nil, nil, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -121,7 +126,22 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) {
}
func makeAgent(t *testing.T, conf *Config) (string, *Agent) {
return makeAgentLog(t, conf, nil)
return makeAgentLog(t, conf, nil, nil)
}
func externalIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", fmt.Errorf("Unable to lookup network interfaces: %v", err)
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
return "", fmt.Errorf("Unable to find a non-loopback interface")
}
func TestAgentStartStop(t *testing.T) {
@ -154,6 +174,28 @@ func TestAgent_RPCPing(t *testing.T) {
}
}
func TestAgent_CheckSerfBindAddrsSettings(t *testing.T) {
c := nextConfig()
ip, err := externalIP()
if err != nil {
t.Fatalf("Unable to get a non-loopback IP: %v", err)
}
c.SerfLanBindAddr = ip
c.SerfWanBindAddr = ip
dir, agent := makeAgent(t, c)
defer os.RemoveAll(dir)
defer agent.Shutdown()
serfWanBind := agent.consulConfig().SerfWANConfig.MemberlistConfig.BindAddr
if serfWanBind != ip {
t.Fatalf("SerfWanBindAddr is should be a non-loopback IP not %s", serfWanBind)
}
serfLanBind := agent.consulConfig().SerfLANConfig.MemberlistConfig.BindAddr
if serfLanBind != ip {
t.Fatalf("SerfLanBindAddr is should be a non-loopback IP not %s", serfWanBind)
}
}
func TestAgent_CheckAdvertiseAddrsSettings(t *testing.T) {
c := nextConfig()
c.AdvertiseAddrs.SerfLan, _ = net.ResolveTCPAddr("tcp", "127.0.0.42:1233")
@ -248,6 +290,7 @@ func TestAgent_ReconnectConfigSettings(t *testing.T) {
}
}()
c = nextConfig()
c.ReconnectTimeoutLan = 24 * time.Hour
c.ReconnectTimeoutWan = 36 * time.Hour
func() {
@ -267,6 +310,71 @@ func TestAgent_ReconnectConfigSettings(t *testing.T) {
}()
}
func TestAgent_NodeID(t *testing.T) {
c := nextConfig()
dir, agent := makeAgent(t, c)
defer os.RemoveAll(dir)
defer agent.Shutdown()
// The auto-assigned ID should be valid.
id := agent.consulConfig().NodeID
if _, err := uuid.ParseUUID(string(id)); err != nil {
t.Fatalf("err: %v", err)
}
// Running again should get the same ID (persisted in the file).
c.NodeID = ""
if err := agent.setupNodeID(c); err != nil {
t.Fatalf("err: %v", err)
}
if newID := agent.consulConfig().NodeID; id != newID {
t.Fatalf("bad: %q vs %q", id, newID)
}
// Set an invalid ID via config.
c.NodeID = types.NodeID("nope")
err := agent.setupNodeID(c)
if err == nil || !strings.Contains(err.Error(), "uuid string is wrong length") {
t.Fatalf("err: %v", err)
}
// Set a valid ID via config.
newID, err := uuid.GenerateUUID()
if err != nil {
t.Fatalf("err: %v", err)
}
c.NodeID = types.NodeID(newID)
if err := agent.setupNodeID(c); err != nil {
t.Fatalf("err: %v", err)
}
if id := agent.consulConfig().NodeID; string(id) != newID {
t.Fatalf("bad: %q vs. %q", id, newID)
}
// Set an invalid ID via the file.
fileID := filepath.Join(c.DataDir, "node-id")
if err := ioutil.WriteFile(fileID, []byte("adf4238a!882b!9ddc!4a9d!5b6758e4159e"), 0600); err != nil {
t.Fatalf("err: %v", err)
}
c.NodeID = ""
err = agent.setupNodeID(c)
if err == nil || !strings.Contains(err.Error(), "uuid is improperly formatted") {
t.Fatalf("err: %v", err)
}
// Set a valid ID via the file.
if err := ioutil.WriteFile(fileID, []byte("adf4238a-882b-9ddc-4a9d-5b6758e4159e"), 0600); err != nil {
t.Fatalf("err: %v", err)
}
c.NodeID = ""
if err := agent.setupNodeID(c); err != nil {
t.Fatalf("err: %v", err)
}
if id := agent.consulConfig().NodeID; string(id) != "adf4238a-882b-9ddc-4a9d-5b6758e4159e" {
t.Fatalf("bad: %q vs. %q", id, newID)
}
}
func TestAgent_AddService(t *testing.T) {
dir, agent := makeAgent(t, nextConfig())
defer os.RemoveAll(dir)
@ -807,7 +915,7 @@ func TestAgent_PersistService(t *testing.T) {
agent.Shutdown()
// Should load it back during later start
agent2, err := Create(config, nil)
agent2, err := Create(config, nil, nil, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -897,6 +1005,11 @@ func TestAgent_PurgeService(t *testing.T) {
t.Fatalf("err: %s", err)
}
// Re-add the service
if err := agent.AddService(svc, nil, true, ""); err != nil {
t.Fatalf("err: %v", err)
}
// Removed
if err := agent.RemoveService(svc.ID, true); err != nil {
t.Fatalf("err: %s", err)
@ -936,7 +1049,7 @@ func TestAgent_PurgeServiceOnDuplicate(t *testing.T) {
}
config.Services = []*ServiceDefinition{svc2}
agent2, err := Create(config, nil)
agent2, err := Create(config, nil, nil, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -1029,7 +1142,7 @@ func TestAgent_PersistCheck(t *testing.T) {
agent.Shutdown()
// Should load it back during later start
agent2, err := Create(config, nil)
agent2, err := Create(config, nil, nil, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -1122,7 +1235,7 @@ func TestAgent_PurgeCheckOnDuplicate(t *testing.T) {
}
config.Checks = []*CheckDefinition{check2}
agent2, err := Create(config, nil)
agent2, err := Create(config, nil, nil, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -1533,13 +1646,13 @@ func TestAgent_NodeMaintenanceMode(t *testing.T) {
agent.EnableNodeMaintenance("broken", "mytoken")
// Make sure the critical health check was added
check, ok := agent.state.Checks()[nodeMaintCheckID]
check, ok := agent.state.Checks()[structs.NodeMaint]
if !ok {
t.Fatalf("should have registered critical node check")
}
// Check that the token was used to register the check
if token := agent.state.CheckToken(nodeMaintCheckID); token != "mytoken" {
if token := agent.state.CheckToken(structs.NodeMaint); token != "mytoken" {
t.Fatalf("expected 'mytoken', got: '%s'", token)
}
@ -1552,7 +1665,7 @@ func TestAgent_NodeMaintenanceMode(t *testing.T) {
agent.DisableNodeMaintenance()
// Ensure the check was deregistered
if _, ok := agent.state.Checks()[nodeMaintCheckID]; ok {
if _, ok := agent.state.Checks()[structs.NodeMaint]; ok {
t.Fatalf("should have deregistered critical node check")
}
@ -1560,7 +1673,7 @@ func TestAgent_NodeMaintenanceMode(t *testing.T) {
agent.EnableNodeMaintenance("", "")
// Make sure the check was registered with the default note
check, ok = agent.state.Checks()[nodeMaintCheckID]
check, ok = agent.state.Checks()[structs.NodeMaint]
if !ok {
t.Fatalf("should have registered critical node check")
}

File diff suppressed because one or more lines are too long

View File

@ -64,6 +64,7 @@ func (s *HTTPServer) CatalogNodes(resp http.ResponseWriter, req *http.Request) (
// Setup the request
args := structs.DCSpecificRequest{}
s.parseSource(req, &args.Source)
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
@ -85,6 +86,7 @@ func (s *HTTPServer) CatalogNodes(resp http.ResponseWriter, req *http.Request) (
func (s *HTTPServer) CatalogServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Set default DC
args := structs.DCSpecificRequest{}
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
@ -101,6 +103,7 @@ func (s *HTTPServer) CatalogServiceNodes(resp http.ResponseWriter, req *http.Req
// Set default DC
args := structs.ServiceSpecificRequest{}
s.parseSource(req, &args.Source)
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}

View File

@ -145,6 +145,53 @@ func TestCatalogNodes(t *testing.T) {
}
}
func TestCatalogNodes_MetaFilter(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
// Register a node with a meta field
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.1",
NodeMeta: map[string]string{
"somekey": "somevalue",
},
}
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/catalog/nodes?node-meta=somekey:somevalue", nil)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
obj, err := srv.CatalogNodes(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
// Verify an index is set
assertIndex(t, resp)
// Verify we only get the node with the correct meta field back
nodes := obj.(structs.Nodes)
if len(nodes) != 1 {
t.Fatalf("bad: %v", obj)
}
if v, ok := nodes[0].Meta["somekey"]; !ok || v != "somevalue" {
t.Fatalf("bad: %v", nodes[0].Meta)
}
}
func TestCatalogNodes_WanTranslation(t *testing.T) {
dir1, srv1 := makeHTTPServerWithConfig(t,
func(c *Config) {
@ -449,6 +496,54 @@ func TestCatalogServices(t *testing.T) {
}
}
func TestCatalogServices_NodeMetaFilter(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
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",
NodeMeta: map[string]string{
"somekey": "somevalue",
},
Service: &structs.NodeService{
Service: "api",
},
}
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/catalog/services?node-meta=somekey:somevalue", nil)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
obj, err := srv.CatalogServices(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
assertIndex(t, resp)
services := obj.(structs.Services)
if len(services) != 1 {
t.Fatalf("bad: %v", obj)
}
if _, ok := services[args.Service.Service]; !ok {
t.Fatalf("bad: %v", services)
}
}
func TestCatalogServiceNodes(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
@ -513,6 +608,72 @@ func TestCatalogServiceNodes(t *testing.T) {
}
}
func TestCatalogServiceNodes_NodeMetaFilter(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
defer srv.agent.Shutdown()
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?node-meta=somekey:somevalue", 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",
Node: "foo",
Address: "127.0.0.1",
NodeMeta: map[string]string{
"somekey": "somevalue",
},
Service: &structs.NodeService{
Service: "api",
},
}
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/catalog/service/api?node-meta=somekey:somevalue", 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 len(nodes) != 1 {
t.Fatalf("bad: %v", obj)
}
}
func TestCatalogServiceNodes_WanTranslation(t *testing.T) {
dir1, srv1 := makeHTTPServerWithConfig(t,
func(c *Config) {

View File

@ -1,6 +1,7 @@
package agent
import (
"crypto/tls"
"fmt"
"io"
"log"
@ -47,6 +48,7 @@ type CheckType struct {
Interval time.Duration
DockerContainerID string
Shell string
TLSSkipVerify bool
Timeout time.Duration
TTL time.Duration
@ -108,7 +110,6 @@ type CheckMonitor struct {
Interval time.Duration
Timeout time.Duration
Logger *log.Logger
ReapLock *sync.RWMutex
stop bool
stopCh chan struct{}
@ -154,12 +155,6 @@ func (c *CheckMonitor) run() {
// check is invoked periodically to perform the script check
func (c *CheckMonitor) check() {
// Disable child process reaping so that we can get this command's
// return value. Note that we take the read lock here since we are
// waiting on a specific PID and don't need to serialize all waits.
c.ReapLock.RLock()
defer c.ReapLock.RUnlock()
// Create the command
cmd, err := ExecScript(c.Script)
if err != nil {
@ -202,7 +197,7 @@ func (c *CheckMonitor) check() {
output.Size(), output.TotalWritten(), outputStr)
}
c.Logger.Printf("[DEBUG] agent: check '%s' script '%s' output: %s",
c.Logger.Printf("[DEBUG] agent: Check '%s' script '%s' output: %s",
c.CheckID, c.Script, outputStr)
// Check if the check passed
@ -340,12 +335,13 @@ type persistedCheckState struct {
// The check is critical if the response code is anything else
// or if the request returns an error
type CheckHTTP struct {
Notify CheckNotifier
CheckID types.CheckID
HTTP string
Interval time.Duration
Timeout time.Duration
Logger *log.Logger
Notify CheckNotifier
CheckID types.CheckID
HTTP string
Interval time.Duration
Timeout time.Duration
Logger *log.Logger
TLSSkipVerify bool
httpClient *http.Client
stop bool
@ -365,6 +361,15 @@ func (c *CheckHTTP) Start() {
trans := cleanhttp.DefaultTransport()
trans.DisableKeepAlives = true
// Skip SSL certificate verification if TLSSkipVerify is true
if trans.TLSClientConfig == nil {
trans.TLSClientConfig = &tls.Config{
InsecureSkipVerify: c.TLSSkipVerify,
}
} else {
trans.TLSClientConfig.InsecureSkipVerify = c.TLSSkipVerify
}
// Create the HTTP client.
c.httpClient = &http.Client{
Timeout: 10 * time.Second,
@ -436,7 +441,7 @@ func (c *CheckHTTP) check() {
// Read the response into a circular buffer to limit the size
output, _ := circbuf.NewBuffer(CheckBufSize)
if _, err := io.Copy(output, resp.Body); err != nil {
c.Logger.Printf("[WARN] agent: check '%v': Get error while reading body: %s", c.CheckID, err)
c.Logger.Printf("[WARN] agent: Check '%v': Get error while reading body: %s", c.CheckID, err)
}
// Format the response body
@ -444,19 +449,19 @@ func (c *CheckHTTP) check() {
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
// PASSING (2xx)
c.Logger.Printf("[DEBUG] agent: check '%v' is passing", c.CheckID)
c.Logger.Printf("[DEBUG] agent: Check '%v' is passing", c.CheckID)
c.Notify.UpdateCheck(c.CheckID, structs.HealthPassing, result)
} else if resp.StatusCode == 429 {
// WARNING
// 429 Too Many Requests (RFC 6585)
// The user has sent too many requests in a given amount of time.
c.Logger.Printf("[WARN] agent: check '%v' is now warning", c.CheckID)
c.Logger.Printf("[WARN] agent: Check '%v' is now warning", c.CheckID)
c.Notify.UpdateCheck(c.CheckID, structs.HealthWarning, result)
} else {
// CRITICAL
c.Logger.Printf("[WARN] agent: check '%v' is now critical", c.CheckID)
c.Logger.Printf("[WARN] agent: Check '%v' is now critical", c.CheckID)
c.Notify.UpdateCheck(c.CheckID, structs.HealthCritical, result)
}
}
@ -540,7 +545,7 @@ func (c *CheckTCP) check() {
return
}
conn.Close()
c.Logger.Printf("[DEBUG] agent: check '%v' is passing", c.CheckID)
c.Logger.Printf("[DEBUG] agent: Check '%v' is passing", c.CheckID)
c.Notify.UpdateCheck(c.CheckID, structs.HealthPassing, fmt.Sprintf("TCP connect %s: Success", c.TCP))
}
@ -666,7 +671,7 @@ func (c *CheckDocker) check() {
output.Size(), output.TotalWritten(), outputStr)
}
c.Logger.Printf("[DEBUG] agent: check '%s' script '%s' output: %s",
c.Logger.Printf("[DEBUG] agent: Check '%s' script '%s' output: %s",
c.CheckID, c.Script, outputStr)
execInfo, err := c.dockerClient.InspectExec(exec.ID)

View File

@ -25,15 +25,44 @@ type MockNotify struct {
state map[types.CheckID]string
updates map[types.CheckID]int
output map[types.CheckID]string
// A guard to protect an access to the internal attributes
// of the notification mock in order to prevent panics
// raised by the race conditions detector.
mu sync.RWMutex
}
func (m *MockNotify) UpdateCheck(id types.CheckID, status, output string) {
m.mu.Lock()
defer m.mu.Unlock()
m.state[id] = status
old := m.updates[id]
m.updates[id] = old + 1
m.output[id] = output
}
// State returns the state of the specified health-check.
func (m *MockNotify) State(id types.CheckID) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.state[id]
}
// Updates returns the count of updates of the specified health-check.
func (m *MockNotify) Updates(id types.CheckID) int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.updates[id]
}
// Output returns an output string of the specified health-check.
func (m *MockNotify) Output(id types.CheckID) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.output[id]
}
func expectStatus(t *testing.T, script, status string) {
mock := &MockNotify{
state: make(map[types.CheckID]string),
@ -46,18 +75,17 @@ func expectStatus(t *testing.T, script, status string) {
Script: script,
Interval: 10 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
ReapLock: &sync.RWMutex{},
}
check.Start()
defer check.Stop()
testutil.WaitForResult(func() (bool, error) {
// Should have at least 2 updates
if mock.updates["foo"] < 2 {
if mock.Updates("foo") < 2 {
return false, fmt.Errorf("should have 2 updates %v", mock.updates)
}
if mock.state["foo"] != status {
if mock.State("foo") != status {
return false, fmt.Errorf("should be %v %v", status, mock.state)
}
@ -96,7 +124,6 @@ func TestCheckMonitor_Timeout(t *testing.T) {
Interval: 10 * time.Millisecond,
Timeout: 5 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
ReapLock: &sync.RWMutex{},
}
check.Start()
defer check.Stop()
@ -104,11 +131,11 @@ func TestCheckMonitor_Timeout(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// Should have at least 2 updates
if mock.updates["foo"] < 2 {
if mock.Updates("foo") < 2 {
t.Fatalf("should have at least 2 updates %v", mock.updates)
}
if mock.state["foo"] != "critical" {
if mock.State("foo") != "critical" {
t.Fatalf("should be critical %v", mock.state)
}
}
@ -125,7 +152,6 @@ func TestCheckMonitor_RandomStagger(t *testing.T) {
Script: "exit 0",
Interval: 25 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
ReapLock: &sync.RWMutex{},
}
check.Start()
defer check.Stop()
@ -133,11 +159,11 @@ func TestCheckMonitor_RandomStagger(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// Should have at least 1 update
if mock.updates["foo"] < 1 {
if mock.Updates("foo") < 1 {
t.Fatalf("should have 1 or more updates %v", mock.updates)
}
if mock.state["foo"] != structs.HealthPassing {
if mock.State("foo") != structs.HealthPassing {
t.Fatalf("should be %v %v", structs.HealthPassing, mock.state)
}
}
@ -154,7 +180,6 @@ func TestCheckMonitor_LimitOutput(t *testing.T) {
Script: "od -N 81920 /dev/urandom",
Interval: 25 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
ReapLock: &sync.RWMutex{},
}
check.Start()
defer check.Stop()
@ -162,7 +187,7 @@ func TestCheckMonitor_LimitOutput(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// Allow for extra bytes for the truncation message
if len(mock.output["foo"]) > CheckBufSize+100 {
if len(mock.Output("foo")) > CheckBufSize+100 {
t.Fatalf("output size is too long")
}
}
@ -185,32 +210,32 @@ func TestCheckTTL(t *testing.T) {
time.Sleep(50 * time.Millisecond)
check.SetStatus(structs.HealthPassing, "test-output")
if mock.updates["foo"] != 1 {
if mock.Updates("foo") != 1 {
t.Fatalf("should have 1 updates %v", mock.updates)
}
if mock.state["foo"] != structs.HealthPassing {
if mock.State("foo") != structs.HealthPassing {
t.Fatalf("should be passing %v", mock.state)
}
// Ensure we don't fail early
time.Sleep(75 * time.Millisecond)
if mock.updates["foo"] != 1 {
if mock.Updates("foo") != 1 {
t.Fatalf("should have 1 updates %v", mock.updates)
}
// Wait for the TTL to expire
time.Sleep(75 * time.Millisecond)
if mock.updates["foo"] != 2 {
if mock.Updates("foo") != 2 {
t.Fatalf("should have 2 updates %v", mock.updates)
}
if mock.state["foo"] != structs.HealthCritical {
if mock.State("foo") != structs.HealthCritical {
t.Fatalf("should be critical %v", mock.state)
}
if !strings.Contains(mock.output["foo"], "test-output") {
if !strings.Contains(mock.Output("foo"), "test-output") {
t.Fatalf("should have retained output %v", mock.output)
}
}
@ -228,6 +253,19 @@ func mockHTTPServer(responseCode int) *httptest.Server {
return httptest.NewServer(mux)
}
func mockTLSHTTPServer(responseCode int) *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Body larger than 4k limit
body := bytes.Repeat([]byte{'a'}, 2*CheckBufSize)
w.WriteHeader(responseCode)
w.Write(body)
return
})
return httptest.NewTLSServer(mux)
}
func expectHTTPStatus(t *testing.T, url string, status string) {
mock := &MockNotify{
state: make(map[types.CheckID]string),
@ -244,21 +282,24 @@ func expectHTTPStatus(t *testing.T, url string, status string) {
check.Start()
defer check.Stop()
time.Sleep(50 * time.Millisecond)
testutil.WaitForResult(func() (bool, error) {
// Should have at least 2 updates
if mock.Updates("foo") < 2 {
return false, fmt.Errorf("should have 2 updates %v", mock.updates)
}
// Should have at least 2 updates
if mock.updates["foo"] < 2 {
t.Fatalf("should have 2 updates %v", mock.updates)
}
if mock.State("foo") != status {
return false, fmt.Errorf("should be %v %v", status, mock.state)
}
if mock.state["foo"] != status {
t.Fatalf("should be %v %v", status, mock.state)
}
// Allow slightly more data than CheckBufSize, for the header
if n := len(mock.output["foo"]); n > (CheckBufSize + 256) {
t.Fatalf("output too long: %d (%d-byte limit)", n, CheckBufSize)
}
// Allow slightly more data than CheckBufSize, for the header
if n := len(mock.Output("foo")); n > (CheckBufSize + 256) {
return false, fmt.Errorf("output too long: %d (%d-byte limit)", n, CheckBufSize)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCheckHTTPCritical(t *testing.T) {
@ -347,16 +388,19 @@ func TestCheckHTTPTimeout(t *testing.T) {
check.Start()
defer check.Stop()
time.Sleep(50 * time.Millisecond)
testutil.WaitForResult(func() (bool, error) {
// Should have at least 2 updates
if mock.updates["bar"] < 2 {
return false, fmt.Errorf("should have at least 2 updates %v", mock.updates)
}
// Should have at least 2 updates
if mock.updates["bar"] < 2 {
t.Fatalf("should have at least 2 updates %v", mock.updates)
}
if mock.state["bar"] != structs.HealthCritical {
t.Fatalf("should be critical %v", mock.state)
}
if mock.state["bar"] != structs.HealthCritical {
return false, fmt.Errorf("should be critical %v", mock.state)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCheckHTTP_disablesKeepAlives(t *testing.T) {
@ -375,6 +419,134 @@ func TestCheckHTTP_disablesKeepAlives(t *testing.T) {
}
}
func TestCheckHTTP_TLSSkipVerify_defaultFalse(t *testing.T) {
check := &CheckHTTP{
CheckID: "foo",
HTTP: "https://foo.bar/baz",
Interval: 10 * time.Second,
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
check.Start()
defer check.Stop()
if check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Fatalf("should default to false")
}
}
func TestCheckHTTP_TLSSkipVerify_true_pass(t *testing.T) {
server := mockTLSHTTPServer(200)
defer server.Close()
mock := &MockNotify{
state: make(map[types.CheckID]string),
updates: make(map[types.CheckID]int),
output: make(map[types.CheckID]string),
}
check := &CheckHTTP{
Notify: mock,
CheckID: types.CheckID("skipverify_true"),
HTTP: server.URL,
Interval: 5 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
TLSSkipVerify: true,
}
check.Start()
defer check.Stop()
if !check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Fatalf("should be true")
}
testutil.WaitForResult(func() (bool, error) {
if mock.state["skipverify_true"] != structs.HealthPassing {
return false, fmt.Errorf("should be passing %v", mock.state)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCheckHTTP_TLSSkipVerify_true_fail(t *testing.T) {
server := mockTLSHTTPServer(500)
defer server.Close()
mock := &MockNotify{
state: make(map[types.CheckID]string),
updates: make(map[types.CheckID]int),
output: make(map[types.CheckID]string),
}
check := &CheckHTTP{
Notify: mock,
CheckID: types.CheckID("skipverify_true"),
HTTP: server.URL,
Interval: 5 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
TLSSkipVerify: true,
}
check.Start()
defer check.Stop()
if !check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Fatalf("should be true")
}
testutil.WaitForResult(func() (bool, error) {
if mock.state["skipverify_true"] != structs.HealthCritical {
return false, fmt.Errorf("should be critical %v", mock.state)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCheckHTTP_TLSSkipVerify_false(t *testing.T) {
server := mockTLSHTTPServer(200)
defer server.Close()
mock := &MockNotify{
state: make(map[types.CheckID]string),
updates: make(map[types.CheckID]int),
output: make(map[types.CheckID]string),
}
check := &CheckHTTP{
Notify: mock,
CheckID: types.CheckID("skipverify_false"),
HTTP: server.URL,
Interval: 100 * time.Millisecond,
Logger: log.New(os.Stderr, "", log.LstdFlags),
TLSSkipVerify: false,
}
check.Start()
defer check.Stop()
if check.httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify {
t.Fatalf("should be false")
}
testutil.WaitForResult(func() (bool, error) {
// This should fail due to an invalid SSL cert
if mock.state["skipverify_false"] != structs.HealthCritical {
return false, fmt.Errorf("should be critical %v", mock.state)
}
if !strings.Contains(mock.output["skipverify_false"], "certificate signed by unknown authority") {
return false, fmt.Errorf("should fail with certificate error %v", mock.output)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func mockTCPServer(network string) net.Listener {
var (
addr string
@ -410,16 +582,19 @@ func expectTCPStatus(t *testing.T, tcp string, status string) {
check.Start()
defer check.Stop()
time.Sleep(50 * time.Millisecond)
testutil.WaitForResult(func() (bool, error) {
// Should have at least 2 updates
if mock.Updates("foo") < 2 {
return false, fmt.Errorf("should have 2 updates %v", mock.updates)
}
// Should have at least 2 updates
if mock.updates["foo"] < 2 {
t.Fatalf("should have 2 updates %v", mock.updates)
}
if mock.state["foo"] != status {
t.Fatalf("should be %v %v", status, mock.state)
}
if mock.State("foo") != status {
return false, fmt.Errorf("should be %v %v", status, mock.state)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestCheckTCPCritical(t *testing.T) {
@ -596,15 +771,15 @@ func expectDockerCheckStatus(t *testing.T, dockerClient DockerClient, status str
time.Sleep(50 * time.Millisecond)
// Should have at least 2 updates
if mock.updates["foo"] < 2 {
if mock.Updates("foo") < 2 {
t.Fatalf("should have 2 updates %v", mock.updates)
}
if mock.state["foo"] != status {
if mock.State("foo") != status {
t.Fatalf("should be %v %v", status, mock.state)
}
if mock.output["foo"] != output {
if mock.Output("foo") != output {
t.Fatalf("should be %v %v", output, mock.output)
}
}
@ -706,7 +881,7 @@ func TestDockerCheckTruncateOutput(t *testing.T) {
time.Sleep(50 * time.Millisecond)
// Allow for extra bytes for the truncation message
if len(mock.output["foo"]) > CheckBufSize+100 {
if len(mock.Output("foo")) > CheckBufSize+100 {
t.Fatalf("output size is too long")
}

View File

@ -1,10 +1,14 @@
package agent
import (
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
@ -14,13 +18,25 @@ import (
"syscall"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
"github.com/armon/go-metrics"
"github.com/armon/go-metrics/circonus"
"github.com/armon/go-metrics/datadog"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/watch"
"github.com/hashicorp/go-checkpoint"
"github.com/hashicorp/go-syslog"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/logutils"
scada "github.com/hashicorp/scada-client/scada"
"github.com/mitchellh/cli"
@ -43,6 +59,7 @@ type Command struct {
HumanVersion string
Ui cli.Ui
ShutdownCh <-chan struct{}
configReloadCh chan chan error
args []string
logFilter *logutils.LevelFilter
logOutput io.Writer
@ -64,16 +81,19 @@ func (c *Command) readConfig() *Config {
var dnsRecursors []string
var dev bool
var dcDeprecated string
var nodeMeta []string
cmdFlags := flag.NewFlagSet("agent", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.Var((*AppendSliceValue)(&configFiles), "config-file", "json file to read config from")
cmdFlags.Var((*AppendSliceValue)(&configFiles), "config-dir", "directory of json files to read")
cmdFlags.Var((*AppendSliceValue)(&dnsRecursors), "recursor", "address of an upstream DNS server")
cmdFlags.Var((*AppendSliceValue)(&nodeMeta), "node-meta", "arbitrary metadata key/value pair")
cmdFlags.BoolVar(&dev, "dev", false, "development server mode")
cmdFlags.StringVar(&cmdConfig.LogLevel, "log-level", "", "log level")
cmdFlags.StringVar(&cmdConfig.NodeName, "node", "", "node name")
cmdFlags.StringVar((*string)(&cmdConfig.NodeID), "node-id", "", "node ID")
cmdFlags.StringVar(&dcDeprecated, "dc", "", "node datacenter (deprecated: use 'datacenter' instead)")
cmdFlags.StringVar(&cmdConfig.Datacenter, "datacenter", "", "node datacenter")
cmdFlags.StringVar(&cmdConfig.DataDir, "data-dir", "", "path to the data directory")
@ -89,6 +109,8 @@ func (c *Command) readConfig() *Config {
cmdFlags.StringVar(&cmdConfig.ClientAddr, "client", "", "address to bind client listeners to (DNS, HTTP, HTTPS, RPC)")
cmdFlags.StringVar(&cmdConfig.BindAddr, "bind", "", "address to bind server listeners to")
cmdFlags.StringVar(&cmdConfig.SerfWanBindAddr, "serf-wan-bind", "", "address to bind Serf WAN listeners to")
cmdFlags.StringVar(&cmdConfig.SerfLanBindAddr, "serf-lan-bind", "", "address to bind Serf LAN listeners to")
cmdFlags.IntVar(&cmdConfig.Ports.HTTP, "http-port", 0, "http port to use")
cmdFlags.IntVar(&cmdConfig.Ports.DNS, "dns-port", 0, "DNS port to use")
cmdFlags.StringVar(&cmdConfig.AdvertiseAddr, "advertise", "", "address to advertise instead of bind addr")
@ -115,6 +137,20 @@ func (c *Command) readConfig() *Config {
"number of retries for joining")
cmdFlags.StringVar(&retryInterval, "retry-interval", "",
"interval between join attempts")
cmdFlags.StringVar(&cmdConfig.RetryJoinEC2.Region, "retry-join-ec2-region", "",
"EC2 Region to discover servers in")
cmdFlags.StringVar(&cmdConfig.RetryJoinEC2.TagKey, "retry-join-ec2-tag-key", "",
"EC2 tag key to filter on for server discovery")
cmdFlags.StringVar(&cmdConfig.RetryJoinEC2.TagValue, "retry-join-ec2-tag-value", "",
"EC2 tag value to filter on for server discovery")
cmdFlags.StringVar(&cmdConfig.RetryJoinGCE.ProjectName, "retry-join-gce-project-name", "",
"Google Compute Engine project to discover servers in")
cmdFlags.StringVar(&cmdConfig.RetryJoinGCE.ZonePattern, "retry-join-gce-zone-pattern", "",
"Google Compute Engine region or zone to discover servers in (regex pattern)")
cmdFlags.StringVar(&cmdConfig.RetryJoinGCE.TagValue, "retry-join-gce-tag-value", "",
"Google Compute Engine tag value to filter on for server discovery")
cmdFlags.StringVar(&cmdConfig.RetryJoinGCE.CredentialsFile, "retry-join-gce-credentials-file", "",
"Path to credentials JSON file to use with Google Compute Engine")
cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryJoinWan), "retry-join-wan",
"address of agent to join -wan on startup with retry")
cmdFlags.IntVar(&cmdConfig.RetryMaxAttemptsWan, "retry-max-wan", 0,
@ -144,6 +180,14 @@ func (c *Command) readConfig() *Config {
cmdConfig.RetryIntervalWan = dur
}
if len(nodeMeta) > 0 {
cmdConfig.Meta = make(map[string]string)
for _, entry := range nodeMeta {
key, value := parseMetaPair(entry)
cmdConfig.Meta[key] = value
}
}
var config *Config
if dev {
config = DevConfig()
@ -188,10 +232,22 @@ func (c *Command) readConfig() *Config {
config.SkipLeaveOnInt = Bool(config.Server)
}
// Ensure we have a data directory
if config.DataDir == "" && !dev {
c.Ui.Error("Must specify data directory using -data-dir")
return nil
// Ensure we have a data directory if we are not in dev mode.
if !dev {
if config.DataDir == "" {
c.Ui.Error("Must specify data directory using -data-dir")
return nil
}
if finfo, err := os.Stat(config.DataDir); err != nil {
if !os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf("Error getting data-dir: %s", err))
return nil
}
} else if !finfo.IsDir() {
c.Ui.Error(fmt.Sprintf("The data-dir specified at %q is not a directory", config.DataDir))
return nil
}
}
// Ensure all endpoints are unique
@ -259,6 +315,17 @@ func (c *Command) readConfig() *Config {
return nil
}
// If 'acl_datacenter' is set, ensure it is lowercased.
if config.ACLDatacenter != "" {
config.ACLDatacenter = strings.ToLower(config.ACLDatacenter)
// Verify 'acl_datacenter' is valid
if !validDatacenter.MatchString(config.ACLDatacenter) {
c.Ui.Error("ACL datacenter must be alpha-numeric with underscores and hypens only")
return nil
}
}
// Only allow bootstrap mode when acting as a server
if config.Bootstrap && !config.Server {
c.Ui.Error("Bootstrap mode cannot be enabled when server mode is not enabled")
@ -271,6 +338,12 @@ func (c *Command) readConfig() *Config {
return nil
}
// Expect can only work when dev mode is off
if config.BootstrapExpect > 0 && config.DevMode {
c.Ui.Error("Expect mode cannot be enabled when dev mode is enabled")
return nil
}
// Expect & Bootstrap are mutually exclusive
if config.BootstrapExpect != 0 && config.Bootstrap {
c.Ui.Error("Bootstrap cannot be provided with an expected server count")
@ -310,6 +383,26 @@ func (c *Command) readConfig() *Config {
c.Ui.Error("WARNING: Bootstrap mode enabled! Do not enable unless necessary")
}
// Need both tag key and value for EC2 discovery
if config.RetryJoinEC2.TagKey != "" || config.RetryJoinEC2.TagValue != "" {
if config.RetryJoinEC2.TagKey == "" || config.RetryJoinEC2.TagValue == "" {
c.Ui.Error("tag key and value are both required for EC2 retry-join")
return nil
}
}
// EC2 and GCE discovery are mutually exclusive
if config.RetryJoinEC2.TagKey != "" && config.RetryJoinEC2.TagValue != "" && config.RetryJoinGCE.TagValue != "" {
c.Ui.Error("EC2 and GCE discovery are mutually exclusive. Please provide one or the other.")
return nil
}
// Verify the node metadata entries are valid
if err := structs.ValidateMetadata(config.Meta); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse node metadata: %v", err))
return nil
}
// Set the version info
config.Revision = c.Revision
config.Version = c.Version
@ -362,52 +455,216 @@ func (config *Config) verifyUniqueListeners() error {
return nil
}
// setupLoggers is used to setup the logGate, logWriter, and our logOutput
func (c *Command) setupLoggers(config *Config) (*GatedWriter, *logWriter, io.Writer) {
// Setup logging. First create the gated log writer, which will
// store logs until we're ready to show them. Then create the level
// filter, filtering logs of the specified level.
logGate := &GatedWriter{
Writer: &cli.UiWriter{Ui: c.Ui},
}
// discoverEc2Hosts searches an AWS region, returning a list of instance ips
// where EC2TagKey = EC2TagValue
func (c *Config) discoverEc2Hosts(logger *log.Logger) ([]string, error) {
config := c.RetryJoinEC2
c.logFilter = LevelFilter()
c.logFilter.MinLevel = logutils.LogLevel(strings.ToUpper(config.LogLevel))
c.logFilter.Writer = logGate
if !ValidateLevelFilter(c.logFilter.MinLevel, c.logFilter) {
c.Ui.Error(fmt.Sprintf(
"Invalid log level: %s. Valid log levels are: %v",
c.logFilter.MinLevel, c.logFilter.Levels))
return nil, nil, nil
}
// Check if syslog is enabled
var syslog io.Writer
if config.EnableSyslog {
l, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, config.SyslogFacility, "consul")
ec2meta := ec2metadata.New(session.New())
if config.Region == "" {
logger.Printf("[INFO] agent: No EC2 region provided, querying instance metadata endpoint...")
identity, err := ec2meta.GetInstanceIdentityDocument()
if err != nil {
c.Ui.Error(fmt.Sprintf("Syslog setup failed: %v", err))
return nil, nil, nil
return nil, err
}
syslog = &SyslogWrapper{l, c.logFilter}
config.Region = identity.Region
}
// Create a log writer, and wrap a logOutput around it
logWriter := NewLogWriter(512)
var logOutput io.Writer
if syslog != nil {
logOutput = io.MultiWriter(c.logFilter, logWriter, syslog)
} else {
logOutput = io.MultiWriter(c.logFilter, logWriter)
awsConfig := &aws.Config{
Region: &config.Region,
Credentials: credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{
Value: credentials.Value{
AccessKeyID: config.AccessKeyID,
SecretAccessKey: config.SecretAccessKey,
},
},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{},
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
}),
}
c.logOutput = logOutput
return logGate, logWriter, logOutput
svc := ec2.New(session.New(), awsConfig)
resp, err := svc.DescribeInstances(&ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:" + config.TagKey),
Values: []*string{
aws.String(config.TagValue),
},
},
},
})
if err != nil {
return nil, err
}
var servers []string
for i := range resp.Reservations {
for _, instance := range resp.Reservations[i].Instances {
// Terminated instances don't have the PrivateIpAddress field
if instance.PrivateIpAddress != nil {
servers = append(servers, *instance.PrivateIpAddress)
}
}
}
return servers, nil
}
// discoverGCEHosts searches a Google Compute Engine region, returning a list
// of instance ips that match the tags given in GCETags.
func (c *Config) discoverGCEHosts(logger *log.Logger) ([]string, error) {
config := c.RetryJoinGCE
ctx := oauth2.NoContext
var client *http.Client
var err error
logger.Printf("[INFO] agent: Initializing GCE client")
if config.CredentialsFile != "" {
logger.Printf("[INFO] agent: Loading credentials from %s", config.CredentialsFile)
key, err := ioutil.ReadFile(config.CredentialsFile)
if err != nil {
return nil, err
}
jwtConfig, err := google.JWTConfigFromJSON(key, compute.ComputeScope)
if err != nil {
return nil, err
}
client = jwtConfig.Client(ctx)
} else {
logger.Printf("[INFO] agent: Using default credential chain")
client, err = google.DefaultClient(ctx, compute.ComputeScope)
if err != nil {
return nil, err
}
}
computeService, err := compute.New(client)
if err != nil {
return nil, err
}
if config.ProjectName == "" {
logger.Printf("[INFO] agent: No GCE project provided, will discover from metadata.")
config.ProjectName, err = gceProjectIDFromMetadata(logger)
if err != nil {
return nil, err
}
} else {
logger.Printf("[INFO] agent: Using pre-defined GCE project name: %s", config.ProjectName)
}
zones, err := gceDiscoverZones(logger, ctx, computeService, config.ProjectName, config.ZonePattern)
if err != nil {
return nil, err
}
logger.Printf("[INFO] agent: Discovering GCE hosts with tag %s in zones: %s", config.TagValue, strings.Join(zones, ", "))
var servers []string
for _, zone := range zones {
addresses, err := gceInstancesAddressesForZone(logger, ctx, computeService, config.ProjectName, zone, config.TagValue)
if err != nil {
return nil, err
}
if len(addresses) > 0 {
logger.Printf("[INFO] agent: Discovered %d instances in %s/%s: %v", len(addresses), config.ProjectName, zone, addresses)
}
servers = append(servers, addresses...)
}
return servers, nil
}
// gceProjectIDFromMetadata queries the metadata service on GCE to get the
// project ID (name) of an instance.
func gceProjectIDFromMetadata(logger *log.Logger) (string, error) {
logger.Printf("[INFO] agent: Attempting to discover GCE project from metadata.")
client := &http.Client{}
req, err := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/project/project-id", nil)
if err != nil {
return "", err
}
req.Header.Add("Metadata-Flavor", "Google")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
project, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
logger.Printf("[INFO] agent: GCE project discovered as: %s", project)
return string(project), nil
}
// gceDiscoverZones discovers a list of zones from a supplied zone pattern, or
// all of the zones available to a project.
func gceDiscoverZones(logger *log.Logger, ctx context.Context, computeService *compute.Service, project, pattern string) ([]string, error) {
var zones []string
if pattern != "" {
logger.Printf("[INFO] agent: Discovering zones for project %s matching pattern: %s", project, pattern)
} else {
logger.Printf("[INFO] agent: Discovering all zones available to project: %s", project)
}
call := computeService.Zones.List(project)
if pattern != "" {
call = call.Filter(fmt.Sprintf("name eq %s", pattern))
}
if err := call.Pages(ctx, func(page *compute.ZoneList) error {
for _, v := range page.Items {
zones = append(zones, v.Name)
}
return nil
}); err != nil {
return zones, err
}
logger.Printf("[INFO] agent: Discovered GCE zones: %s", strings.Join(zones, ", "))
return zones, nil
}
// gceInstancesAddressesForZone locates all instances within a specific project
// and zone, matching the supplied tag. Only the private IP addresses are
// returned, but ID is also logged.
func gceInstancesAddressesForZone(logger *log.Logger, ctx context.Context, computeService *compute.Service, project, zone, tag string) ([]string, error) {
var addresses []string
call := computeService.Instances.List(project, zone)
if err := call.Pages(ctx, func(page *compute.InstanceList) error {
for _, v := range page.Items {
for _, t := range v.Tags.Items {
if t == tag && len(v.NetworkInterfaces) > 0 && v.NetworkInterfaces[0].NetworkIP != "" {
addresses = append(addresses, v.NetworkInterfaces[0].NetworkIP)
}
}
}
return nil
}); err != nil {
return addresses, err
}
return addresses, nil
}
// setupAgent is used to start the agent and various interfaces
func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *logWriter) error {
func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *logger.LogWriter) error {
c.Ui.Output("Starting Consul agent...")
agent, err := Create(config, logOutput)
agent, err := Create(config, logOutput, logWriter, c.configReloadCh)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting agent: %s", err))
return err
@ -568,7 +825,9 @@ func (c *Command) startupJoinWan(config *Config) error {
// retryJoin is used to handle retrying a join until it succeeds or all
// retries are exhausted.
func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) {
if len(config.RetryJoin) == 0 {
ec2Enabled := config.RetryJoinEC2.TagKey != "" && config.RetryJoinEC2.TagValue != ""
if len(config.RetryJoin) == 0 && !ec2Enabled && config.RetryJoinGCE.TagValue == "" {
return
}
@ -577,10 +836,32 @@ func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) {
attempt := 0
for {
n, err := c.agent.JoinLAN(config.RetryJoin)
if err == nil {
logger.Printf("[INFO] agent: Join completed. Synced with %d initial agents", n)
return
var servers []string
var err error
switch {
case ec2Enabled:
servers, err = config.discoverEc2Hosts(logger)
if err != nil {
logger.Printf("[ERROR] agent: Unable to query EC2 instances: %s", err)
}
logger.Printf("[INFO] agent: Discovered %d servers from EC2", len(servers))
case config.RetryJoinGCE.TagValue != "":
servers, err = config.discoverGCEHosts(logger)
if err != nil {
logger.Printf("[ERROR] agent: Unable to query GCE insances: %s", err)
}
logger.Printf("[INFO] agent: Discovered %d servers from GCE", len(servers))
}
servers = append(servers, config.RetryJoin...)
if len(servers) == 0 {
err = fmt.Errorf("No servers to join")
} else {
n, err := c.agent.JoinLAN(servers)
if err == nil {
logger.Printf("[INFO] agent: Join completed. Synced with %d initial agents", n)
return
}
}
attempt++
@ -659,10 +940,20 @@ func (c *Command) Run(args []string) int {
}
// Setup the log outputs
logGate, logWriter, logOutput := c.setupLoggers(config)
if logWriter == nil {
logConfig := &logger.Config{
LogLevel: config.LogLevel,
EnableSyslog: config.EnableSyslog,
SyslogFacility: config.SyslogFacility,
}
logFilter, logGate, logWriter, logOutput, ok := logger.Setup(logConfig, c.Ui)
if !ok {
return 1
}
c.logFilter = logFilter
c.logOutput = logOutput
// Setup the channel for triggering config reloads
c.configReloadCh = make(chan chan error)
/* Setup telemetry
Aggregate on 10 second intervals for 1 minute. Expose the
@ -722,6 +1013,8 @@ func (c *Command) Run(args []string) int {
cfg.CheckManager.Check.ForceMetricActivation = config.Telemetry.CirconusCheckForceMetricActivation
cfg.CheckManager.Check.InstanceID = config.Telemetry.CirconusCheckInstanceID
cfg.CheckManager.Check.SearchTag = config.Telemetry.CirconusCheckSearchTag
cfg.CheckManager.Check.DisplayName = config.Telemetry.CirconusCheckDisplayName
cfg.CheckManager.Check.Tags = config.Telemetry.CirconusCheckTags
cfg.CheckManager.Broker.ID = config.Telemetry.CirconusBrokerID
cfg.CheckManager.Broker.SelectTag = config.Telemetry.CirconusBrokerSelectTag
@ -729,10 +1022,6 @@ func (c *Command) Run(args []string) int {
cfg.CheckManager.API.TokenApp = "consul"
}
if cfg.CheckManager.Check.InstanceID == "" {
cfg.CheckManager.Check.InstanceID = fmt.Sprintf("%s:%s", config.NodeName, config.Datacenter)
}
if cfg.CheckManager.Check.SearchTag == "" {
cfg.CheckManager.Check.SearchTag = "service:consul"
}
@ -828,6 +1117,7 @@ func (c *Command) Run(args []string) int {
c.Ui.Output("Consul agent running!")
c.Ui.Info(fmt.Sprintf(" Version: '%s'", c.HumanVersion))
c.Ui.Info(fmt.Sprintf(" Node ID: '%s'", config.NodeID))
c.Ui.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName))
c.Ui.Info(fmt.Sprintf(" Datacenter: '%s'", config.Datacenter))
c.Ui.Info(fmt.Sprintf(" Server: %v (bootstrap: %v)", config.Server, config.Bootstrap))
@ -860,15 +1150,20 @@ func (c *Command) Run(args []string) int {
func (c *Command) handleSignals(config *Config, retryJoin <-chan struct{}, retryJoinWan <-chan struct{}) int {
signalCh := make(chan os.Signal, 4)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE)
// Wait for a signal
WAIT:
var sig os.Signal
var reloadErrCh chan error
select {
case s := <-signalCh:
sig = s
case <-c.rpcServer.ReloadCh():
sig = syscall.SIGHUP
case ch := <-c.configReloadCh:
sig = syscall.SIGHUP
reloadErrCh = ch
case <-c.ShutdownCh:
sig = os.Interrupt
case <-retryJoin:
@ -881,11 +1176,24 @@ WAIT:
}
c.Ui.Output(fmt.Sprintf("Caught signal: %v", sig))
// Skip SIGPIPE signals
if sig == syscall.SIGPIPE {
goto WAIT
}
// Check if this is a SIGHUP
if sig == syscall.SIGHUP {
if conf := c.handleReload(config); conf != nil {
conf, err := c.handleReload(config)
if conf != nil {
config = conf
}
if err != nil {
c.Ui.Error(err.Error())
}
// Send result back if reload was called via HTTP
if reloadErrCh != nil {
reloadErrCh <- err
}
goto WAIT
}
@ -925,20 +1233,21 @@ WAIT:
}
// handleReload is invoked when we should reload our configs, e.g. SIGHUP
func (c *Command) handleReload(config *Config) *Config {
func (c *Command) handleReload(config *Config) (*Config, error) {
c.Ui.Output("Reloading configuration...")
var errs error
newConf := c.readConfig()
if newConf == nil {
c.Ui.Error(fmt.Sprintf("Failed to reload configs"))
return config
errs = multierror.Append(errs, fmt.Errorf("Failed to reload configs"))
return config, errs
}
// Change the log level
minLevel := logutils.LogLevel(strings.ToUpper(newConf.LogLevel))
if ValidateLevelFilter(minLevel, c.logFilter) {
if logger.ValidateLevelFilter(minLevel, c.logFilter) {
c.logFilter.SetMinLevel(minLevel)
} else {
c.Ui.Error(fmt.Sprintf(
errs = multierror.Append(fmt.Errorf(
"Invalid log level: %s. Valid log levels are: %v",
minLevel, c.logFilter.Levels))
@ -954,31 +1263,36 @@ func (c *Command) handleReload(config *Config) *Config {
snap := c.agent.snapshotCheckState()
defer c.agent.restoreCheckState(snap)
// First unload all checks and services. This lets us begin the reload
// First unload all checks, services, and metadata. This lets us begin the reload
// with a clean slate.
if err := c.agent.unloadServices(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed unloading services: %s", err))
return nil
errs = multierror.Append(errs, fmt.Errorf("Failed unloading services: %s", err))
return nil, errs
}
if err := c.agent.unloadChecks(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed unloading checks: %s", err))
return nil
errs = multierror.Append(errs, fmt.Errorf("Failed unloading checks: %s", err))
return nil, errs
}
c.agent.unloadMetadata()
// Reload services and check definitions.
// Reload service/check definitions and metadata.
if err := c.agent.loadServices(newConf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed reloading services: %s", err))
return nil
errs = multierror.Append(errs, fmt.Errorf("Failed reloading services: %s", err))
return nil, errs
}
if err := c.agent.loadChecks(newConf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed reloading checks: %s", err))
return nil
errs = multierror.Append(errs, fmt.Errorf("Failed reloading checks: %s", err))
return nil, errs
}
if err := c.agent.loadMetadata(newConf); err != nil {
errs = multierror.Append(errs, fmt.Errorf("Failed reloading metadata: %s", err))
return nil, errs
}
// Get the new client listener addr
httpAddr, err := newConf.ClientListener(config.Addresses.HTTP, config.Ports.HTTP)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to determine HTTP address: %v", err))
errs = multierror.Append(errs, fmt.Errorf("Failed to determine HTTP address: %v", err))
}
// Deregister the old watches
@ -992,7 +1306,7 @@ func (c *Command) handleReload(config *Config) *Config {
wp.Handler = makeWatchHandler(c.logOutput, wp.Exempt["handler"])
wp.LogOutput = c.logOutput
if err := wp.Run(httpAddr.String()); err != nil {
c.Ui.Error(fmt.Sprintf("Error running watch: %v", err))
errs = multierror.Append(errs, fmt.Errorf("Error running watch: %v", err))
}
}(wp)
}
@ -1002,12 +1316,12 @@ func (c *Command) handleReload(config *Config) *Config {
newConf.AtlasToken != config.AtlasToken ||
newConf.AtlasEndpoint != config.AtlasEndpoint {
if err := c.setupScadaConn(newConf); err != nil {
c.Ui.Error(fmt.Sprintf("Failed reloading SCADA client: %s", err))
return nil
errs = multierror.Append(errs, fmt.Errorf("Failed reloading SCADA client: %s", err))
return nil, errs
}
}
return newConf
return newConf, errs
}
// startScadaClient is used to start a new SCADA provider and listener,
@ -1026,6 +1340,10 @@ func (c *Command) setupScadaConn(config *Config) error {
return nil
}
c.Ui.Error("WARNING: The hosted version of Consul Enterprise will be deprecated " +
"on March 7th, 2017. For details, see " +
"https://atlas.hashicorp.com/help/consul/alternatives")
scadaConfig := &scada.Config{
Service: "consul",
Version: fmt.Sprintf("%s%s", config.Version, config.VersionPrerelease),
@ -1066,54 +1384,86 @@ Usage: consul agent [options]
Options:
-advertise=addr Sets the advertise address to use
-advertise-wan=addr Sets address to advertise on wan instead of advertise addr
-atlas=org/name Sets the Atlas infrastructure name, enables SCADA.
-atlas-join Enables auto-joining the Atlas cluster
-atlas-token=token Provides the Atlas API token
-atlas-endpoint=1.2.3.4 The address of the endpoint for Atlas integration.
-bootstrap Sets server to bootstrap mode
-bind=0.0.0.0 Sets the bind address for cluster communication
-http-port=8500 Sets the HTTP API port to listen on
-bootstrap-expect=0 Sets server to expect bootstrap mode.
-client=127.0.0.1 Sets the address to bind for client access.
This includes RPC, DNS, HTTP and HTTPS (if configured)
-config-file=foo Path to a JSON file to read configuration from.
This can be specified multiple times.
-config-dir=foo Path to a directory to read configuration files
from. This will read every file ending in ".json"
as configuration in this directory in alphabetical
order. This can be specified multiple times.
-data-dir=path Path to a data directory to store agent state
-dev Starts the agent in development mode.
-recursor=1.2.3.4 Address of an upstream DNS server.
Can be specified multiple times.
-dc=east-aws Datacenter of the agent (deprecated: use 'datacenter' instead).
-datacenter=east-aws Datacenter of the agent.
-encrypt=key Provides the gossip encryption key
-join=1.2.3.4 Address of an agent to join at start time.
Can be specified multiple times.
-join-wan=1.2.3.4 Address of an agent to join -wan at start time.
Can be specified multiple times.
-retry-join=1.2.3.4 Address of an agent to join at start time with
retries enabled. Can be specified multiple times.
-retry-interval=30s Time to wait between join attempts.
-retry-max=0 Maximum number of join attempts. Defaults to 0, which
will retry indefinitely.
-retry-join-wan=1.2.3.4 Address of an agent to join -wan at start time with
retries enabled. Can be specified multiple times.
-retry-interval-wan=30s Time to wait between join -wan attempts.
-retry-max-wan=0 Maximum number of join -wan attempts. Defaults to 0, which
will retry indefinitely.
-log-level=info Log level of the agent.
-node=hostname Name of this node. Must be unique in the cluster
-protocol=N Sets the protocol version. Defaults to latest.
-rejoin Ignores a previous leave and attempts to rejoin the cluster.
-server Switches agent to server mode.
-syslog Enables logging to syslog
-ui Enables the built-in static web UI server
-ui-dir=path Path to directory containing the Web UI resources
-pid-file=path Path to file to store agent PID
-advertise=addr Sets the advertise address to use
-advertise-wan=addr Sets address to advertise on wan instead of
advertise addr
-atlas=org/name Sets the Atlas infrastructure name, enables
SCADA.
-atlas-join Enables auto-joining the Atlas cluster
-atlas-token=token Provides the Atlas API token
-atlas-endpoint=1.2.3.4 The address of the endpoint for Atlas
integration.
-bootstrap Sets server to bootstrap mode
-bind=0.0.0.0 Sets the bind address for cluster
communication
-http-port=8500 Sets the HTTP API port to listen on
-bootstrap-expect=0 Sets server to expect bootstrap mode.
-client=127.0.0.1 Sets the address to bind for client access.
This includes RPC, DNS, HTTP and HTTPS (if
configured)
-config-file=foo Path to a JSON file to read configuration
from. This can be specified multiple times.
-config-dir=foo Path to a directory to read configuration
files from. This will read every file ending
in ".json" as configuration in this
directory in alphabetical order. This can be
specified multiple times.
-data-dir=path Path to a data directory to store agent
state
-dev Starts the agent in development mode.
-recursor=1.2.3.4 Address of an upstream DNS server.
Can be specified multiple times.
-dc=east-aws Datacenter of the agent (deprecated: use
'datacenter' instead).
-datacenter=east-aws Datacenter of the agent.
-encrypt=key Provides the gossip encryption key
-join=1.2.3.4 Address of an agent to join at start time.
Can be specified multiple times.
-join-wan=1.2.3.4 Address of an agent to join -wan at start
time. Can be specified multiple times.
-retry-join=1.2.3.4 Address of an agent to join at start time
with retries enabled. Can be specified
multiple times.
-retry-interval=30s Time to wait between join attempts.
-retry-max=0 Maximum number of join attempts. Defaults to
0, which will retry indefinitely.
-retry-join-ec2-region EC2 Region to use for discovering servers to
join.
-retry-join-ec2-tag-key EC2 tag key to filter on for server
discovery
-retry-join-ec2-tag-value EC2 tag value to filter on for server
discovery
-retry-join-gce-project-name Google Compute Engine project to discover
servers in
-retry-join-gce-zone-pattern Google Compute Engine region or zone to
discover servers in (regex pattern)
-retry-join-gce-tag-value Google Compute Engine tag value to filter
for server discovery
-retry-join-gce-credentials-file Path to credentials JSON file to use with
Google Compute Engine
-retry-join-wan=1.2.3.4 Address of an agent to join -wan at start
time with retries enabled. Can be specified
multiple times.
-retry-interval-wan=30s Time to wait between join -wan attempts.
-retry-max-wan=0 Maximum number of join -wan attempts.
Defaults to 0, which will retry
indefinitely.
-log-level=info Log level of the agent.
-node=hostname Name of this node. Must be unique in the
cluster
-node-meta=key:value An arbitrary metadata key/value pair for
this node.
This can be specified multiple times.
-protocol=N Sets the protocol version. Defaults to
latest.
-rejoin Ignores a previous leave and attempts to
rejoin the cluster.
-server Switches agent to server mode.
-syslog Enables logging to syslog
-ui Enables the built-in static web UI server
-ui-dir=path Path to directory containing the Web UI
resources
-pid-file=path Path to file to store agent PID
`
return strings.TrimSpace(helpText)

View File

@ -10,8 +10,10 @@ import (
"strings"
"testing"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil"
"github.com/mitchellh/cli"
"reflect"
)
func TestCommand_implements(t *testing.T) {
@ -126,6 +128,9 @@ func TestReadCliConfig(t *testing.T) {
"-data-dir", tmpDir,
"-node", `"a"`,
"-advertise-wan", "1.2.3.4",
"-serf-wan-bind", "4.3.2.1",
"-serf-lan-bind", "4.3.2.2",
"-node-meta", "somekey:somevalue",
},
ShutdownCh: shutdownCh,
Ui: new(cli.MockUi),
@ -135,6 +140,36 @@ func TestReadCliConfig(t *testing.T) {
if config.AdvertiseAddrWan != "1.2.3.4" {
t.Fatalf("expected -advertise-addr-wan 1.2.3.4 got %s", config.AdvertiseAddrWan)
}
if config.SerfWanBindAddr != "4.3.2.1" {
t.Fatalf("expected -serf-wan-bind 4.3.2.1 got %s", config.SerfWanBindAddr)
}
if config.SerfLanBindAddr != "4.3.2.2" {
t.Fatalf("expected -serf-lan-bind 4.3.2.2 got %s", config.SerfLanBindAddr)
}
if len(config.Meta) != 1 || config.Meta["somekey"] != "somevalue" {
t.Fatalf("expected somekey=somevalue, got %v", config.Meta)
}
}
// Test multiple node meta flags
{
cmd := &Command{
args: []string{
"-data-dir", tmpDir,
"-node-meta", "somekey:somevalue",
"-node-meta", "otherkey:othervalue",
},
ShutdownCh: shutdownCh,
Ui: new(cli.MockUi),
}
expected := map[string]string{
"somekey": "somevalue",
"otherkey": "othervalue",
}
config := cmd.readConfig()
if !reflect.DeepEqual(config.Meta, expected) {
t.Fatalf("bad: %v %v", config.Meta, expected)
}
}
// Test LeaveOnTerm and SkipLeaveOnInt defaults for server mode
@ -266,6 +301,63 @@ func TestRetryJoinWanFail(t *testing.T) {
}
}
func TestDiscoverEC2Hosts(t *testing.T) {
if os.Getenv("AWS_REGION") == "" {
t.Skip("AWS_REGION not set, skipping")
}
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
t.Skip("AWS_ACCESS_KEY_ID not set, skipping")
}
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
t.Skip("AWS_SECRET_ACCESS_KEY not set, skipping")
}
c := &Config{
RetryJoinEC2: RetryJoinEC2{
Region: os.Getenv("AWS_REGION"),
TagKey: "ConsulRole",
TagValue: "Server",
},
}
servers, err := c.discoverEc2Hosts(&log.Logger{})
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}
func TestDiscoverGCEHosts(t *testing.T) {
if os.Getenv("GCE_PROJECT") == "" {
t.Skip("GCE_PROJECT not set, skipping")
}
if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" && os.Getenv("GCE_CONFIG_CREDENTIALS") == "" {
t.Skip("GOOGLE_APPLICATION_CREDENTIALS or GCE_CONFIG_CREDENTIALS not set, skipping")
}
c := &Config{
RetryJoinGCE: RetryJoinGCE{
ProjectName: os.Getenv("GCE_PROJECT"),
ZonePattern: os.Getenv("GCE_ZONE"),
TagValue: "consulrole-server",
CredentialsFile: os.Getenv("GCE_CONFIG_CREDENTIALS"),
},
}
servers, err := c.discoverGCEHosts(log.New(os.Stderr, "", log.LstdFlags))
if err != nil {
t.Fatal(err)
}
if len(servers) != 3 {
t.Fatalf("bad: %v", servers)
}
}
func TestSetupAgent_RPCUnixSocket_FileExists(t *testing.T) {
conf := nextConfig()
tmpDir, err := ioutil.TempDir("", "consul")
@ -299,7 +391,7 @@ func TestSetupAgent_RPCUnixSocket_FileExists(t *testing.T) {
Ui: new(cli.MockUi),
}
logWriter := NewLogWriter(512)
logWriter := logger.NewLogWriter(512)
logOutput := new(bytes.Buffer)
// Ensure the server is created

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/consul/consul"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/watch"
"github.com/mitchellh/mapstructure"
)
@ -118,6 +119,45 @@ type DNSConfig struct {
RecursorTimeoutRaw string `mapstructure:"recursor_timeout" json:"-"`
}
// RetryJoinEC2 is used to configure discovery of instances via Amazon's EC2 api
type RetryJoinEC2 struct {
// The AWS region to look for instances in
Region string `mapstructure:"region"`
// The tag key and value to use when filtering instances
TagKey string `mapstructure:"tag_key"`
TagValue string `mapstructure:"tag_value"`
// The AWS credentials to use for making requests to EC2
AccessKeyID string `mapstructure:"access_key_id" json:"-"`
SecretAccessKey string `mapstructure:"secret_access_key" json:"-"`
}
// RetryJoinGCE is used to configure discovery of instances via Google Compute
// Engine's API.
type RetryJoinGCE struct {
// The name of the project the instances reside in.
ProjectName string `mapstructure:"project_name"`
// A regular expression (RE2) pattern for the zones you want to discover the instances in.
// Example: us-west1-.*, or us-(?west|east).*.
ZonePattern string `mapstructure:"zone_pattern"`
// The tag value to search for when filtering instances.
TagValue string `mapstructure:"tag_value"`
// A path to a JSON file with the service account credentials necessary to
// connect to GCE. If this is not defined, the following chain is respected:
// 1. A JSON file whose path is specified by the
// GOOGLE_APPLICATION_CREDENTIALS environment variable.
// 2. A JSON file in a location known to the gcloud command-line tool.
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
// 3. On Google Compute Engine, it fetches credentials from the metadata
// server. (In this final case any provided scopes are ignored.)
CredentialsFile string `mapstructure:"credentials_file"`
}
// Performance is used to tune the performance of Consul's subsystems.
type Performance struct {
// RaftMultiplier is an integer multiplier used to scale Raft timing
@ -199,6 +239,13 @@ type Telemetry struct {
// narrow down the search results when neither a Submission URL or Check ID is provided.
// Default: service:app (e.g. service:consul)
CirconusCheckSearchTag string `mapstructure:"circonus_check_search_tag"`
// CirconusCheckTags is a comma separated list of tags to apply to the check. Note that
// the value of CirconusCheckSearchTag will always be added to the check.
// Default: none
CirconusCheckTags string `mapstructure:"circonus_check_tags"`
// CirconusCheckDisplayName is the name for the check which will be displayed in the Circonus UI.
// Default: value of CirconusCheckInstanceID
CirconusCheckDisplayName string `mapstructure:"circonus_check_display_name"`
// CirconusBrokerID is an explicit broker to use when creating a new check. The numeric portion
// of broker._cid. If metric management is enabled and neither a Submission URL nor Check ID
// is provided, an attempt will be made to search for an existing check using Instance ID and
@ -266,6 +313,10 @@ type Config struct {
// LogLevel is the level of the logs to putout
LogLevel string `mapstructure:"log_level"`
// Node ID is a unique ID for this node across space and time. Defaults
// to a randomly-generated ID that persists in the data-dir.
NodeID types.NodeID `mapstructure:"node_id"`
// Node name is the name we use to advertise. Defaults to hostname.
NodeName string `mapstructure:"node_name"`
@ -279,6 +330,18 @@ type Config struct {
// services (Gossip, Server RPC)
BindAddr string `mapstructure:"bind_addr"`
// SerfWanBindAddr is used to control the address we bind to.
// If not specified, the first private IP we find is used.
// This controls the address we use for cluster facing
// services (Gossip) Serf
SerfWanBindAddr string `mapstructure:"serf_wan_bind"`
// SerfLanBindAddr is used to control the address we bind to.
// If not specified, the first private IP we find is used.
// This controls the address we use for cluster facing
// services (Gossip) Serf
SerfLanBindAddr string `mapstructure:"serf_lan_bind"`
// AdvertiseAddr is the address we use for advertising our Serf,
// and Consul RPC IP. If not specified, bind address is used.
AdvertiseAddr string `mapstructure:"advertise_addr"`
@ -309,6 +372,11 @@ type Config struct {
// they are configured with TranslateWanAddrs set to true.
TaggedAddresses map[string]string
// Node metadata key/value pairs. These are excluded from JSON output
// because they can be reloaded and might be stale when shown from the
// config instead of the local state.
Meta map[string]string `mapstructure:"node_meta" json:"-"`
// LeaveOnTerm controls if Serf does a graceful leave when receiving
// the TERM signal. Defaults true on clients, false on servers. This can
// be changed on reload.
@ -361,6 +429,9 @@ type Config struct {
// provide matches the certificate
ServerName string `mapstructure:"server_name"`
// TLSMinVersion is used to set the minimum TLS version used for TLS connections.
TLSMinVersion string `mapstructure:"tls_min_version"`
// StartJoin is a list of addresses to attempt to join when the
// agent starts. If Serf is unable to communicate with any of these
// addresses, then the agent will error and exit.
@ -385,6 +456,12 @@ type Config struct {
RetryInterval time.Duration `mapstructure:"-" json:"-"`
RetryIntervalRaw string `mapstructure:"retry_interval"`
// RetryJoinEC2 configuration
RetryJoinEC2 RetryJoinEC2 `mapstructure:"retry_join_ec2"`
// The config struct for the GCE tag server discovery feature.
RetryJoinGCE RetryJoinGCE `mapstructure:"retry_join_gce"`
// RetryJoinWan is a list of addresses to join -wan with retry enabled.
RetryJoinWan []string `mapstructure:"retry_join_wan"`
@ -452,6 +529,16 @@ type Config struct {
// token is not provided. If not configured the 'anonymous' token is used.
ACLToken string `mapstructure:"acl_token" json:"-"`
// ACLAgentMasterToken is a special token that has full read and write
// privileges for this agent, and can be used to call agent endpoints
// when no servers are available.
ACLAgentMasterToken string `mapstructure:"acl_agent_master_token" json:"-"`
// ACLAgentToken is the default token used to make requests for the agent
// itself, such as for registering itself with the catalog. If not
// configured, the 'acl_token' will be used.
ACLAgentToken string `mapstructure:"acl_agent_token" json:"-"`
// ACLMasterToken is used to bootstrap the ACL system. It should be specified
// on the servers in the ACLDatacenter. When the leader comes online, it ensures
// that the Master token is available. This provides the initial token.
@ -473,9 +560,15 @@ type Config struct {
// white-lists.
ACLDefaultPolicy string `mapstructure:"acl_default_policy"`
// ACLDisabledTTL is used by clients to determine how long they will
// wait to check again with the servers if they discover ACLs are not
// enabled.
ACLDisabledTTL time.Duration `mapstructure:"-"`
// ACLDownPolicy is used to control the ACL interaction when we cannot
// reach the ACLDatacenter and the token is not in the cache.
// There are two modes:
// * allow - Allow all requests
// * deny - Deny all requests
// * extend-cache - Ignore the cache expiration, and allow cached
// ACL's to be used to service requests. This
@ -489,6 +582,10 @@ type Config struct {
// other than the ACLDatacenter.
ACLReplicationToken string `mapstructure:"acl_replication_token" json:"-"`
// ACLEnforceVersion8 is used to gate a set of ACL policy features that
// are opt-in prior to Consul 0.8 and opt-out in Consul 0.8 and later.
ACLEnforceVersion8 *bool `mapstructure:"acl_enforce_version_8"`
// Watches are used to monitor various endpoints and to invoke a
// handler to act appropriately. These are managed entirely in the
// agent layer using the standard APIs.
@ -648,12 +745,13 @@ func DefaultConfig() *Config {
DNSConfig: DNSConfig{
AllowStale: Bool(true),
UDPAnswerLimit: 3,
MaxStale: 5 * time.Second,
MaxStale: 10 * 365 * 24 * time.Hour,
RecursorTimeout: 2 * time.Second,
},
Telemetry: Telemetry{
StatsitePrefix: "consul",
},
Meta: make(map[string]string),
SyslogFacility: "LOCAL0",
Protocol: consul.ProtocolVersion2Compatible,
CheckUpdateInterval: 5 * time.Minute,
@ -669,11 +767,15 @@ func DefaultConfig() *Config {
SyncCoordinateRateTarget: 64.0, // updates / second
SyncCoordinateIntervalMin: 15 * time.Second,
ACLTTL: 30 * time.Second,
ACLDownPolicy: "extend-cache",
ACLDefaultPolicy: "allow",
RetryInterval: 30 * time.Second,
RetryIntervalWan: 30 * time.Second,
ACLTTL: 30 * time.Second,
ACLDownPolicy: "extend-cache",
ACLDefaultPolicy: "allow",
ACLDisabledTTL: 120 * time.Second,
ACLEnforceVersion8: Bool(false),
RetryInterval: 30 * time.Second,
RetryIntervalWan: 30 * time.Second,
TLSMinVersion: "tls10",
}
}
@ -715,6 +817,18 @@ func (c *Config) ClientListener(override string, port int) (net.Addr, error) {
return &net.TCPAddr{IP: ip, Port: port}, nil
}
// GetTokenForAgent returns the token the agent should use for its own internal
// operations, such as registering itself with the catalog.
func (c *Config) GetTokenForAgent() string {
if c.ACLAgentToken != "" {
return c.ACLAgentToken
} else if c.ACLToken != "" {
return c.ACLToken
} else {
return ""
}
}
// DecodeConfig reads the configuration from the given reader in JSON
// format and decodes it into a proper Config structure.
func DecodeConfig(r io.Reader) (*Config, error) {
@ -931,6 +1045,12 @@ func DecodeConfig(r io.Reader) (*Config, error) {
}
if result.AdvertiseAddrs.SerfLanRaw != "" {
ipStr, err := parseSingleIPTemplate(result.AdvertiseAddrs.SerfLanRaw)
if err != nil {
return nil, fmt.Errorf("Serf Advertise LAN address resolution failed: %v", err)
}
result.AdvertiseAddrs.SerfLanRaw = ipStr
addr, err := net.ResolveTCPAddr("tcp", result.AdvertiseAddrs.SerfLanRaw)
if err != nil {
return nil, fmt.Errorf("AdvertiseAddrs.SerfLan is invalid: %v", err)
@ -939,6 +1059,12 @@ func DecodeConfig(r io.Reader) (*Config, error) {
}
if result.AdvertiseAddrs.SerfWanRaw != "" {
ipStr, err := parseSingleIPTemplate(result.AdvertiseAddrs.SerfWanRaw)
if err != nil {
return nil, fmt.Errorf("Serf Advertise WAN address resolution failed: %v", err)
}
result.AdvertiseAddrs.SerfWanRaw = ipStr
addr, err := net.ResolveTCPAddr("tcp", result.AdvertiseAddrs.SerfWanRaw)
if err != nil {
return nil, fmt.Errorf("AdvertiseAddrs.SerfWan is invalid: %v", err)
@ -947,6 +1073,12 @@ func DecodeConfig(r io.Reader) (*Config, error) {
}
if result.AdvertiseAddrs.RPCRaw != "" {
ipStr, err := parseSingleIPTemplate(result.AdvertiseAddrs.RPCRaw)
if err != nil {
return nil, fmt.Errorf("RPC Advertise address resolution failed: %v", err)
}
result.AdvertiseAddrs.RPCRaw = ipStr
addr, err := net.ResolveTCPAddr("tcp", result.AdvertiseAddrs.RPCRaw)
if err != nil {
return nil, fmt.Errorf("AdvertiseAddrs.RPC is invalid: %v", err)
@ -1037,6 +1169,9 @@ func FixupCheckType(raw interface{}) error {
case "docker_container_id":
rawMap["DockerContainerID"] = v
delete(rawMap, k)
case "tls_skip_verify":
rawMap["TLSSkipVerify"] = v
delete(rawMap, k)
}
}
@ -1148,6 +1283,9 @@ func MergeConfig(a, b *Config) *Config {
if b.Protocol > 0 {
result.Protocol = b.Protocol
}
if b.NodeID != "" {
result.NodeID = b.NodeID
}
if b.NodeName != "" {
result.NodeName = b.NodeName
}
@ -1163,6 +1301,12 @@ func MergeConfig(a, b *Config) *Config {
if b.AdvertiseAddrWan != "" {
result.AdvertiseAddrWan = b.AdvertiseAddrWan
}
if b.SerfWanBindAddr != "" {
result.SerfWanBindAddr = b.SerfWanBindAddr
}
if b.SerfLanBindAddr != "" {
result.SerfLanBindAddr = b.SerfLanBindAddr
}
if b.TranslateWanAddrs == true {
result.TranslateWanAddrs = true
}
@ -1232,6 +1376,12 @@ func MergeConfig(a, b *Config) *Config {
if b.Telemetry.CirconusCheckSearchTag != "" {
result.Telemetry.CirconusCheckSearchTag = b.Telemetry.CirconusCheckSearchTag
}
if b.Telemetry.CirconusCheckDisplayName != "" {
result.Telemetry.CirconusCheckDisplayName = b.Telemetry.CirconusCheckDisplayName
}
if b.Telemetry.CirconusCheckTags != "" {
result.Telemetry.CirconusCheckTags = b.Telemetry.CirconusCheckTags
}
if b.Telemetry.CirconusBrokerID != "" {
result.Telemetry.CirconusBrokerID = b.Telemetry.CirconusBrokerID
}
@ -1262,6 +1412,9 @@ func MergeConfig(a, b *Config) *Config {
if b.ServerName != "" {
result.ServerName = b.ServerName
}
if b.TLSMinVersion != "" {
result.TLSMinVersion = b.TLSMinVersion
}
if b.Checks != nil {
result.Checks = append(result.Checks, b.Checks...)
}
@ -1322,6 +1475,33 @@ func MergeConfig(a, b *Config) *Config {
if b.RetryInterval != 0 {
result.RetryInterval = b.RetryInterval
}
if b.RetryJoinEC2.AccessKeyID != "" {
result.RetryJoinEC2.AccessKeyID = b.RetryJoinEC2.AccessKeyID
}
if b.RetryJoinEC2.SecretAccessKey != "" {
result.RetryJoinEC2.SecretAccessKey = b.RetryJoinEC2.SecretAccessKey
}
if b.RetryJoinEC2.Region != "" {
result.RetryJoinEC2.Region = b.RetryJoinEC2.Region
}
if b.RetryJoinEC2.TagKey != "" {
result.RetryJoinEC2.TagKey = b.RetryJoinEC2.TagKey
}
if b.RetryJoinEC2.TagValue != "" {
result.RetryJoinEC2.TagValue = b.RetryJoinEC2.TagValue
}
if b.RetryJoinGCE.ProjectName != "" {
result.RetryJoinGCE.ProjectName = b.RetryJoinGCE.ProjectName
}
if b.RetryJoinGCE.ZonePattern != "" {
result.RetryJoinGCE.ZonePattern = b.RetryJoinGCE.ZonePattern
}
if b.RetryJoinGCE.TagValue != "" {
result.RetryJoinGCE.TagValue = b.RetryJoinGCE.TagValue
}
if b.RetryJoinGCE.CredentialsFile != "" {
result.RetryJoinGCE.CredentialsFile = b.RetryJoinGCE.CredentialsFile
}
if b.RetryMaxAttemptsWan != 0 {
result.RetryMaxAttemptsWan = b.RetryMaxAttemptsWan
}
@ -1377,6 +1557,12 @@ func MergeConfig(a, b *Config) *Config {
if b.ACLToken != "" {
result.ACLToken = b.ACLToken
}
if b.ACLAgentMasterToken != "" {
result.ACLAgentMasterToken = b.ACLAgentMasterToken
}
if b.ACLAgentToken != "" {
result.ACLAgentToken = b.ACLAgentToken
}
if b.ACLMasterToken != "" {
result.ACLMasterToken = b.ACLMasterToken
}
@ -1396,6 +1582,9 @@ func MergeConfig(a, b *Config) *Config {
if b.ACLReplicationToken != "" {
result.ACLReplicationToken = b.ACLReplicationToken
}
if b.ACLEnforceVersion8 != nil {
result.ACLEnforceVersion8 = b.ACLEnforceVersion8
}
if len(b.Watches) != 0 {
result.Watches = append(result.Watches, b.Watches...)
}
@ -1450,6 +1639,14 @@ func MergeConfig(a, b *Config) *Config {
result.HTTPAPIResponseHeaders[field] = value
}
}
if len(b.Meta) != 0 {
if result.Meta == nil {
result.Meta = make(map[string]string)
}
for field, value := range b.Meta {
result.Meta[field] = value
}
}
// Copy the start join addresses
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))

View File

@ -60,7 +60,7 @@ func TestDecodeConfig(t *testing.T) {
}
// Without a protocol
input = `{"node_name": "foo", "datacenter": "dc2"}`
input = `{"node_id": "bar", "node_name": "foo", "datacenter": "dc2"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
@ -70,6 +70,10 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
if config.NodeID != "bar" {
t.Fatalf("bad: %#v", config)
}
if config.Datacenter != "dc2" {
t.Fatalf("bad: %#v", config)
}
@ -179,8 +183,9 @@ func TestDecodeConfig(t *testing.T) {
}
// Server addrs
input = `{"ports": {"server": 8000}, "bind_addr": "127.0.0.2", "advertise_addr": "127.0.0.3"}`
input = `{"ports": {"server": 8000}, "bind_addr": "127.0.0.2", "advertise_addr": "127.0.0.3", "serf_lan_bind": "127.0.0.4", "serf_wan_bind": "52.54.55.56"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
@ -189,6 +194,14 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
if config.SerfWanBindAddr != "52.54.55.56" {
t.Fatalf("bad: %#v", config)
}
if config.SerfLanBindAddr != "127.0.0.4" {
t.Fatalf("bad: %#v", config)
}
if config.AdvertiseAddr != "127.0.0.3" {
t.Fatalf("bad: %#v", config)
}
@ -272,6 +285,19 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
// Node metadata fields
input = `{"node_meta": {"thing1": "1", "thing2": "2"}}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if v, ok := config.Meta["thing1"]; !ok || v != "1" {
t.Fatalf("bad: %#v", config)
}
if v, ok := config.Meta["thing2"]; !ok || v != "2" {
t.Fatalf("bad: %#v", config)
}
// leave_on_terminate
input = `{"leave_on_terminate": true}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
@ -306,7 +332,7 @@ func TestDecodeConfig(t *testing.T) {
}
// TLS
input = `{"verify_incoming": true, "verify_outgoing": true, "verify_server_hostname": true}`
input = `{"verify_incoming": true, "verify_outgoing": true, "verify_server_hostname": true, "tls_min_version": "tls12"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
@ -324,6 +350,10 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
if config.TLSMinVersion != "tls12" {
t.Fatalf("bad: %#v", config)
}
// TLS keys
input = `{"ca_file": "my/ca/file", "cert_file": "my.cert", "key_file": "key.pem", "server_name": "example.com"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
@ -634,7 +664,8 @@ func TestDecodeConfig(t *testing.T) {
}
// ACLs
input = `{"acl_token": "1234", "acl_datacenter": "dc2",
input = `{"acl_token": "1111", "acl_agent_master_token": "2222",
"acl_agent_token": "3333", "acl_datacenter": "dc2",
"acl_ttl": "60s", "acl_down_policy": "deny",
"acl_default_policy": "deny", "acl_master_token": "2345",
"acl_replication_token": "8675309"}`
@ -643,7 +674,13 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("err: %s", err)
}
if config.ACLToken != "1234" {
if config.ACLToken != "1111" {
t.Fatalf("bad: %#v", config)
}
if config.ACLAgentMasterToken != "2222" {
t.Fatalf("bad: %#v", config)
}
if config.ACLAgentToken != "3333" {
t.Fatalf("bad: %#v", config)
}
if config.ACLMasterToken != "2345" {
@ -665,6 +702,48 @@ func TestDecodeConfig(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
// ACL token precedence.
input = `{}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if token := config.GetTokenForAgent(); token != "" {
t.Fatalf("bad: %s", token)
}
input = `{"acl_token": "hello"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if token := config.GetTokenForAgent(); token != "hello" {
t.Fatalf("bad: %s", token)
}
input = `{"acl_agent_token": "world", "acl_token": "hello"}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if token := config.GetTokenForAgent(); token != "world" {
t.Fatalf("bad: %s", token)
}
// ACL flag for Consul version 0.8 features (broken out since we will
// eventually remove this). We first verify this is opt-out.
config = DefaultConfig()
if *config.ACLEnforceVersion8 != false {
t.Fatalf("bad: %#v", config)
}
input = `{"acl_enforce_version_8": true}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if *config.ACLEnforceVersion8 != true {
t.Fatalf("bad: %#v", config)
}
// Watches
input = `{"watches": [{"type":"keyprefix", "prefix":"foo/", "handler":"foobar"}]}`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
@ -749,6 +828,7 @@ func TestDecodeConfig(t *testing.T) {
"circonus_submission_url": "https://submit.host.bar:123/one/two/three",
"circonus_check_id": "12345", "circonus_check_force_metric_activation": "true",
"circonus_check_instance_id": "a:b", "circonus_check_search_tag": "c:d",
"circonus_check_display_name": "node1:consul", "circonus_check_tags": "cat1:tag1,cat2:tag2",
"circonus_broker_id": "6789", "circonus_broker_select_tag": "e:f"} }`
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
@ -781,6 +861,12 @@ func TestDecodeConfig(t *testing.T) {
if config.Telemetry.CirconusCheckSearchTag != "c:d" {
t.Fatalf("bad: %#v", config)
}
if config.Telemetry.CirconusCheckDisplayName != "node1:consul" {
t.Fatalf("bad: %#v", config)
}
if config.Telemetry.CirconusCheckTags != "cat1:tag1,cat2:tag2" {
t.Fatalf("bad: %#v", config)
}
if config.Telemetry.CirconusBrokerID != "6789" {
t.Fatalf("bad: %#v", config)
}
@ -939,6 +1025,62 @@ func TestDecodeConfig_invalidKeys(t *testing.T) {
}
}
func TestRetryJoinEC2(t *testing.T) {
input := `{"retry_join_ec2": {
"region": "us-east-1",
"tag_key": "ConsulRole",
"tag_value": "Server",
"access_key_id": "asdf",
"secret_access_key": "qwerty"
}}`
config, err := DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if config.RetryJoinEC2.Region != "us-east-1" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinEC2.TagKey != "ConsulRole" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinEC2.TagValue != "Server" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinEC2.AccessKeyID != "asdf" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinEC2.SecretAccessKey != "qwerty" {
t.Fatalf("bad: %#v", config)
}
}
func TestRetryJoinGCE(t *testing.T) {
input := `{"retry_join_gce": {
"project_name": "test-project",
"zone_pattern": "us-west1-a",
"tag_value": "consul-server",
"credentials_file": "/path/to/foo.json"
}}`
config, err := DecodeConfig(bytes.NewReader([]byte(input)))
if err != nil {
t.Fatalf("err: %s", err)
}
if config.RetryJoinGCE.ProjectName != "test-project" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinGCE.ZonePattern != "us-west1-a" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinGCE.TagValue != "consul-server" {
t.Fatalf("bad: %#v", config)
}
if config.RetryJoinGCE.CredentialsFile != "/path/to/foo.json" {
t.Fatalf("bad: %#v", config)
}
}
func TestDecodeConfig_Performance(t *testing.T) {
input := `{"performance": { "raft_multiplier": 3 }}`
config, err := DecodeConfig(bytes.NewReader([]byte(input)))
@ -1136,6 +1278,23 @@ func TestDecodeConfig_Checks(t *testing.T) {
"interval": "10s",
"timeout": "100ms",
"service_id": "elasticsearch"
},
{
"id": "chk5",
"name": "service:sslservice",
"HTTP": "https://sslservice/status",
"interval": "10s",
"timeout": "100ms",
"service_id": "sslservice"
},
{
"id": "chk6",
"name": "service:insecure-sslservice",
"HTTP": "https://insecure-sslservice/status",
"interval": "10s",
"timeout": "100ms",
"service_id": "insecure-sslservice",
"tls_skip_verify": true
}
]
}`
@ -1182,6 +1341,28 @@ func TestDecodeConfig_Checks(t *testing.T) {
Timeout: 100 * time.Millisecond,
},
},
&CheckDefinition{
ID: "chk5",
Name: "service:sslservice",
ServiceID: "sslservice",
CheckType: CheckType{
HTTP: "https://sslservice/status",
Interval: 10 * time.Second,
Timeout: 100 * time.Millisecond,
TLSSkipVerify: false,
},
},
&CheckDefinition{
ID: "chk6",
Name: "service:insecure-sslservice",
ServiceID: "insecure-sslservice",
CheckType: CheckType{
HTTP: "https://insecure-sslservice/status",
Interval: 10 * time.Second,
Timeout: 100 * time.Millisecond,
TLSSkipVerify: true,
},
},
},
}
@ -1359,6 +1540,7 @@ func TestMergeConfig(t *testing.T) {
DataDir: "/tmp/foo",
Domain: "basic",
LogLevel: "debug",
NodeID: "bar",
NodeName: "foo",
ClientAddr: "127.0.0.1",
BindAddr: "127.0.0.1",
@ -1370,6 +1552,13 @@ func TestMergeConfig(t *testing.T) {
CheckUpdateIntervalRaw: "8m",
RetryIntervalRaw: "10s",
RetryIntervalWanRaw: "10s",
RetryJoinEC2: RetryJoinEC2{
Region: "us-east-1",
TagKey: "Key1",
TagValue: "Value1",
AccessKeyID: "nope",
SecretAccessKey: "nope",
},
Telemetry: Telemetry{
DisableHostname: false,
StatsdAddr: "nope",
@ -1378,6 +1567,9 @@ func TestMergeConfig(t *testing.T) {
DogStatsdAddr: "nope",
DogStatsdTags: []string{"nope"},
},
Meta: map[string]string{
"key": "value1",
},
}
b := &Config{
@ -1403,6 +1595,7 @@ func TestMergeConfig(t *testing.T) {
},
Domain: "other",
LogLevel: "info",
NodeID: "bar",
NodeName: "baz",
ClientAddr: "127.0.0.2",
BindAddr: "127.0.0.2",
@ -1432,6 +1625,7 @@ func TestMergeConfig(t *testing.T) {
CAFile: "test/ca.pem",
CertFile: "test/cert.pem",
KeyFile: "test/key.pem",
TLSMinVersion: "tls12",
Checks: []*CheckDefinition{nil},
Services: []*ServiceDefinition{nil},
StartJoin: []string{"1.1.1.1"},
@ -1452,14 +1646,17 @@ func TestMergeConfig(t *testing.T) {
ReconnectTimeoutWan: 36 * time.Hour,
CheckUpdateInterval: 8 * time.Minute,
CheckUpdateIntervalRaw: "8m",
ACLToken: "1234",
ACLMasterToken: "2345",
ACLToken: "1111",
ACLAgentMasterToken: "2222",
ACLAgentToken: "3333",
ACLMasterToken: "4444",
ACLDatacenter: "dc2",
ACLTTL: 15 * time.Second,
ACLTTLRaw: "15s",
ACLDownPolicy: "deny",
ACLDefaultPolicy: "deny",
ACLReplicationToken: "8765309",
ACLEnforceVersion8: Bool(true),
Watches: []map[string]interface{}{
map[string]interface{}{
"type": "keyprefix",
@ -1476,6 +1673,9 @@ func TestMergeConfig(t *testing.T) {
DogStatsdAddr: "127.0.0.1:7254",
DogStatsdTags: []string{"tag_1:val_1", "tag_2:val_2"},
},
Meta: map[string]string{
"key": "value2",
},
DisableUpdateCheck: true,
DisableAnonymousSignature: true,
HTTPAPIResponseHeaders: map[string]string{
@ -1492,8 +1692,15 @@ func TestMergeConfig(t *testing.T) {
AtlasToken: "123456789",
AtlasACLToken: "abcdefgh",
AtlasJoin: true,
SessionTTLMinRaw: "1000s",
SessionTTLMin: 1000 * time.Second,
RetryJoinEC2: RetryJoinEC2{
Region: "us-east-2",
TagKey: "Key2",
TagValue: "Value2",
AccessKeyID: "foo",
SecretAccessKey: "bar",
},
SessionTTLMinRaw: "1000s",
SessionTTLMin: 1000 * time.Second,
AdvertiseAddrs: AdvertiseAddrsConfig{
SerfLan: &net.TCPAddr{},
SerfLanRaw: "127.0.0.5:1231",

View File

@ -1,6 +1,7 @@
package agent
import (
"encoding/hex"
"fmt"
"io"
"log"
@ -23,6 +24,9 @@ const (
// times.
maxUDPAnswerLimit = 8
maxRecurseRecords = 5
// Increment a counter when requests staler than this are served
staleCounterThreshold = 5 * time.Second
)
// DNSServer is used to wrap an Agent and expose various
@ -50,8 +54,8 @@ func (d *DNSServer) Shutdown() {
// NewDNSServer starts a new DNS server to provide an agent interface
func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain string, bind string, recursors []string) (*DNSServer, error) {
// Make sure domain is FQDN
domain = dns.Fqdn(domain)
// Make sure domain is FQDN, make it case insensitive for ServeMux
domain = dns.Fqdn(strings.ToLower(domain))
// Construct the DNS components
mux := dns.NewServeMux()
@ -357,6 +361,46 @@ PARSE:
query := strings.Join(labels[:n-1], ".")
d.preparedQueryLookup(network, datacenter, query, req, resp)
case "addr":
if n != 2 {
goto INVALID
}
switch len(labels[0]) / 2 {
// IPv4
case 4:
ip, err := hex.DecodeString(labels[0])
if err != nil {
goto INVALID
}
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: qName + d.domain,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(d.config.NodeTTL / time.Second),
},
A: ip,
})
// IPv6
case 16:
ip, err := hex.DecodeString(labels[0])
if err != nil {
goto INVALID
}
resp.Answer = append(resp.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: qName + d.domain,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: uint32(d.config.NodeTTL / time.Second),
},
AAAA: ip,
})
}
default:
// Store the DC, and re-parse
datacenter = labels[n-1]
@ -396,10 +440,14 @@ RPC:
}
// 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
if args.AllowStale {
if out.LastContact > d.config.MaxStale {
args.AllowStale = false
d.logger.Printf("[WARN] dns: Query results too stale, re-requesting")
goto RPC
} else if out.LastContact > staleCounterThreshold {
metrics.IncrCounter([]string{"consul", "dns", "stale_queries"}, 1)
}
}
// If we have no address, return not found!
@ -596,10 +644,14 @@ RPC:
}
// 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
if args.AllowStale {
if out.LastContact > d.config.MaxStale {
args.AllowStale = false
d.logger.Printf("[WARN] dns: Query results too stale, re-requesting")
goto RPC
} else if out.LastContact > staleCounterThreshold {
metrics.IncrCounter([]string{"consul", "dns", "stale_queries"}, 1)
}
}
// Determine the TTL
@ -698,10 +750,14 @@ RPC:
}
// 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
if args.AllowStale {
if out.LastContact > d.config.MaxStale {
args.AllowStale = false
d.logger.Printf("[WARN] dns: Query results too stale, re-requesting")
goto RPC
} else if out.LastContact > staleCounterThreshold {
metrics.IncrCounter([]string{"consul", "dns", "stale_queries"}, 1)
}
}
// Determine the TTL. The parse should never fail since we vet it when
@ -820,8 +876,35 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
// Add the extra record
records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl)
if records != nil {
resp.Extra = append(resp.Extra, records...)
if len(records) > 0 {
// Use the node address if it doesn't differ from the service address
if addr == node.Node.Address {
resp.Extra = append(resp.Extra, records...)
} else {
// If it differs from the service address, give a special response in the
// 'addr.consul' domain with the service IP encoded in it. We have to do
// this because we can't put an IP in the target field of an SRV record.
switch record := records[0].(type) {
// IPv4
case *dns.A:
addr := hex.EncodeToString(record.A)
// Take the last 8 chars (4 bytes) of the encoded address to avoid junk bytes
srvRec.Target = fmt.Sprintf("%s.addr.%s.%s", addr[len(addr)-(net.IPv4len*2):], dc, d.domain)
record.Hdr.Name = srvRec.Target
resp.Extra = append(resp.Extra, record)
// IPv6
case *dns.AAAA:
srvRec.Target = fmt.Sprintf("%s.addr.%s.%s", hex.EncodeToString(record.AAAA), dc, d.domain)
record.Hdr.Name = srvRec.Target
resp.Extra = append(resp.Extra, record)
// Something else (probably a CNAME; just add the records).
default:
resp.Extra = append(resp.Extra, records...)
}
}
}
}
}
@ -848,7 +931,7 @@ func (d *DNSServer) handleRecurse(resp dns.ResponseWriter, req *dns.Msg) {
var err error
for _, recursor := range d.recursors {
r, rtt, err = c.Exchange(req, recursor)
if err == nil {
if err == nil || err == dns.ErrTruncated {
// Compress the response; we don't know if the incoming
// response was compressed or not, so by not compressing
// we might generate an invalid packet on the way out.
@ -877,6 +960,19 @@ func (d *DNSServer) handleRecurse(resp dns.ResponseWriter, req *dns.Msg) {
// resolveCNAME is used to recursively resolve CNAME records
func (d *DNSServer) resolveCNAME(name string) []dns.RR {
// If the CNAME record points to a Consul address, resolve it internally
// Convert query to lowercase because DNS is case insensitive; d.domain is
// already converted
if strings.HasSuffix(strings.ToLower(name), "."+d.domain) {
req := &dns.Msg{}
resp := &dns.Msg{}
req.SetQuestion(name, dns.TypeANY)
d.dispatch("udp", req, resp)
return resp.Answer
}
// Do nothing if we don't have a recursor
if len(d.recursors) == 0 {
return nil

View File

@ -86,6 +86,25 @@ func makeRecursor(t *testing.T, answer []dns.RR) *dns.Server {
return server
}
func makeRecursorWithMessage(t *testing.T, answer dns.Msg) *dns.Server {
dnsConf := nextConfig()
dnsAddr := fmt.Sprintf("%s:%d", dnsConf.Addresses.DNS, dnsConf.Ports.DNS)
mux := dns.NewServeMux()
mux.HandleFunc(".", func(resp dns.ResponseWriter, msg *dns.Msg) {
answer.SetReply(msg)
if err := resp.WriteMsg(&answer); err != nil {
t.Fatalf("err: %s", err)
}
})
server := &dns.Server{
Addr: dnsAddr,
Net: "udp",
Handler: mux,
}
go server.ListenAndServe()
return server
}
// dnsCNAME returns a DNS CNAME record struct
func dnsCNAME(src, dest string) *dns.CNAME {
return &dns.CNAME{
@ -550,6 +569,7 @@ func TestDNS_ServiceLookup(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -639,7 +659,336 @@ func TestDNS_ServiceLookup(t *testing.T) {
}
}
func TestDNS_ServiceLookup_ServiceAddress(t *testing.T) {
func TestDNS_ExternalServiceLookup(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 with an external service.
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "www.google.com",
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)
}
}
// Look up the service
questions := []string{
"db.service.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) != 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])
}
cnameRec, ok := in.Extra[0].(*dns.CNAME)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Name != "foo.node.dc1.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Target != "www.google.com." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[0])
}
}
}
func TestDNS_ExternalServiceToConsulCNAMELookup(t *testing.T) {
dir, srv := makeDNSServerConfig(t, func(c *Config) {
c.Domain = "CONSUL."
}, nil)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
// Register the initial node with a service
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "web",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "web",
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Register an external service pointing to the 'web' service
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "alias",
Address: "web.service.consul",
Service: &structs.NodeService{
Service: "alias",
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Look up the service directly
questions := []string{
"alias.service.consul.",
"alias.service.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) != 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 != "alias.node.dc1.consul." {
t.Fatalf("Bad: %#v", srvRec)
}
if srvRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Answer[0])
}
if len(in.Extra) != 2 {
t.Fatalf("Bad: %#v", in)
}
cnameRec, ok := in.Extra[0].(*dns.CNAME)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Name != "alias.node.dc1.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Target != "web.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[0])
}
aRec, ok := in.Extra[1].(*dns.A)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if aRec.Hdr.Name != "web.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if aRec.A.String() != "127.0.0.1" {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if aRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[1])
}
}
}
func TestDNS_ExternalServiceToConsulCNAMENestedLookup(t *testing.T) {
dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
// Register the initial node with a service
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "web",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "web",
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Register an external service pointing to the 'web' service
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "alias",
Address: "web.service.consul",
Service: &structs.NodeService{
Service: "alias",
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Register an external service pointing to the 'alias' service
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "alias2",
Address: "alias.service.consul",
Service: &structs.NodeService{
Service: "alias2",
Port: 12345,
},
}
var out struct{}
if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
}
// Look up the service directly
questions := []string{
"alias2.service.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) != 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 != "alias2.node.dc1.consul." {
t.Fatalf("Bad: %#v", srvRec)
}
if srvRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Answer[0])
}
if len(in.Extra) != 3 {
t.Fatalf("Bad: %#v", in)
}
cnameRec, ok := in.Extra[0].(*dns.CNAME)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Name != "alias2.node.dc1.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Target != "alias.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[0])
}
cnameRec, ok = in.Extra[1].(*dns.CNAME)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if cnameRec.Hdr.Name != "alias.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if cnameRec.Target != "web.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if cnameRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[1])
}
aRec, ok := in.Extra[2].(*dns.A)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[2])
}
if aRec.Hdr.Name != "web.service.consul." {
t.Fatalf("Bad: %#v", in.Extra[1])
}
if aRec.A.String() != "127.0.0.1" {
t.Fatalf("Bad: %#v", in.Extra[2])
}
if aRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[2])
}
}
}
func TestDNS_ServiceLookup_ServiceAddress_A(t *testing.T) {
dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
@ -673,6 +1022,102 @@ func TestDNS_ServiceLookup_ServiceAddress(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
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.
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) != 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 != "7f000002.addr.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 != "7f000002.addr.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])
}
}
}
func TestDNS_ServiceLookup_ServiceAddress_CNAME(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 with a service whose address isn't an IP.
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "db",
Tags: []string{"master"},
Address: "www.google.com",
Port: 12345,
},
}
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{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -717,14 +1162,109 @@ func TestDNS_ServiceLookup_ServiceAddress(t *testing.T) {
t.Fatalf("Bad: %#v", in.Answer[0])
}
aRec, ok := in.Extra[0].(*dns.A)
cnameRec, ok := in.Extra[0].(*dns.CNAME)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.Hdr.Name != "foo.node.dc1.consul." {
if cnameRec.Hdr.Name != "foo.node.dc1.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.A.String() != "127.0.0.2" {
if cnameRec.Target != "www.google.com." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if cnameRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Extra[0])
}
}
}
func TestDNS_ServiceLookup_ServiceAddressIPV6(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 with a service.
{
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.1",
Service: &structs.NodeService{
Service: "db",
Tags: []string{"master"},
Address: "2607:20:4005:808::200e",
Port: 12345,
},
}
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{
Name: "test",
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.
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) != 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 != "2607002040050808000000000000200e.addr.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.AAAA)
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.Hdr.Name != "2607002040050808000000000000200e.addr.dc1.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.AAAA.String() != "2607:20:4005:808::200e" {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.Hdr.Ttl != 0 {
@ -794,6 +1334,7 @@ func TestDNS_ServiceLookup_WanAddress(t *testing.T) {
Datacenter: "dc2",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -828,7 +1369,7 @@ func TestDNS_ServiceLookup_WanAddress(t *testing.T) {
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.Hdr.Name != "foo.node.dc2.consul." {
if aRec.Hdr.Name != "7f000002.addr.dc2.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if aRec.A.String() != "127.0.0.2" {
@ -1199,6 +1740,7 @@ func TestDNS_ServiceLookup_Dedup(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -1303,6 +1845,7 @@ func TestDNS_ServiceLookup_Dedup_SRV(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -1400,12 +1943,58 @@ func TestDNS_Recurse(t *testing.T) {
}
}
func TestDNS_Recurse_Truncation(t *testing.T) {
answerMessage := dns.Msg{
MsgHdr: dns.MsgHdr{Truncated: true},
Answer: []dns.RR{dnsA("apple.com", "1.2.3.4")},
}
recursor := makeRecursorWithMessage(t, answerMessage)
defer recursor.Shutdown()
dir, srv := makeDNSServerConfig(t, func(c *Config) {
c.DNSRecursor = recursor.Addr
}, nil)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
m := new(dns.Msg)
m.SetQuestion("apple.com.", 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 != dns.ErrTruncated {
t.Fatalf("err: %v", err)
}
if in.Truncated != true {
t.Fatalf("err: message should have been truncated %v", in)
}
if len(in.Answer) == 0 {
t.Fatalf("Bad: Truncated message ignored, expected some reply %#v", in)
}
if in.Rcode != dns.RcodeSuccess {
t.Fatalf("Bad: %#v", in)
}
}
func TestDNS_RecursorTimeout(t *testing.T) {
serverClientTimeout := 3 * time.Second
testClientTimeout := serverClientTimeout + 5*time.Second
resolverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
if err != nil {
t.Error(err)
}
resolver, err := net.ListenUDP("udp", resolverAddr)
if err != nil {
t.Error(err)
}
defer resolver.Close()
dir, srv := makeDNSServerConfig(t, func(c *Config) {
c.DNSRecursor = "10.255.255.1" // host must cause a connection|read|write timeout
c.DNSRecursor = resolver.LocalAddr().String() // host must cause a connection|read|write timeout
}, func(c *DNSConfig) {
c.RecursorTimeout = serverClientTimeout
})
@ -1552,6 +2141,7 @@ func TestDNS_ServiceLookup_FilterCritical(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -1675,6 +2265,7 @@ func TestDNS_ServiceLookup_OnlyFailing(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},
@ -1795,6 +2386,7 @@ func TestDNS_ServiceLookup_OnlyPassing(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
OnlyPassing: true,
@ -1868,6 +2460,7 @@ func TestDNS_ServiceLookup_Randomize(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "web",
},
@ -1962,6 +2555,7 @@ func TestDNS_ServiceLookup_Truncate(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "web",
},
@ -2282,6 +2876,7 @@ func TestDNS_ServiceLookup_CNAME(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "search",
},
@ -2853,7 +3448,7 @@ func TestDNS_PreparedQuery_Failover(t *testing.T) {
if !ok {
t.Fatalf("Bad: %#v", in.Answer[0])
}
if srv.Target != "foo.node.dc2.consul." {
if srv.Target != "7f000002.addr.dc2.consul." {
t.Fatalf("Bad: %#v", in.Answer[0])
}
@ -2861,7 +3456,7 @@ func TestDNS_PreparedQuery_Failover(t *testing.T) {
if !ok {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if a.Hdr.Name != "foo.node.dc2.consul." {
if a.Hdr.Name != "7f000002.addr.dc2.consul." {
t.Fatalf("Bad: %#v", in.Extra[0])
}
if a.A.String() != "127.0.0.2" {
@ -3059,6 +3654,85 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) {
}
}
func TestDNS_AddressLookup(t *testing.T) {
dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
// Look up the addresses
cases := map[string]string{
"7f000001.addr.dc1.consul.": "127.0.0.1",
}
for question, answer := range cases {
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) != 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.To4().String() != answer {
t.Fatalf("Bad: %#v", aRec)
}
if aRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Answer[0])
}
}
}
func TestDNS_AddressLookupIPV6(t *testing.T) {
dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir)
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
// Look up the addresses
cases := map[string]string{
"2607002040050808000000000000200e.addr.consul.": "2607:20:4005:808::200e",
"2607112040051808ffffffffffff200e.addr.consul.": "2607:1120:4005:1808:ffff:ffff:ffff:200e",
}
for question, answer := range cases {
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) != 1 {
t.Fatalf("Bad: %#v", in)
}
aaaaRec, ok := in.Answer[0].(*dns.AAAA)
if !ok {
t.Fatalf("Bad: %#v", in.Answer[0])
}
if aaaaRec.AAAA.To16().String() != answer {
t.Fatalf("Bad: %#v", aaaaRec)
}
if aaaaRec.Hdr.Ttl != 0 {
t.Fatalf("Bad: %#v", in.Answer[0])
}
}
}
func TestDNS_NonExistingLookup(t *testing.T) {
dir, srv := makeDNSServer(t)
defer os.RemoveAll(dir)
@ -3775,6 +4449,7 @@ func TestDNS_Compression_Query(t *testing.T) {
Datacenter: "dc1",
Op: structs.PreparedQueryCreate,
Query: &structs.PreparedQuery{
Name: "test",
Service: structs.ServiceQuery{
Service: "db",
},

View File

@ -83,13 +83,21 @@ func (s *HTTPServer) EventList(resp http.ResponseWriter, req *http.Request) (int
return nil, nil
}
// Fetch the ACL token, if any.
var token string
s.parseToken(req, &token)
acl, err := s.agent.resolveToken(token)
if err != nil {
return nil, err
}
// Look for a name filter
var nameFilter string
if filt := req.URL.Query().Get("name"); filt != "" {
nameFilter = filt
}
// Lots of this logic is borrowed from consul/rpc.go:blockingRPC
// Lots of this logic is borrowed from consul/rpc.go:blockingQuery
// However we cannot use that directly since this code has some
// slight semantics differences...
var timeout <-chan time.Time
@ -126,7 +134,20 @@ RUN_QUERY:
// Get the recent events
events := s.agent.UserEvents()
// Filter the events if necessary
// Filter the events using the ACL, if present
if acl != nil {
for i := 0; i < len(events); i++ {
name := events[i].Name
if acl.EventRead(name) {
continue
}
s.agent.logger.Printf("[DEBUG] agent: dropping event %q from result due to ACLs", name)
events = append(events[:i], events[i+1:]...)
i--
}
}
// Filter the events if requested
if nameFilter != "" {
for i := 0; i < len(events); i++ {
if events[i].Name != nameFilter {

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
@ -192,6 +193,71 @@ func TestEventList_Filter(t *testing.T) {
})
}
func TestEventList_ACLFilter(t *testing.T) {
dir, srv := makeHTTPServerWithACLs(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
defer srv.agent.Shutdown()
// Fire an event.
p := &UserEvent{Name: "foo"}
if err := srv.agent.UserEvent("dc1", "root", p); err != nil {
t.Fatalf("err: %v", err)
}
// Try no token.
{
testutil.WaitForResult(func() (bool, error) {
req, err := http.NewRequest("GET", "/v1/event/list", nil)
if err != nil {
return false, err
}
resp := httptest.NewRecorder()
obj, err := srv.EventList(resp, req)
if err != nil {
return false, err
}
list, ok := obj.([]*UserEvent)
if !ok {
return false, fmt.Errorf("bad: %#v", obj)
}
if len(list) != 0 {
return false, fmt.Errorf("bad: %#v", list)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v", err)
})
}
// Try the root token.
{
testutil.WaitForResult(func() (bool, error) {
req, err := http.NewRequest("GET", "/v1/event/list?token=root", nil)
if err != nil {
return false, err
}
resp := httptest.NewRecorder()
obj, err := srv.EventList(resp, req)
if err != nil {
return false, err
}
list, ok := obj.([]*UserEvent)
if !ok {
return false, fmt.Errorf("bad: %#v", obj)
}
if len(list) != 1 || list[0].Name != "foo" {
return false, fmt.Errorf("bad: %#v", list)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v", err)
})
}
}
func TestEventList_Blocking(t *testing.T) {
httpTest(t, func(srv *HTTPServer) {
p := &UserEvent{Name: "test"}

View File

@ -11,6 +11,7 @@ func (s *HTTPServer) HealthChecksInState(resp http.ResponseWriter, req *http.Req
// Set default DC
args := structs.ChecksInStateRequest{}
s.parseSource(req, &args.Source)
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
@ -70,6 +71,7 @@ func (s *HTTPServer) HealthServiceChecks(resp http.ResponseWriter, req *http.Req
// Set default DC
args := structs.ServiceSpecificRequest{}
s.parseSource(req, &args.Source)
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
@ -100,6 +102,7 @@ func (s *HTTPServer) HealthServiceNodes(resp http.ResponseWriter, req *http.Requ
// Set default DC
args := structs.ServiceSpecificRequest{}
s.parseSource(req, &args.Source)
args.NodeMetaFilters = s.parseMetaFilter(req)
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}

View File

@ -7,7 +7,6 @@ import (
"os"
"reflect"
"testing"
"time"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil"
@ -66,6 +65,49 @@ func TestHealthChecksInState(t *testing.T) {
})
}
func TestHealthChecksInState_NodeMetaFilter(t *testing.T) {
httpTest(t, func(srv *HTTPServer) {
args := &structs.RegisterRequest{
Datacenter: "dc1",
Node: "bar",
Address: "127.0.0.1",
NodeMeta: map[string]string{"somekey": "somevalue"},
Check: &structs.HealthCheck{
Node: "bar",
Name: "node check",
Status: structs.HealthCritical,
},
}
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/state/critical?node-meta=somekey:somevalue", 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 1 health check for the server
nodes := obj.(structs.HealthChecks)
if len(nodes) != 1 {
return false, fmt.Errorf("bad: %v", obj)
}
return true, nil
}, func(err error) { t.Fatalf("err: %v", err) })
})
}
func TestHealthChecksInState_DistanceSort(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
@ -126,25 +168,29 @@ func TestHealthChecksInState_DistanceSort(t *testing.T) {
if err := srv.agent.RPC("Coordinate.Update", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
time.Sleep(300 * time.Millisecond)
// Query again and now foo should have moved to the front of the line.
resp = httptest.NewRecorder()
obj, err = srv.HealthChecksInState(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.HealthChecks)
if len(nodes) != 2 {
t.Fatalf("bad: %v", nodes)
}
if nodes[0].Node != "foo" {
t.Fatalf("bad: %v", nodes)
}
if nodes[1].Node != "bar" {
t.Fatalf("bad: %v", nodes)
}
// Retry until foo moves to the front of the line.
testutil.WaitForResult(func() (bool, error) {
resp = httptest.NewRecorder()
obj, err = srv.HealthChecksInState(resp, req)
if err != nil {
return false, fmt.Errorf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.HealthChecks)
if len(nodes) != 2 {
return false, fmt.Errorf("bad: %v", nodes)
}
if nodes[0].Node != "foo" {
return false, fmt.Errorf("bad: %v", nodes)
}
if nodes[1].Node != "bar" {
return false, fmt.Errorf("bad: %v", nodes)
}
return true, nil
}, func(err error) {
t.Fatalf("failed to get sorted service nodes: %v", err)
})
}
func TestHealthNodeChecks(t *testing.T) {
@ -255,6 +301,69 @@ func TestHealthServiceChecks(t *testing.T) {
}
}
func TestHealthServiceChecks_NodeMetaFilter(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
req, err := http.NewRequest("GET", "/v1/health/checks/consul?dc=dc1&node-meta=somekey:somevalue", 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",
Node: srv.agent.config.NodeName,
Address: "127.0.0.1",
NodeMeta: map[string]string{"somekey": "somevalue"},
Check: &structs.HealthCheck{
Node: srv.agent.config.NodeName,
Name: "consul check",
ServiceID: "consul",
},
}
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/checks/consul?dc=dc1&node-meta=somekey:somevalue", 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 1 health check for consul
nodes = obj.(structs.HealthChecks)
if len(nodes) != 1 {
t.Fatalf("bad: %v", obj)
}
}
func TestHealthServiceChecks_DistanceSort(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
@ -320,25 +429,29 @@ func TestHealthServiceChecks_DistanceSort(t *testing.T) {
if err := srv.agent.RPC("Coordinate.Update", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
time.Sleep(300 * time.Millisecond)
// Query again and now foo should have moved to the front of the line.
resp = httptest.NewRecorder()
obj, err = srv.HealthServiceChecks(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.HealthChecks)
if len(nodes) != 2 {
t.Fatalf("bad: %v", obj)
}
if nodes[0].Node != "foo" {
t.Fatalf("bad: %v", nodes)
}
if nodes[1].Node != "bar" {
t.Fatalf("bad: %v", nodes)
}
// Retry until foo has moved to the front of the line.
testutil.WaitForResult(func() (bool, error) {
resp = httptest.NewRecorder()
obj, err = srv.HealthServiceChecks(resp, req)
if err != nil {
return false, fmt.Errorf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.HealthChecks)
if len(nodes) != 2 {
return false, fmt.Errorf("bad: %v", obj)
}
if nodes[0].Node != "foo" {
return false, fmt.Errorf("bad: %v", nodes)
}
if nodes[1].Node != "bar" {
return false, fmt.Errorf("bad: %v", nodes)
}
return true, nil
}, func(err error) {
t.Fatalf("failed to get sorted service checks: %v", err)
})
}
func TestHealthServiceNodes(t *testing.T) {
@ -422,6 +535,69 @@ func TestHealthServiceNodes(t *testing.T) {
}
}
func TestHealthServiceNodes_NodeMetaFilter(t *testing.T) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
defer srv.Shutdown()
defer srv.agent.Shutdown()
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
req, err := http.NewRequest("GET", "/v1/health/service/consul?dc=dc1&node-meta=somekey:somevalue", 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",
NodeMeta: map[string]string{"somekey": "somevalue"},
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&node-meta=somekey:somevalue", 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) {
dir, srv := makeHTTPServer(t)
defer os.RemoveAll(dir)
@ -487,25 +663,29 @@ func TestHealthServiceNodes_DistanceSort(t *testing.T) {
if err := srv.agent.RPC("Coordinate.Update", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
time.Sleep(300 * time.Millisecond)
// Query again and now foo should have moved to the front of the line.
resp = httptest.NewRecorder()
obj, err = srv.HealthServiceNodes(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.CheckServiceNodes)
if len(nodes) != 2 {
t.Fatalf("bad: %v", obj)
}
if nodes[0].Node.Node != "foo" {
t.Fatalf("bad: %v", nodes)
}
if nodes[1].Node.Node != "bar" {
t.Fatalf("bad: %v", nodes)
}
// Retry until foo has moved to the front of the line.
testutil.WaitForResult(func() (bool, error) {
resp = httptest.NewRecorder()
obj, err = srv.HealthServiceNodes(resp, req)
if err != nil {
return false, fmt.Errorf("err: %v", err)
}
assertIndex(t, resp)
nodes = obj.(structs.CheckServiceNodes)
if len(nodes) != 2 {
return false, fmt.Errorf("bad: %v", obj)
}
if nodes[0].Node.Node != "foo" {
return false, fmt.Errorf("bad: %v", nodes)
}
if nodes[1].Node.Node != "bar" {
return false, fmt.Errorf("bad: %v", nodes)
}
return true, nil
}, func(err error) {
t.Fatalf("failed to get sorted service nodes: %v", err)
})
}
func TestHealthServiceNodes_PassingFilter(t *testing.T) {

View File

@ -62,7 +62,9 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS
CertFile: config.CertFile,
KeyFile: config.KeyFile,
NodeName: config.NodeName,
ServerName: config.ServerName}
ServerName: config.ServerName,
TLSMinVersion: config.TLSMinVersion,
}
tlsConfig, err := tlsConf.IncomingTLSConfig()
if err != nil {
@ -231,64 +233,7 @@ func (s *HTTPServer) handleFuncMetrics(pattern string, handler func(http.Respons
func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/", s.Index)
s.handleFuncMetrics("/v1/status/leader", s.wrap(s.StatusLeader))
s.handleFuncMetrics("/v1/status/peers", s.wrap(s.StatusPeers))
s.handleFuncMetrics("/v1/operator/raft/configuration", s.wrap(s.OperatorRaftConfiguration))
s.handleFuncMetrics("/v1/operator/raft/peer", s.wrap(s.OperatorRaftPeer))
s.handleFuncMetrics("/v1/catalog/register", s.wrap(s.CatalogRegister))
s.handleFuncMetrics("/v1/catalog/deregister", s.wrap(s.CatalogDeregister))
s.handleFuncMetrics("/v1/catalog/datacenters", s.wrap(s.CatalogDatacenters))
s.handleFuncMetrics("/v1/catalog/nodes", s.wrap(s.CatalogNodes))
s.handleFuncMetrics("/v1/catalog/services", s.wrap(s.CatalogServices))
s.handleFuncMetrics("/v1/catalog/service/", s.wrap(s.CatalogServiceNodes))
s.handleFuncMetrics("/v1/catalog/node/", s.wrap(s.CatalogNodeServices))
if !s.agent.config.DisableCoordinates {
s.handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(s.CoordinateDatacenters))
s.handleFuncMetrics("/v1/coordinate/nodes", s.wrap(s.CoordinateNodes))
} else {
s.handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(coordinateDisabled))
s.handleFuncMetrics("/v1/coordinate/nodes", s.wrap(coordinateDisabled))
}
s.handleFuncMetrics("/v1/health/node/", s.wrap(s.HealthNodeChecks))
s.handleFuncMetrics("/v1/health/checks/", s.wrap(s.HealthServiceChecks))
s.handleFuncMetrics("/v1/health/state/", s.wrap(s.HealthChecksInState))
s.handleFuncMetrics("/v1/health/service/", s.wrap(s.HealthServiceNodes))
s.handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf))
s.handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance))
s.handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices))
s.handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks))
s.handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers))
s.handleFuncMetrics("/v1/agent/join/", s.wrap(s.AgentJoin))
s.handleFuncMetrics("/v1/agent/force-leave/", s.wrap(s.AgentForceLeave))
s.handleFuncMetrics("/v1/agent/check/register", s.wrap(s.AgentRegisterCheck))
s.handleFuncMetrics("/v1/agent/check/deregister/", s.wrap(s.AgentDeregisterCheck))
s.handleFuncMetrics("/v1/agent/check/pass/", s.wrap(s.AgentCheckPass))
s.handleFuncMetrics("/v1/agent/check/warn/", s.wrap(s.AgentCheckWarn))
s.handleFuncMetrics("/v1/agent/check/fail/", s.wrap(s.AgentCheckFail))
s.handleFuncMetrics("/v1/agent/check/update/", s.wrap(s.AgentCheckUpdate))
s.handleFuncMetrics("/v1/agent/service/register", s.wrap(s.AgentRegisterService))
s.handleFuncMetrics("/v1/agent/service/deregister/", s.wrap(s.AgentDeregisterService))
s.handleFuncMetrics("/v1/agent/service/maintenance/", s.wrap(s.AgentServiceMaintenance))
s.handleFuncMetrics("/v1/event/fire/", s.wrap(s.EventFire))
s.handleFuncMetrics("/v1/event/list", s.wrap(s.EventList))
s.handleFuncMetrics("/v1/kv/", s.wrap(s.KVSEndpoint))
s.handleFuncMetrics("/v1/session/create", s.wrap(s.SessionCreate))
s.handleFuncMetrics("/v1/session/destroy/", s.wrap(s.SessionDestroy))
s.handleFuncMetrics("/v1/session/renew/", s.wrap(s.SessionRenew))
s.handleFuncMetrics("/v1/session/info/", s.wrap(s.SessionGet))
s.handleFuncMetrics("/v1/session/node/", s.wrap(s.SessionsForNode))
s.handleFuncMetrics("/v1/session/list", s.wrap(s.SessionList))
// API V1.
if s.agent.config.ACLDatacenter != "" {
s.handleFuncMetrics("/v1/acl/create", s.wrap(s.ACLCreate))
s.handleFuncMetrics("/v1/acl/update", s.wrap(s.ACLUpdate))
@ -298,20 +243,74 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.handleFuncMetrics("/v1/acl/list", s.wrap(s.ACLList))
s.handleFuncMetrics("/v1/acl/replication", s.wrap(s.ACLReplicationStatus))
} else {
s.handleFuncMetrics("/v1/acl/create", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/update", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/info/", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/clone/", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/list", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/replication", s.wrap(aclDisabled))
s.handleFuncMetrics("/v1/acl/create", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/update", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/info/", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/clone/", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/list", s.wrap(ACLDisabled))
s.handleFuncMetrics("/v1/acl/replication", s.wrap(ACLDisabled))
}
s.handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf))
s.handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance))
s.handleFuncMetrics("/v1/agent/reload", s.wrap(s.AgentReload))
s.handleFuncMetrics("/v1/agent/monitor", s.wrap(s.AgentMonitor))
s.handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices))
s.handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks))
s.handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers))
s.handleFuncMetrics("/v1/agent/join/", s.wrap(s.AgentJoin))
s.handleFuncMetrics("/v1/agent/leave", s.wrap(s.AgentLeave))
s.handleFuncMetrics("/v1/agent/force-leave/", s.wrap(s.AgentForceLeave))
s.handleFuncMetrics("/v1/agent/check/register", s.wrap(s.AgentRegisterCheck))
s.handleFuncMetrics("/v1/agent/check/deregister/", s.wrap(s.AgentDeregisterCheck))
s.handleFuncMetrics("/v1/agent/check/pass/", s.wrap(s.AgentCheckPass))
s.handleFuncMetrics("/v1/agent/check/warn/", s.wrap(s.AgentCheckWarn))
s.handleFuncMetrics("/v1/agent/check/fail/", s.wrap(s.AgentCheckFail))
s.handleFuncMetrics("/v1/agent/check/update/", s.wrap(s.AgentCheckUpdate))
s.handleFuncMetrics("/v1/agent/service/register", s.wrap(s.AgentRegisterService))
s.handleFuncMetrics("/v1/agent/service/deregister/", s.wrap(s.AgentDeregisterService))
s.handleFuncMetrics("/v1/agent/service/maintenance/", s.wrap(s.AgentServiceMaintenance))
s.handleFuncMetrics("/v1/catalog/register", s.wrap(s.CatalogRegister))
s.handleFuncMetrics("/v1/catalog/deregister", s.wrap(s.CatalogDeregister))
s.handleFuncMetrics("/v1/catalog/datacenters", s.wrap(s.CatalogDatacenters))
s.handleFuncMetrics("/v1/catalog/nodes", s.wrap(s.CatalogNodes))
s.handleFuncMetrics("/v1/catalog/services", s.wrap(s.CatalogServices))
s.handleFuncMetrics("/v1/catalog/service/", s.wrap(s.CatalogServiceNodes))
s.handleFuncMetrics("/v1/catalog/node/", s.wrap(s.CatalogNodeServices))
if !s.agent.config.DisableCoordinates {
s.handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(s.CoordinateDatacenters))
s.handleFuncMetrics("/v1/coordinate/nodes", s.wrap(s.CoordinateNodes))
} else {
s.handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(coordinateDisabled))
s.handleFuncMetrics("/v1/coordinate/nodes", s.wrap(coordinateDisabled))
}
s.handleFuncMetrics("/v1/event/fire/", s.wrap(s.EventFire))
s.handleFuncMetrics("/v1/event/list", s.wrap(s.EventList))
s.handleFuncMetrics("/v1/health/node/", s.wrap(s.HealthNodeChecks))
s.handleFuncMetrics("/v1/health/checks/", s.wrap(s.HealthServiceChecks))
s.handleFuncMetrics("/v1/health/state/", s.wrap(s.HealthChecksInState))
s.handleFuncMetrics("/v1/health/service/", s.wrap(s.HealthServiceNodes))
s.handleFuncMetrics("/v1/internal/ui/nodes", s.wrap(s.UINodes))
s.handleFuncMetrics("/v1/internal/ui/node/", s.wrap(s.UINodeInfo))
s.handleFuncMetrics("/v1/internal/ui/services", s.wrap(s.UIServices))
s.handleFuncMetrics("/v1/kv/", s.wrap(s.KVSEndpoint))
s.handleFuncMetrics("/v1/operator/raft/configuration", s.wrap(s.OperatorRaftConfiguration))
s.handleFuncMetrics("/v1/operator/raft/peer", s.wrap(s.OperatorRaftPeer))
s.handleFuncMetrics("/v1/operator/keyring", s.wrap(s.OperatorKeyringEndpoint))
s.handleFuncMetrics("/v1/query", s.wrap(s.PreparedQueryGeneral))
s.handleFuncMetrics("/v1/query/", s.wrap(s.PreparedQuerySpecific))
s.handleFuncMetrics("/v1/session/create", s.wrap(s.SessionCreate))
s.handleFuncMetrics("/v1/session/destroy/", s.wrap(s.SessionDestroy))
s.handleFuncMetrics("/v1/session/renew/", s.wrap(s.SessionRenew))
s.handleFuncMetrics("/v1/session/info/", s.wrap(s.SessionGet))
s.handleFuncMetrics("/v1/session/node/", s.wrap(s.SessionsForNode))
s.handleFuncMetrics("/v1/session/list", s.wrap(s.SessionList))
s.handleFuncMetrics("/v1/status/leader", s.wrap(s.StatusLeader))
s.handleFuncMetrics("/v1/status/peers", s.wrap(s.StatusPeers))
s.handleFuncMetrics("/v1/snapshot", s.wrap(s.Snapshot))
s.handleFuncMetrics("/v1/txn", s.wrap(s.Txn))
// Debug endpoints.
if enableDebug {
s.handleFuncMetrics("/debug/pprof/", pprof.Index)
s.handleFuncMetrics("/debug/pprof/cmdline", pprof.Cmdline)
@ -326,10 +325,6 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(assetFS())))
}
// API's are under /internal/ui/ to avoid conflict
s.handleFuncMetrics("/v1/internal/ui/nodes", s.wrap(s.UINodes))
s.handleFuncMetrics("/v1/internal/ui/node/", s.wrap(s.UINodeInfo))
s.handleFuncMetrics("/v1/internal/ui/services", s.wrap(s.UIServices))
}
// wrap is used to wrap functions to make them more convenient
@ -404,7 +399,7 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
// marshalJSON marshals the object into JSON, respecting the user's pretty-ness
// configuration.
func (s *HTTPServer) marshalJSON(req *http.Request, obj interface{}) ([]byte, error) {
if _, ok := req.URL.Query()["pretty"]; ok {
if _, ok := req.URL.Query()["pretty"]; ok || s.agent.config.DevMode {
buf, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return nil, err
@ -591,6 +586,20 @@ func (s *HTTPServer) parseSource(req *http.Request, source *structs.QuerySource)
}
}
// parseMetaFilter is used to parse the ?node-meta=key:value query parameter, used for
// filtering results to nodes with the given metadata key/value
func (s *HTTPServer) parseMetaFilter(req *http.Request) map[string]string {
if filterList, ok := req.URL.Query()["node-meta"]; ok {
filters := make(map[string]string)
for _, filter := range filterList {
key, value := parseMetaPair(filter)
filters[key] = value
}
return filters
}
return nil
}
// parse is a convenience method for endpoints that need
// to use both parseWait and parseDC.
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {

View File

@ -19,6 +19,7 @@ import (
"time"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/go-cleanhttp"
)
@ -28,6 +29,26 @@ func makeHTTPServer(t *testing.T) (string, *HTTPServer) {
}
func makeHTTPServerWithConfig(t *testing.T, cb func(c *Config)) (string, *HTTPServer) {
return makeHTTPServerWithConfigLog(t, cb, nil, nil)
}
func makeHTTPServerWithACLs(t *testing.T) (string, *HTTPServer) {
dir, srv := makeHTTPServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = c.Datacenter
c.ACLDefaultPolicy = "deny"
c.ACLMasterToken = "root"
c.ACLAgentToken = "root"
c.ACLAgentMasterToken = "towel"
c.ACLEnforceVersion8 = Bool(true)
})
// Need a leader to look up ACLs, so wait here so we don't need to
// repeat this in each test.
testutil.WaitForLeader(t, srv.agent.RPC, "dc1")
return dir, srv
}
func makeHTTPServerWithConfigLog(t *testing.T, cb func(c *Config), l io.Writer, logWriter *logger.LogWriter) (string, *HTTPServer) {
configTry := 0
RECONF:
configTry += 1
@ -36,7 +57,7 @@ RECONF:
cb(conf)
}
dir, agent := makeAgent(t, conf)
dir, agent := makeAgentLog(t, conf, l, logWriter)
servers, err := NewHTTPServers(agent, conf, agent.logOutput)
if err != nil {
if configTry < 3 {

View File

@ -121,31 +121,44 @@ func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringRe
return &reply, nil
}
// ParseRelayFactor validates and converts the given relay factor to uint8
func ParseRelayFactor(n int) (uint8, error) {
if n < 0 || n > 5 {
return 0, fmt.Errorf("Relay factor must be in range: [0, 5]")
}
return uint8(n), nil
}
// ListKeys lists out all keys installed on the collective Consul cluster. This
// includes both servers and clients in all DC's.
func (a *Agent) ListKeys(token string) (*structs.KeyringResponses, error) {
func (a *Agent) ListKeys(token string, relayFactor uint8) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Operation: structs.KeyringList}
args.Token = token
parseKeyringRequest(&args, token, relayFactor)
return a.keyringProcess(&args)
}
// InstallKey installs a new gossip encryption key
func (a *Agent) InstallKey(key, token string) (*structs.KeyringResponses, error) {
func (a *Agent) InstallKey(key, token string, relayFactor uint8) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringInstall}
args.Token = token
parseKeyringRequest(&args, token, relayFactor)
return a.keyringProcess(&args)
}
// UseKey changes the primary encryption key used to encrypt messages
func (a *Agent) UseKey(key, token string) (*structs.KeyringResponses, error) {
func (a *Agent) UseKey(key, token string, relayFactor uint8) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringUse}
args.Token = token
parseKeyringRequest(&args, token, relayFactor)
return a.keyringProcess(&args)
}
// RemoveKey will remove a gossip encryption key from the keyring
func (a *Agent) RemoveKey(key, token string) (*structs.KeyringResponses, error) {
func (a *Agent) RemoveKey(key, token string, relayFactor uint8) (*structs.KeyringResponses, error) {
args := structs.KeyringRequest{Key: key, Operation: structs.KeyringRemove}
args.Token = token
parseKeyringRequest(&args, token, relayFactor)
return a.keyringProcess(&args)
}
func parseKeyringRequest(req *structs.KeyringRequest, token string, relayFactor uint8) {
req.Token = token
req.RelayFactor = relayFactor
}

View File

@ -132,49 +132,49 @@ func TestAgentKeyring_ACL(t *testing.T) {
testutil.WaitForLeader(t, agent.RPC, "dc1")
// List keys without access fails
_, err := agent.ListKeys("")
_, err := agent.ListKeys("", 0)
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// List keys with access works
_, err = agent.ListKeys("root")
_, err = agent.ListKeys("root", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
// Install without access fails
_, err = agent.InstallKey(key2, "")
_, err = agent.InstallKey(key2, "", 0)
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Install with access works
_, err = agent.InstallKey(key2, "root")
_, err = agent.InstallKey(key2, "root", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
// Use without access fails
_, err = agent.UseKey(key2, "")
_, err = agent.UseKey(key2, "", 0)
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Use with access works
_, err = agent.UseKey(key2, "root")
_, err = agent.UseKey(key2, "root", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
// Remove without access fails
_, err = agent.RemoveKey(key1, "")
_, err = agent.RemoveKey(key1, "", 0)
if err == nil || !strings.Contains(err.Error(), "denied") {
t.Fatalf("expected denied error, got: %#v", err)
}
// Remove with access works
_, err = agent.RemoveKey(key1, "root")
_, err = agent.RemoveKey(key1, "root", 0)
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -18,9 +18,6 @@ import (
const (
syncStaggerIntv = 3 * time.Second
syncRetryIntv = 15 * time.Second
// permissionDenied is returned when an ACL based rejection happens
permissionDenied = "Permission denied"
)
// syncStatus is used to represent the difference between
@ -47,7 +44,7 @@ type localState struct {
iface consul.Interface
// nodeInfoInSync tracks whether the server has our correct top-level
// node information in sync (currently only used for tagged addresses)
// node information in sync
nodeInfoInSync bool
// Services tracks the local services
@ -64,6 +61,9 @@ type localState struct {
// Used to track checks that are being deferred
deferCheck map[types.CheckID]*time.Timer
// metadata tracks the local metadata fields
metadata map[string]string
// consulCh is used to inform of a change to the known
// consul nodes. This may be used to retry a sync run
consulCh chan struct{}
@ -85,6 +85,7 @@ func (l *localState) Init(config *Config, logger *log.Logger) {
l.checkTokens = make(map[types.CheckID]string)
l.checkCriticalTime = make(map[types.CheckID]time.Time)
l.deferCheck = make(map[types.CheckID]*time.Timer)
l.metadata = make(map[string]string)
l.consulCh = make(chan struct{}, 1)
l.triggerCh = make(chan struct{}, 1)
}
@ -170,14 +171,20 @@ func (l *localState) AddService(service *structs.NodeService, token string) {
// RemoveService is used to remove a service entry from the local state.
// The agent will make a best effort to ensure it is deregistered
func (l *localState) RemoveService(serviceID string) {
func (l *localState) RemoveService(serviceID string) error {
l.Lock()
defer l.Unlock()
delete(l.services, serviceID)
delete(l.serviceTokens, serviceID)
l.serviceStatus[serviceID] = syncStatus{inSync: false}
l.changeMade()
if _, ok := l.services[serviceID]; ok {
delete(l.services, serviceID)
delete(l.serviceTokens, serviceID)
l.serviceStatus[serviceID] = syncStatus{inSync: false}
l.changeMade()
} else {
return fmt.Errorf("Service does not exist")
}
return nil
}
// Services returns the locally registered services that the
@ -336,6 +343,19 @@ func (l *localState) CriticalChecks() map[types.CheckID]CriticalCheck {
return checks
}
// Metadata returns the local node metadata fields that the
// agent is aware of and are being kept in sync with the server
func (l *localState) Metadata() map[string]string {
metadata := make(map[string]string)
l.RLock()
defer l.RUnlock()
for key, value := range l.metadata {
metadata[key] = value
}
return metadata
}
// antiEntropy is a long running method used to perform anti-entropy
// between local and remote state.
func (l *localState) antiEntropy(shutdownCh chan struct{}) {
@ -394,7 +414,7 @@ func (l *localState) setSyncState() error {
req := structs.NodeSpecificRequest{
Datacenter: l.config.Datacenter,
Node: l.config.NodeName,
QueryOptions: structs.QueryOptions{Token: l.config.ACLToken},
QueryOptions: structs.QueryOptions{Token: l.config.GetTokenForAgent()},
}
var out1 structs.IndexedNodeServices
var out2 structs.IndexedHealthChecks
@ -409,10 +429,11 @@ func (l *localState) setSyncState() error {
l.Lock()
defer l.Unlock()
// Check the node info (currently limited to tagged addresses since
// everything else is managed by the Serf layer)
// Check the node info
if out1.NodeServices == nil || out1.NodeServices.Node == nil ||
!reflect.DeepEqual(out1.NodeServices.Node.TaggedAddresses, l.config.TaggedAddresses) {
out1.NodeServices.Node.ID != l.config.NodeID ||
!reflect.DeepEqual(out1.NodeServices.Node.TaggedAddresses, l.config.TaggedAddresses) ||
!reflect.DeepEqual(out1.NodeServices.Node.Meta, l.metadata) {
l.nodeInfoInSync = false
}
@ -613,9 +634,11 @@ func (l *localState) deleteCheck(id types.CheckID) error {
func (l *localState) syncService(id string) error {
req := structs.RegisterRequest{
Datacenter: l.config.Datacenter,
ID: l.config.NodeID,
Node: l.config.NodeName,
Address: l.config.AdvertiseAddr,
TaggedAddresses: l.config.TaggedAddresses,
NodeMeta: l.metadata,
Service: l.services[id],
WriteRequest: structs.WriteRequest{Token: l.serviceToken(id)},
}
@ -674,9 +697,11 @@ func (l *localState) syncCheck(id types.CheckID) error {
req := structs.RegisterRequest{
Datacenter: l.config.Datacenter,
ID: l.config.NodeID,
Node: l.config.NodeName,
Address: l.config.AdvertiseAddr,
TaggedAddresses: l.config.TaggedAddresses,
NodeMeta: l.metadata,
Service: service,
Check: l.checks[id],
WriteRequest: structs.WriteRequest{Token: l.checkToken(id)},
@ -700,10 +725,12 @@ func (l *localState) syncCheck(id types.CheckID) error {
func (l *localState) syncNodeInfo() error {
req := structs.RegisterRequest{
Datacenter: l.config.Datacenter,
ID: l.config.NodeID,
Node: l.config.NodeName,
Address: l.config.AdvertiseAddr,
TaggedAddresses: l.config.TaggedAddresses,
WriteRequest: structs.WriteRequest{Token: l.config.ACLToken},
NodeMeta: l.metadata,
WriteRequest: structs.WriteRequest{Token: l.config.GetTokenForAgent()},
}
var out struct{}
err := l.iface.RPC("Catalog.Register", &req, &out)

View File

@ -89,6 +89,14 @@ func TestAgentAntiEntropy_Services(t *testing.T) {
}
agent.state.AddService(srv5, "")
srv5_mod := new(structs.NodeService)
*srv5_mod = *srv5
srv5_mod.Address = "127.0.0.1"
args.Service = srv5_mod
if err := agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
// Exists local, in sync, remote missing (create)
srv6 := &structs.NodeService{
ID: "cache",
@ -99,139 +107,148 @@ func TestAgentAntiEntropy_Services(t *testing.T) {
agent.state.AddService(srv6, "")
agent.state.serviceStatus["cache"] = syncStatus{inSync: true}
srv5_mod := new(structs.NodeService)
*srv5_mod = *srv5
srv5_mod.Address = "127.0.0.1"
args.Service = srv5_mod
if err := agent.RPC("Catalog.Register", args, &out); err != nil {
t.Fatalf("err: %v", err)
}
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
var services structs.IndexedNodeServices
req := structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: agent.config.NodeName,
}
var services structs.IndexedNodeServices
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
t.Fatalf("err: %v", err)
}
// Make sure we sent along our tagged addresses when we synced.
addrs := services.NodeServices.Node.TaggedAddresses
if len(addrs) == 0 || !reflect.DeepEqual(addrs, conf.TaggedAddresses) {
t.Fatalf("bad: %v", addrs)
}
// We should have 6 services (consul included)
if len(services.NodeServices.Services) != 6 {
t.Fatalf("bad: %v", services.NodeServices.Services)
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "mysql":
if !reflect.DeepEqual(serv, srv1) {
t.Fatalf("bad: %v %v", serv, srv1)
}
case "redis":
if !reflect.DeepEqual(serv, srv2) {
t.Fatalf("bad: %#v %#v", serv, srv2)
}
case "web":
if !reflect.DeepEqual(serv, srv3) {
t.Fatalf("bad: %v %v", serv, srv3)
}
case "api":
if !reflect.DeepEqual(serv, srv5) {
t.Fatalf("bad: %v %v", serv, srv5)
}
case "cache":
if !reflect.DeepEqual(serv, srv6) {
t.Fatalf("bad: %v %v", serv, srv6)
}
case "consul":
// ignore
default:
t.Fatalf("unexpected service: %v", id)
verifyServices := func() (bool, error) {
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
return false, fmt.Errorf("err: %v", err)
}
// Make sure we sent along our node info when we synced.
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
if id != conf.NodeID ||
!reflect.DeepEqual(addrs, conf.TaggedAddresses) ||
!reflect.DeepEqual(meta, conf.Meta) {
return false, fmt.Errorf("bad: %v", services.NodeServices.Node)
}
// We should have 6 services (consul included)
if len(services.NodeServices.Services) != 6 {
return false, fmt.Errorf("bad: %v", services.NodeServices.Services)
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "mysql":
if !reflect.DeepEqual(serv, srv1) {
return false, fmt.Errorf("bad: %v %v", serv, srv1)
}
case "redis":
if !reflect.DeepEqual(serv, srv2) {
return false, fmt.Errorf("bad: %#v %#v", serv, srv2)
}
case "web":
if !reflect.DeepEqual(serv, srv3) {
return false, fmt.Errorf("bad: %v %v", serv, srv3)
}
case "api":
if !reflect.DeepEqual(serv, srv5) {
return false, fmt.Errorf("bad: %v %v", serv, srv5)
}
case "cache":
if !reflect.DeepEqual(serv, srv6) {
return false, fmt.Errorf("bad: %v %v", serv, srv6)
}
case "consul":
// ignore
default:
return false, fmt.Errorf("unexpected service: %v", id)
}
}
// Check the local state
if len(agent.state.services) != 6 {
return false, fmt.Errorf("bad: %v", agent.state.services)
}
if len(agent.state.serviceStatus) != 6 {
return false, fmt.Errorf("bad: %v", agent.state.serviceStatus)
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
return false, fmt.Errorf("should be in sync: %v %v", name, status)
}
}
return true, nil
}
// Check the local state
if len(agent.state.services) != 6 {
t.Fatalf("bad: %v", agent.state.services)
}
if len(agent.state.serviceStatus) != 6 {
t.Fatalf("bad: %v", agent.state.serviceStatus)
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
t.Fatalf("should be in sync: %v %v", name, status)
}
}
testutil.WaitForResult(verifyServices, func(err error) {
t.Fatal(err)
})
// Remove one of the services
agent.state.RemoveService("api")
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
t.Fatalf("err: %v", err)
}
// We should have 5 services (consul included)
if len(services.NodeServices.Services) != 5 {
t.Fatalf("bad: %v", services.NodeServices.Services)
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "mysql":
if !reflect.DeepEqual(serv, srv1) {
t.Fatalf("bad: %v %v", serv, srv1)
}
case "redis":
if !reflect.DeepEqual(serv, srv2) {
t.Fatalf("bad: %#v %#v", serv, srv2)
}
case "web":
if !reflect.DeepEqual(serv, srv3) {
t.Fatalf("bad: %v %v", serv, srv3)
}
case "cache":
if !reflect.DeepEqual(serv, srv6) {
t.Fatalf("bad: %v %v", serv, srv6)
}
case "consul":
// ignore
default:
t.Fatalf("unexpected service: %v", id)
verifyServicesAfterRemove := func() (bool, error) {
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
return false, fmt.Errorf("err: %v", err)
}
// We should have 5 services (consul included)
if len(services.NodeServices.Services) != 5 {
return false, fmt.Errorf("bad: %v", services.NodeServices.Services)
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "mysql":
if !reflect.DeepEqual(serv, srv1) {
return false, fmt.Errorf("bad: %v %v", serv, srv1)
}
case "redis":
if !reflect.DeepEqual(serv, srv2) {
return false, fmt.Errorf("bad: %#v %#v", serv, srv2)
}
case "web":
if !reflect.DeepEqual(serv, srv3) {
return false, fmt.Errorf("bad: %v %v", serv, srv3)
}
case "cache":
if !reflect.DeepEqual(serv, srv6) {
return false, fmt.Errorf("bad: %v %v", serv, srv6)
}
case "consul":
// ignore
default:
return false, fmt.Errorf("unexpected service: %v", id)
}
}
// Check the local state
if len(agent.state.services) != 5 {
return false, fmt.Errorf("bad: %v", agent.state.services)
}
if len(agent.state.serviceStatus) != 5 {
return false, fmt.Errorf("bad: %v", agent.state.serviceStatus)
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
return false, fmt.Errorf("should be in sync: %v %v", name, status)
}
}
return true, nil
}
// Check the local state
if len(agent.state.services) != 5 {
t.Fatalf("bad: %v", agent.state.services)
}
if len(agent.state.serviceStatus) != 5 {
t.Fatalf("bad: %v", agent.state.serviceStatus)
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
t.Fatalf("should be in sync: %v %v", name, status)
}
}
testutil.WaitForResult(verifyServicesAfterRemove, func(err error) {
t.Fatal(err)
})
}
func TestAgentAntiEntropy_EnableTagOverride(t *testing.T) {
@ -287,48 +304,55 @@ func TestAgentAntiEntropy_EnableTagOverride(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
req := structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: agent.config.NodeName,
}
var services structs.IndexedNodeServices
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
t.Fatalf("err: %v", err)
verifyServices := func() (bool, error) {
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
return false, fmt.Errorf("err: %v", err)
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "svc_id1":
if serv.ID != "svc_id1" ||
serv.Service != "svc1" ||
serv.Port != 6100 ||
!reflect.DeepEqual(serv.Tags, []string{"tag1_mod"}) {
return false, fmt.Errorf("bad: %v %v", serv, srv1)
}
case "svc_id2":
if serv.ID != "svc_id2" ||
serv.Service != "svc2" ||
serv.Port != 6200 ||
!reflect.DeepEqual(serv.Tags, []string{"tag2"}) {
return false, fmt.Errorf("bad: %v %v", serv, srv2)
}
case "consul":
// ignore
default:
return false, fmt.Errorf("unexpected service: %v", id)
}
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
return false, fmt.Errorf("should be in sync: %v %v", name, status)
}
}
return true, nil
}
// All the services should match
for id, serv := range services.NodeServices.Services {
serv.CreateIndex, serv.ModifyIndex = 0, 0
switch id {
case "svc_id1":
if serv.ID != "svc_id1" ||
serv.Service != "svc1" ||
serv.Port != 6100 ||
!reflect.DeepEqual(serv.Tags, []string{"tag1_mod"}) {
t.Fatalf("bad: %v %v", serv, srv1)
}
case "svc_id2":
if serv.ID != "svc_id2" ||
serv.Service != "svc2" ||
serv.Port != 6200 ||
!reflect.DeepEqual(serv.Tags, []string{"tag2"}) {
t.Fatalf("bad: %v %v", serv, srv2)
}
case "consul":
// ignore
default:
t.Fatalf("unexpected service: %v", id)
}
}
for name, status := range agent.state.serviceStatus {
if !status.inSync {
t.Fatalf("should be in sync: %v %v", name, status)
}
}
testutil.WaitForResult(verifyServices, func(err error) {
t.Fatal(err)
})
}
func TestAgentAntiEntropy_Services_WithChecks(t *testing.T) {
@ -635,49 +659,54 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
req := structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: agent.config.NodeName,
}
var checks structs.IndexedHealthChecks
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
t.Fatalf("err: %v", err)
}
// We should have 5 checks (serf included)
if len(checks.HealthChecks) != 5 {
t.Fatalf("bad: %v", checks)
}
// All the checks should match
for _, chk := range checks.HealthChecks {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
if !reflect.DeepEqual(chk, chk1) {
t.Fatalf("bad: %v %v", chk, chk1)
}
case "redis":
if !reflect.DeepEqual(chk, chk2) {
t.Fatalf("bad: %v %v", chk, chk2)
}
case "web":
if !reflect.DeepEqual(chk, chk3) {
t.Fatalf("bad: %v %v", chk, chk3)
}
case "cache":
if !reflect.DeepEqual(chk, chk5) {
t.Fatalf("bad: %v %v", chk, chk5)
}
case "serfHealth":
// ignore
default:
t.Fatalf("unexpected check: %v", chk)
// Verify that we are in sync
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
return false, fmt.Errorf("err: %v", err)
}
}
// We should have 5 checks (serf included)
if len(checks.HealthChecks) != 5 {
return false, fmt.Errorf("bad: %v", checks)
}
// All the checks should match
for _, chk := range checks.HealthChecks {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
if !reflect.DeepEqual(chk, chk1) {
return false, fmt.Errorf("bad: %v %v", chk, chk1)
}
case "redis":
if !reflect.DeepEqual(chk, chk2) {
return false, fmt.Errorf("bad: %v %v", chk, chk2)
}
case "web":
if !reflect.DeepEqual(chk, chk3) {
return false, fmt.Errorf("bad: %v %v", chk, chk3)
}
case "cache":
if !reflect.DeepEqual(chk, chk5) {
return false, fmt.Errorf("bad: %v %v", chk, chk5)
}
case "serfHealth":
// ignore
default:
return false, fmt.Errorf("unexpected check: %v", chk)
}
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Check the local state
if len(agent.state.checks) != 4 {
@ -692,7 +721,7 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
}
}
// Make sure we sent along our tagged addresses when we synced.
// Make sure we sent along our node info addresses when we synced.
{
req := structs.NodeSpecificRequest{
Datacenter: "dc1",
@ -703,9 +732,13 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
t.Fatalf("err: %v", err)
}
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
if len(addrs) == 0 || !reflect.DeepEqual(addrs, conf.TaggedAddresses) {
t.Fatalf("bad: %v", addrs)
meta := services.NodeServices.Node.Meta
if id != conf.NodeID ||
!reflect.DeepEqual(addrs, conf.TaggedAddresses) ||
!reflect.DeepEqual(meta, conf.Meta) {
t.Fatalf("bad: %v", services.NodeServices.Node)
}
}
@ -714,40 +747,44 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
t.Fatalf("err: %v", err)
}
// We should have 5 checks (serf included)
if len(checks.HealthChecks) != 4 {
t.Fatalf("bad: %v", checks)
}
// All the checks should match
for _, chk := range checks.HealthChecks {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
if !reflect.DeepEqual(chk, chk1) {
t.Fatalf("bad: %v %v", chk, chk1)
}
case "web":
if !reflect.DeepEqual(chk, chk3) {
t.Fatalf("bad: %v %v", chk, chk3)
}
case "cache":
if !reflect.DeepEqual(chk, chk5) {
t.Fatalf("bad: %v %v", chk, chk5)
}
case "serfHealth":
// ignore
default:
t.Fatalf("unexpected check: %v", chk)
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
return false, fmt.Errorf("err: %v", err)
}
}
// We should have 5 checks (serf included)
if len(checks.HealthChecks) != 4 {
return false, fmt.Errorf("bad: %v", checks)
}
// All the checks should match
for _, chk := range checks.HealthChecks {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
if !reflect.DeepEqual(chk, chk1) {
return false, fmt.Errorf("bad: %v %v", chk, chk1)
}
case "web":
if !reflect.DeepEqual(chk, chk3) {
return false, fmt.Errorf("bad: %v %v", chk, chk3)
}
case "cache":
if !reflect.DeepEqual(chk, chk5) {
return false, fmt.Errorf("bad: %v %v", chk, chk5)
}
case "serfHealth":
// ignore
default:
return false, fmt.Errorf("unexpected check: %v", chk)
}
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Check the local state
if len(agent.state.checks) != 3 {
@ -784,7 +821,6 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
req := structs.NodeSpecificRequest{
@ -792,14 +828,21 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) {
Node: agent.config.NodeName,
}
var checks structs.IndexedHealthChecks
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
t.Fatalf("err: %v", err)
}
// Verify checks in place
if len(checks.HealthChecks) != 2 {
t.Fatalf("checks: %v", check)
}
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil {
return false, fmt.Errorf("err: %v", err)
}
// Verify checks in place
if len(checks.HealthChecks) != 2 {
return false, fmt.Errorf("checks: %v", check)
}
return true, nil
}, func(err error) {
t.Fatal(err)
})
// Update the check output! Should be deferred
agent.state.UpdateCheck("web", structs.HealthPassing, "output")
@ -950,6 +993,8 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) {
func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
conf := nextConfig()
conf.NodeID = types.NodeID("40e4a748-2192-161a-0510-9bf59fe950b5")
conf.Meta["somekey"] = "somevalue"
dir, agent := makeAgent(t, conf)
defer os.RemoveAll(dir)
defer agent.Shutdown()
@ -969,24 +1014,33 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync
req := structs.NodeSpecificRequest{
Datacenter: "dc1",
Node: agent.config.NodeName,
}
var services structs.IndexedNodeServices
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
t.Fatalf("err: %v", err)
}
// Make sure we synced our node info - this should have ridden on the
// "consul" service sync
addrs := services.NodeServices.Node.TaggedAddresses
if len(addrs) == 0 || !reflect.DeepEqual(addrs, conf.TaggedAddresses) {
t.Fatalf("bad: %v", addrs)
}
// Wait for the sync
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
return false, fmt.Errorf("err: %v", err)
}
// Make sure we synced our node info - this should have ridden on the
// "consul" service sync
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
if id != conf.NodeID ||
!reflect.DeepEqual(addrs, conf.TaggedAddresses) ||
!reflect.DeepEqual(meta, conf.Meta) {
return false, fmt.Errorf("bad: %v", services.NodeServices.Node)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Blow away the catalog version of the node info
if err := agent.RPC("Catalog.Register", args, &out); err != nil {
@ -995,17 +1049,26 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
// Trigger anti-entropy run and wait
agent.StartSync()
time.Sleep(200 * time.Millisecond)
// Verify that we are in sync - this should have been a sync of just the
// Wait for the sync - this should have been a sync of just the
// node info
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
t.Fatalf("err: %v", err)
}
addrs = services.NodeServices.Node.TaggedAddresses
if len(addrs) == 0 || !reflect.DeepEqual(addrs, conf.TaggedAddresses) {
t.Fatalf("bad: %v", addrs)
}
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil {
return false, fmt.Errorf("err: %v", err)
}
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
if id != conf.NodeID ||
!reflect.DeepEqual(addrs, conf.TaggedAddresses) ||
!reflect.DeepEqual(meta, conf.Meta) {
return false, fmt.Errorf("bad: %v", services.NodeServices.Node)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
}
func TestAgentAntiEntropy_deleteService_fails(t *testing.T) {
@ -1028,6 +1091,10 @@ func TestAgent_serviceTokens(t *testing.T) {
l := new(localState)
l.Init(config, nil)
l.AddService(&structs.NodeService{
ID: "redis",
}, "")
// Returns default when no token is set
if token := l.ServiceToken("redis"); token != "default" {
t.Fatalf("bad: %s", token)
@ -1175,22 +1242,24 @@ func TestAgent_sendCoordinate(t *testing.T) {
testutil.WaitForLeader(t, agent.RPC, "dc1")
// Wait a little while for an update.
time.Sleep(3 * conf.ConsulConfig.CoordinateUpdatePeriod)
// Make sure the coordinate is present.
req := structs.DCSpecificRequest{
Datacenter: agent.config.Datacenter,
}
var reply structs.IndexedCoordinates
if err := agent.RPC("Coordinate.ListNodes", &req, &reply); err != nil {
testutil.WaitForResult(func() (bool, error) {
if err := agent.RPC("Coordinate.ListNodes", &req, &reply); err != nil {
return false, fmt.Errorf("err: %s", err)
}
if len(reply.Coordinates) != 1 {
return false, fmt.Errorf("expected a coordinate: %v", reply)
}
coord := reply.Coordinates[0]
if coord.Node != agent.config.NodeName || coord.Coord == nil {
return false, fmt.Errorf("bad: %v", coord)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
}
if len(reply.Coordinates) != 1 {
t.Fatalf("expected a coordinate: %v", reply)
}
coord := reply.Coordinates[0]
if coord.Node != agent.config.NodeName || coord.Coord == nil {
t.Fatalf("bad: %v", coord)
}
})
}

View File

@ -1,10 +1,13 @@
package agent
import (
"fmt"
"net/http"
"github.com/hashicorp/consul/consul/structs"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/raft"
"strconv"
)
// OperatorRaftConfiguration is used to inspect the current Raft configuration.
@ -55,3 +58,104 @@ func (s *HTTPServer) OperatorRaftPeer(resp http.ResponseWriter, req *http.Reques
}
return nil, nil
}
type keyringArgs struct {
Key string
Token string
RelayFactor uint8
}
// OperatorKeyringEndpoint handles keyring operations (install, list, use, remove)
func (s *HTTPServer) OperatorKeyringEndpoint(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var args keyringArgs
if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" {
if err := decodeBody(req, &args, nil); err != nil {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("Request decode failed: %v", err)))
return nil, nil
}
}
s.parseToken(req, &args.Token)
// Parse relay factor
if relayFactor := req.URL.Query().Get("relay-factor"); relayFactor != "" {
n, err := strconv.Atoi(relayFactor)
if err != nil {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("Error parsing relay factor: %v", err)))
return nil, nil
}
args.RelayFactor, err = ParseRelayFactor(n)
if err != nil {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf("Invalid relay factor: %v", err)))
return nil, nil
}
}
// Switch on the method
switch req.Method {
case "GET":
return s.KeyringList(resp, req, &args)
case "POST":
return s.KeyringInstall(resp, req, &args)
case "PUT":
return s.KeyringUse(resp, req, &args)
case "DELETE":
return s.KeyringRemove(resp, req, &args)
default:
resp.WriteHeader(405)
return nil, nil
}
}
// KeyringInstall is used to install a new gossip encryption key into the cluster
func (s *HTTPServer) KeyringInstall(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) {
responses, err := s.agent.InstallKey(args.Key, args.Token, args.RelayFactor)
if err != nil {
return nil, err
}
return nil, keyringErrorsOrNil(responses.Responses)
}
// KeyringList is used to list the keys installed in the cluster
func (s *HTTPServer) KeyringList(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) {
responses, err := s.agent.ListKeys(args.Token, args.RelayFactor)
if err != nil {
return nil, err
}
return responses.Responses, keyringErrorsOrNil(responses.Responses)
}
// KeyringRemove is used to list the keys installed in the cluster
func (s *HTTPServer) KeyringRemove(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) {
responses, err := s.agent.RemoveKey(args.Key, args.Token, args.RelayFactor)
if err != nil {
return nil, err
}
return nil, keyringErrorsOrNil(responses.Responses)
}
// KeyringUse is used to change the primary gossip encryption key
func (s *HTTPServer) KeyringUse(resp http.ResponseWriter, req *http.Request, args *keyringArgs) (interface{}, error) {
responses, err := s.agent.UseKey(args.Key, args.Token, args.RelayFactor)
if err != nil {
return nil, err
}
return nil, keyringErrorsOrNil(responses.Responses)
}
func keyringErrorsOrNil(responses []*structs.KeyringResponse) error {
var errs error
for _, response := range responses {
if response.Error != "" {
errs = multierror.Append(errs, fmt.Errorf(response.Error))
}
}
return errs
}

View File

@ -2,6 +2,7 @@ package agent
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@ -56,3 +57,231 @@ func TestOperator_OperatorRaftPeer(t *testing.T) {
}
})
}
func TestOperator_KeyringInstall(t *testing.T) {
oldKey := "H3/9gBxcKKRf45CaI2DlRg=="
newKey := "z90lFx3sZZLtTOkutXcwYg=="
configFunc := func(c *Config) {
c.EncryptKey = oldKey
}
httpTestWithConfig(t, func(srv *HTTPServer) {
body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey))
req, err := http.NewRequest("POST", "/v1/operator/keyring", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.OperatorKeyringEndpoint(resp, req)
if err != nil {
t.Fatalf("err: %s", err)
}
listResponse, err := srv.agent.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(listResponse.Responses) != 2 {
t.Fatalf("bad: %d", len(listResponse.Responses))
}
for _, response := range listResponse.Responses {
count, ok := response.Keys[newKey]
if !ok {
t.Fatalf("bad: %v", response.Keys)
}
if count != response.NumNodes {
t.Fatalf("bad: %d, %d", count, response.NumNodes)
}
}
}, configFunc)
}
func TestOperator_KeyringList(t *testing.T) {
key := "H3/9gBxcKKRf45CaI2DlRg=="
configFunc := func(c *Config) {
c.EncryptKey = key
}
httpTestWithConfig(t, func(srv *HTTPServer) {
req, err := http.NewRequest("GET", "/v1/operator/keyring", nil)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
r, err := srv.OperatorKeyringEndpoint(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
responses, ok := r.([]*structs.KeyringResponse)
if !ok {
t.Fatalf("err: %v", !ok)
}
// Check that we get both a LAN and WAN response, and that they both only
// contain the original key
if len(responses) != 2 {
t.Fatalf("bad: %d", len(responses))
}
// WAN
if len(responses[0].Keys) != 1 {
t.Fatalf("bad: %d", len(responses[0].Keys))
}
if !responses[0].WAN {
t.Fatalf("bad: %v", responses[0].WAN)
}
if _, ok := responses[0].Keys[key]; !ok {
t.Fatalf("bad: %v", ok)
}
// LAN
if len(responses[1].Keys) != 1 {
t.Fatalf("bad: %d", len(responses[1].Keys))
}
if responses[1].WAN {
t.Fatalf("bad: %v", responses[1].WAN)
}
if _, ok := responses[1].Keys[key]; !ok {
t.Fatalf("bad: %v", ok)
}
}, configFunc)
}
func TestOperator_KeyringRemove(t *testing.T) {
key := "H3/9gBxcKKRf45CaI2DlRg=="
tempKey := "z90lFx3sZZLtTOkutXcwYg=="
configFunc := func(c *Config) {
c.EncryptKey = key
}
httpTestWithConfig(t, func(srv *HTTPServer) {
_, err := srv.agent.InstallKey(tempKey, "", 0)
if err != nil {
t.Fatalf("err: %v", err)
}
// Make sure the temp key is installed
list, err := srv.agent.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %v", err)
}
responses := list.Responses
if len(responses) != 2 {
t.Fatalf("bad: %d", len(responses))
}
for _, response := range responses {
if len(response.Keys) != 2 {
t.Fatalf("bad: %d", len(response.Keys))
}
if _, ok := response.Keys[tempKey]; !ok {
t.Fatalf("bad: %v", ok)
}
}
body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", tempKey))
req, err := http.NewRequest("DELETE", "/v1/operator/keyring", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.OperatorKeyringEndpoint(resp, req)
if err != nil {
t.Fatalf("err: %s", err)
}
// Make sure the temp key has been removed
list, err = srv.agent.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %v", err)
}
responses = list.Responses
if len(responses) != 2 {
t.Fatalf("bad: %d", len(responses))
}
for _, response := range responses {
if len(response.Keys) != 1 {
t.Fatalf("bad: %d", len(response.Keys))
}
if _, ok := response.Keys[tempKey]; ok {
t.Fatalf("bad: %v", ok)
}
}
}, configFunc)
}
func TestOperator_KeyringUse(t *testing.T) {
oldKey := "H3/9gBxcKKRf45CaI2DlRg=="
newKey := "z90lFx3sZZLtTOkutXcwYg=="
configFunc := func(c *Config) {
c.EncryptKey = oldKey
}
httpTestWithConfig(t, func(srv *HTTPServer) {
if _, err := srv.agent.InstallKey(newKey, "", 0); err != nil {
t.Fatalf("err: %v", err)
}
body := bytes.NewBufferString(fmt.Sprintf("{\"Key\":\"%s\"}", newKey))
req, err := http.NewRequest("PUT", "/v1/operator/keyring", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.OperatorKeyringEndpoint(resp, req)
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := srv.agent.RemoveKey(oldKey, "", 0); err != nil {
t.Fatalf("err: %v", err)
}
// Make sure only the new key remains
list, err := srv.agent.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %v", err)
}
responses := list.Responses
if len(responses) != 2 {
t.Fatalf("bad: %d", len(responses))
}
for _, response := range responses {
if len(response.Keys) != 1 {
t.Fatalf("bad: %d", len(response.Keys))
}
if _, ok := response.Keys[newKey]; !ok {
t.Fatalf("bad: %v", ok)
}
}
}, configFunc)
}
func TestOperator_Keyring_InvalidRelayFactor(t *testing.T) {
key := "H3/9gBxcKKRf45CaI2DlRg=="
configFunc := func(c *Config) {
c.EncryptKey = key
}
httpTestWithConfig(t, func(srv *HTTPServer) {
cases := map[string]string{
"999": "Relay factor must be in range",
"asdf": "Error parsing relay factor",
}
for relayFactor, errString := range cases {
req, err := http.NewRequest("GET", "/v1/operator/keyring?relay-factor="+relayFactor, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.OperatorKeyringEndpoint(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
body := resp.Body.String()
if !strings.Contains(body, errString) {
t.Fatalf("bad: %v", body)
}
}
}, configFunc)
}

View File

@ -90,6 +90,7 @@ func TestPreparedQuery_Create(t *testing.T) {
},
OnlyPassing: true,
Tags: []string{"foo", "bar"},
NodeMeta: map[string]string{"somekey": "somevalue"},
},
DNS: structs.QueryDNSOptions{
TTL: "10s",
@ -120,6 +121,7 @@ func TestPreparedQuery_Create(t *testing.T) {
},
"OnlyPassing": true,
"Tags": []string{"foo", "bar"},
"NodeMeta": map[string]string{"somekey": "somevalue"},
},
"DNS": map[string]interface{}{
"TTL": "10s",
@ -645,6 +647,7 @@ func TestPreparedQuery_Update(t *testing.T) {
},
OnlyPassing: true,
Tags: []string{"foo", "bar"},
NodeMeta: map[string]string{"somekey": "somevalue"},
},
DNS: structs.QueryDNSOptions{
TTL: "10s",
@ -676,6 +679,7 @@ func TestPreparedQuery_Update(t *testing.T) {
},
"OnlyPassing": true,
"Tags": []string{"foo", "bar"},
"NodeMeta": map[string]string{"somekey": "somevalue"},
},
"DNS": map[string]interface{}{
"TTL": "10s",

View File

@ -32,6 +32,7 @@ import (
"sync"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/logutils"
"github.com/hashicorp/serf/serf"
@ -105,7 +106,8 @@ type joinResponse struct {
}
type keyringRequest struct {
Key string
Key string
RelayFactor uint8
}
type KeyringEntry struct {
@ -171,7 +173,7 @@ type AgentRPC struct {
clients map[string]*rpcClient
listener net.Listener
logger *log.Logger
logWriter *logWriter
logWriter *logger.LogWriter
reloadCh chan struct{}
stop bool
stopCh chan struct{}
@ -218,7 +220,7 @@ func (c *rpcClient) String() string {
// NewAgentRPC is used to create a new Agent RPC handler
func NewAgentRPC(agent *Agent, listener net.Listener,
logOutput io.Writer, logWriter *logWriter) *AgentRPC {
logOutput io.Writer, logWriter *logger.LogWriter) *AgentRPC {
if logOutput == nil {
logOutput = os.Stderr
}
@ -513,9 +515,9 @@ func (i *AgentRPC) handleMonitor(client *rpcClient, seq uint64) error {
req.LogLevel = strings.ToUpper(req.LogLevel)
// Create a level filter
filter := LevelFilter()
filter := logger.LevelFilter()
filter.MinLevel = logutils.LogLevel(req.LogLevel)
if !ValidateLevelFilter(filter.MinLevel, filter) {
if !logger.ValidateLevelFilter(filter.MinLevel, filter) {
resp.Error = fmt.Sprintf("Unknown log level: %s", filter.MinLevel)
goto SEND
}
@ -603,21 +605,21 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd, token strin
var r keyringResponse
var err error
if cmd != listKeysCommand {
if err = client.dec.Decode(&req); err != nil {
return fmt.Errorf("decode failed: %v", err)
}
if err = client.dec.Decode(&req); err != nil {
return fmt.Errorf("decode failed: %v", err)
}
i.agent.logger.Printf("[INFO] agent: Sending rpc command with relay factor %d", req.RelayFactor)
switch cmd {
case listKeysCommand:
queryResp, err = i.agent.ListKeys(token)
queryResp, err = i.agent.ListKeys(token, req.RelayFactor)
case installKeyCommand:
queryResp, err = i.agent.InstallKey(req.Key, token)
queryResp, err = i.agent.InstallKey(req.Key, token, req.RelayFactor)
case useKeyCommand:
queryResp, err = i.agent.UseKey(req.Key, token)
queryResp, err = i.agent.UseKey(req.Key, token, req.RelayFactor)
case removeKeyCommand:
queryResp, err = i.agent.RemoveKey(req.Key, token)
queryResp, err = i.agent.RemoveKey(req.Key, token, req.RelayFactor)
default:
respHeader := responseHeader{Seq: seq, Error: unsupportedCommand}
client.Send(&respHeader, nil)

View File

@ -13,6 +13,12 @@ import (
"sync/atomic"
)
const (
// RPCAddrEnvName defines an environment variable name which sets
// an RPC address if there is no -rpc-addr specified.
RPCAddrEnvName = "CONSUL_RPC_ADDR"
)
var (
clientClosed = fmt.Errorf("client closed")
)
@ -84,7 +90,7 @@ func NewRPCClient(addr string) (*RPCClient, error) {
var conn net.Conn
var err error
if envAddr := os.Getenv("CONSUL_RPC_ADDR"); envAddr != "" {
if envAddr := os.Getenv(RPCAddrEnvName); envAddr != "" {
addr = envAddr
}
@ -188,48 +194,49 @@ func (c *RPCClient) WANMembers() ([]Member, error) {
return resp.Members, err
}
func (c *RPCClient) ListKeys(token string) (keyringResponse, error) {
func (c *RPCClient) ListKeys(token string, relayFactor uint8) (keyringResponse, error) {
header := requestHeader{
Command: listKeysCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{RelayFactor: relayFactor}
var resp keyringResponse
err := c.genericRPC(&header, nil, &resp)
err := c.genericRPC(&header, req, &resp)
return resp, err
}
func (c *RPCClient) InstallKey(key, token string) (keyringResponse, error) {
func (c *RPCClient) InstallKey(key, token string, relayFactor uint8) (keyringResponse, error) {
header := requestHeader{
Command: installKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
req := keyringRequest{Key: key, RelayFactor: relayFactor}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err
}
func (c *RPCClient) UseKey(key, token string) (keyringResponse, error) {
func (c *RPCClient) UseKey(key, token string, relayFactor uint8) (keyringResponse, error) {
header := requestHeader{
Command: useKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
req := keyringRequest{Key: key, RelayFactor: relayFactor}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err
}
func (c *RPCClient) RemoveKey(key, token string) (keyringResponse, error) {
func (c *RPCClient) RemoveKey(key, token string, relayFactor uint8) (keyringResponse, error) {
header := requestHeader{
Command: removeKeyCommand,
Seq: c.getSeq(),
Token: token,
}
req := keyringRequest{key}
req := keyringRequest{Key: key, RelayFactor: relayFactor}
var resp keyringResponse
err := c.genericRPC(&header, &req, &resp)
return resp, err

View File

@ -13,6 +13,7 @@ import (
"testing"
"time"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/serf/serf"
)
@ -38,7 +39,7 @@ func testRPCClient(t *testing.T) *rpcParts {
}
func testRPCClientWithConfig(t *testing.T, cb func(c *Config)) *rpcParts {
lw := NewLogWriter(512)
lw := logger.NewLogWriter(512)
mult := io.MultiWriter(os.Stderr, lw)
configTry := 0
@ -60,7 +61,7 @@ RECONF:
t.Fatalf("err: %s", err)
}
dir, agent := makeAgentLog(t, conf, mult)
dir, agent := makeAgentLog(t, conf, mult, lw)
rpc := NewAgentRPC(agent, l, mult, lw)
rpcClient, err := NewRPCClient(l.Addr().String())
@ -370,7 +371,7 @@ func TestRPCClientInstallKey(t *testing.T) {
})
// install key2
r, err := p1.client.InstallKey(key2, "")
r, err := p1.client.InstallKey(key2, "", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -401,7 +402,7 @@ func TestRPCClientUseKey(t *testing.T) {
defer p1.Close()
// add a second key to the ring
r, err := p1.client.InstallKey(key2, "")
r, err := p1.client.InstallKey(key2, "", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -422,21 +423,21 @@ func TestRPCClientUseKey(t *testing.T) {
})
// can't remove key1 yet
r, err = p1.client.RemoveKey(key1, "")
r, err = p1.client.RemoveKey(key1, "", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringError(t, r)
// change primary key
r, err = p1.client.UseKey(key2, "")
r, err = p1.client.UseKey(key2, "", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
keyringSuccess(t, r)
// can remove key1 now
r, err = p1.client.RemoveKey(key1, "")
r, err = p1.client.RemoveKey(key1, "", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -449,7 +450,7 @@ func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) {
})
defer p1.Close()
r, err := p1.client.ListKeys("")
r, err := p1.client.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -457,7 +458,7 @@ func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) {
}
func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int {
resp, err := c.ListKeys("")
resp, err := c.ListKeys("", 0)
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -1,11 +1,13 @@
package agent
import (
"github.com/hashicorp/logutils"
"log"
"os"
"testing"
"time"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/logutils"
)
type MockStreamClient struct {
@ -22,7 +24,7 @@ func (m *MockStreamClient) Send(h *responseHeader, o interface{}) error {
func TestRPCLogStream(t *testing.T) {
sc := &MockStreamClient{}
filter := LevelFilter()
filter := logger.LevelFilter()
filter.MinLevel = logutils.LogLevel("INFO")
ls := newLogStream(sc, filter, 42, log.New(os.Stderr, "", log.LstdFlags))

View File

@ -46,6 +46,7 @@ func (s *HTTPServer) SessionCreate(resp http.ResponseWriter, req *http.Request)
},
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
// Handle optional request body
if req.ContentLength > 0 {
@ -117,6 +118,7 @@ func (s *HTTPServer) SessionDestroy(resp http.ResponseWriter, req *http.Request)
Op: structs.SessionDestroy,
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
// Pull out the session id
args.Session.ID = strings.TrimPrefix(req.URL.Path, "/v1/session/destroy/")

View File

@ -0,0 +1,50 @@
package agent
import (
"bytes"
"net/http"
"github.com/hashicorp/consul/consul/structs"
)
// Snapshot handles requests to take and restore snapshots. This uses a special
// mechanism to make the RPC since we potentially stream large amounts of data
// as part of these requests.
func (s *HTTPServer) Snapshot(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
var args structs.SnapshotRequest
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if _, ok := req.URL.Query()["stale"]; ok {
args.AllowStale = true
}
switch req.Method {
case "GET":
args.Op = structs.SnapshotSave
// Headers need to go out before we stream the body.
replyFn := func(reply *structs.SnapshotResponse) error {
setMeta(resp, &reply.QueryMeta)
return nil
}
// Don't bother sending any request body through since it will
// be ignored.
var null bytes.Buffer
if err := s.agent.SnapshotRPC(&args, &null, resp, replyFn); err != nil {
return nil, err
}
return nil, nil
case "PUT":
args.Op = structs.SnapshotRestore
if err := s.agent.SnapshotRPC(&args, req.Body, resp, nil); err != nil {
return nil, err
}
return nil, nil
default:
resp.WriteHeader(http.StatusMethodNotAllowed)
return nil, nil
}
}

View File

@ -0,0 +1,142 @@
package agent
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSnapshot(t *testing.T) {
var snap io.Reader
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
req, err := http.NewRequest("GET", "/v1/snapshot?token=root", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
snap = resp.Body
header := resp.Header().Get("X-Consul-Index")
if header == "" {
t.Fatalf("bad: %v", header)
}
header = resp.Header().Get("X-Consul-KnownLeader")
if header != "true" {
t.Fatalf("bad: %v", header)
}
header = resp.Header().Get("X-Consul-LastContact")
if header != "0" {
t.Fatalf("bad: %v", header)
}
})
httpTest(t, func(srv *HTTPServer) {
req, err := http.NewRequest("PUT", "/v1/snapshot?token=root", snap)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
})
}
func TestSnapshot_Options(t *testing.T) {
for _, method := range []string{"GET", "PUT"} {
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
req, err := http.NewRequest(method, "/v1/snapshot?token=anonymous", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if err == nil || !strings.Contains(err.Error(), "Permission denied") {
t.Fatalf("err: %v", err)
}
})
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
req, err := http.NewRequest(method, "/v1/snapshot?dc=nope", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if err == nil || !strings.Contains(err.Error(), "No path to datacenter") {
t.Fatalf("err: %v", err)
}
})
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
req, err := http.NewRequest(method, "/v1/snapshot?token=root&stale", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if method == "GET" {
if err != nil {
t.Fatalf("err: %v", err)
}
} else {
if err == nil || !strings.Contains(err.Error(), "stale not allowed") {
t.Fatalf("err: %v", err)
}
}
})
}
}
func TestSnapshot_BadMethods(t *testing.T) {
httpTest(t, func(srv *HTTPServer) {
body := bytes.NewBuffer(nil)
req, err := http.NewRequest("POST", "/v1/snapshot", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(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("DELETE", "/v1/snapshot", body)
if err != nil {
t.Fatalf("err: %v", err)
}
resp := httptest.NewRecorder()
_, err = srv.Snapshot(resp, req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.Code != 405 {
t.Fatalf("bad code: %d", resp.Code)
}
})
}

View File

@ -23,7 +23,7 @@ func TestExecCommandRun(t *testing.T) {
ui := new(cli.MockUi)
c := &ExecCommand{Ui: ui}
args := []string{"-http-addr=" + a1.httpAddr, "-wait=400ms", "uptime"}
args := []string{"-http-addr=" + a1.httpAddr, "-wait=10s", "uptime"}
code := c.Run(args)
if code != 0 {

View File

@ -4,8 +4,9 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/mitchellh/cli"
"strings"
"github.com/mitchellh/cli"
)
// KeygenCommand is a Command implementation that generates an encryption

View File

@ -18,6 +18,7 @@ type KeyringCommand struct {
func (c *KeyringCommand) Run(args []string) int {
var installKey, useKey, removeKey, token string
var listKeys bool
var relay int
cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
@ -27,6 +28,7 @@ func (c *KeyringCommand) Run(args []string) int {
cmdFlags.StringVar(&removeKey, "remove", "", "remove key")
cmdFlags.BoolVar(&listKeys, "list", false, "list keys")
cmdFlags.StringVar(&token, "token", "", "acl token")
cmdFlags.IntVar(&relay, "relay-factor", 0, "relay factor")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
@ -56,6 +58,13 @@ func (c *KeyringCommand) Run(args []string) int {
return 1
}
// Validate the relay factor
relayFactor, err := agent.ParseRelayFactor(relay)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing relay factor: %s", err))
return 1
}
// All other operations will require a client connection
client, err := RPCClient(*rpcAddr)
if err != nil {
@ -66,7 +75,7 @@ func (c *KeyringCommand) Run(args []string) int {
if listKeys {
c.Ui.Info("Gathering installed encryption keys...")
r, err := client.ListKeys(token)
r, err := client.ListKeys(token, relayFactor)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -80,7 +89,7 @@ func (c *KeyringCommand) Run(args []string) int {
if installKey != "" {
c.Ui.Info("Installing new gossip encryption key...")
r, err := client.InstallKey(installKey, token)
r, err := client.InstallKey(installKey, token, relayFactor)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -90,7 +99,7 @@ func (c *KeyringCommand) Run(args []string) int {
if useKey != "" {
c.Ui.Info("Changing primary gossip encryption key...")
r, err := client.UseKey(useKey, token)
r, err := client.UseKey(useKey, token, relayFactor)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -100,7 +109,7 @@ func (c *KeyringCommand) Run(args []string) int {
if removeKey != "" {
c.Ui.Info("Removing gossip encryption key...")
r, err := client.RemoveKey(removeKey, token)
r, err := client.RemoveKey(removeKey, token, relayFactor)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
@ -206,6 +215,11 @@ Options:
not currently the primary key.
-token="" ACL token to use during requests. Defaults to that
of the agent.
-relay-factor Added in Consul 0.7.4, setting this to a non-zero
value will cause nodes to relay their response to
the operation through this many randomly-chosen
other nodes in the cluster. The maximum allowed
value is 5.
-use=<key> Change the primary encryption key, which is used to
encrypt messages. The key must already be installed
before this operation can succeed.

View File

@ -89,6 +89,17 @@ func TestKeyringCommandRun_failedConnection(t *testing.T) {
}
}
func TestKeyringCommandRun_invalidRelayFactor(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-list", "-relay-factor=6"}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}
func listKeys(t *testing.T, addr string) string {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}

123
command/kv_export.go Normal file
View File

@ -0,0 +1,123 @@
package command
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
)
// KVExportCommand is a Command implementation that is used to export
// a KV tree as JSON
type KVExportCommand struct {
Ui cli.Ui
}
func (c *KVExportCommand) Synopsis() string {
return "Exports a tree from the KV store as JSON"
}
func (c *KVExportCommand) Help() string {
helpText := `
Usage: consul kv export [KEY_OR_PREFIX]
Retrieves key-value pairs for the given prefix from Consul's key-value store,
and writes a JSON representation to stdout. This can be used with the command
"consul kv import" to move entire trees between Consul clusters.
$ consul kv export vault
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText + `
KV Export Options:
None.
`
return strings.TrimSpace(helpText)
}
func (c *KVExportCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("export", flag.ContinueOnError)
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
stale := cmdFlags.Bool("stale", false, "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
key := ""
// Check for arg validation
args = cmdFlags.Args()
switch len(args) {
case 0:
key = ""
case 1:
key = args[0]
default:
c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// This is just a "nice" thing to do. Since pairs cannot start with a /, but
// users will likely put "/" or "/foo", lets go ahead and strip that for them
// here.
if len(key) > 0 && key[0] == '/' {
key = key[1:]
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
pairs, _, err := client.KV().List(key, &api.QueryOptions{
Datacenter: *datacenter,
AllowStale: *stale,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
return 1
}
exported := make([]*kvExportEntry, len(pairs))
for i, pair := range pairs {
exported[i] = toExportEntry(pair)
}
marshaled, err := json.MarshalIndent(exported, "", "\t")
if err != nil {
c.Ui.Error(fmt.Sprintf("Error exporting KV data: %s", err))
return 1
}
c.Ui.Info(string(marshaled))
return 0
}
type kvExportEntry struct {
Key string `json:"key"`
Flags uint64 `json:"flags"`
Value string `json:"value"`
}
func toExportEntry(pair *api.KVPair) *kvExportEntry {
return &kvExportEntry{
Key: pair.Key,
Flags: pair.Flags,
Value: base64.StdEncoding.EncodeToString(pair.Value),
}
}

60
command/kv_export_test.go Normal file
View File

@ -0,0 +1,60 @@
package command
import (
"encoding/base64"
"encoding/json"
"testing"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
)
func TestKVExportCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVExportCommand{Ui: ui}
keys := map[string]string{
"foo/a": "a",
"foo/b": "b",
"foo/c": "c",
"bar": "d",
}
for k, v := range keys {
pair := &api.KVPair{Key: k, Value: []byte(v)}
if _, err := client.KV().Put(pair, nil); err != nil {
t.Fatalf("err: %#v", err)
}
}
args := []string{
"-http-addr=" + srv.httpAddr,
"foo",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
var exported []*kvExportEntry
err := json.Unmarshal([]byte(output), &exported)
if err != nil {
t.Fatalf("bad: %d", code)
}
if len(exported) != 3 {
t.Fatalf("bad: expected 3, got %d", len(exported))
}
for _, entry := range exported {
if base64.StdEncoding.EncodeToString([]byte(keys[entry.Key])) != entry.Value {
t.Fatalf("bad: expected %s, got %s", keys[entry.Key], entry.Value)
}
}
}

View File

@ -2,6 +2,7 @@ package command
import (
"bytes"
"encoding/base64"
"flag"
"fmt"
"io"
@ -54,6 +55,8 @@ Usage: consul kv get [options] [KEY_OR_PREFIX]
KV Get Options:
-base64 Base64 encode the value. The default value is false.
-detailed Provide additional metadata about the key in addition
to the value such as the ModifyIndex and any flags
that may have been set on the key. The default value
@ -84,6 +87,7 @@ func (c *KVGetCommand) Run(args []string) int {
stale := cmdFlags.Bool("stale", false, "")
detailed := cmdFlags.Bool("detailed", false, "")
keys := cmdFlags.Bool("keys", false, "")
base64encode := cmdFlags.Bool("base64", false, "")
recurse := cmdFlags.Bool("recurse", false, "")
separator := cmdFlags.String("separator", "/", "")
httpAddr := HTTPAddrFlag(cmdFlags)
@ -158,7 +162,7 @@ func (c *KVGetCommand) Run(args []string) int {
for i, pair := range pairs {
if *detailed {
var b bytes.Buffer
if err := prettyKVPair(&b, pair); err != nil {
if err := prettyKVPair(&b, pair, *base64encode); err != nil {
c.Ui.Error(fmt.Sprintf("Error rendering KV pair: %s", err))
return 1
}
@ -169,7 +173,11 @@ func (c *KVGetCommand) Run(args []string) int {
c.Ui.Info("")
}
} else {
c.Ui.Info(fmt.Sprintf("%s:%s", pair.Key, pair.Value))
if *base64encode {
c.Ui.Info(fmt.Sprintf("%s:%s", pair.Key, base64.StdEncoding.EncodeToString(pair.Value)))
} else {
c.Ui.Info(fmt.Sprintf("%s:%s", pair.Key, pair.Value))
}
}
}
@ -191,7 +199,7 @@ func (c *KVGetCommand) Run(args []string) int {
if *detailed {
var b bytes.Buffer
if err := prettyKVPair(&b, pair); err != nil {
if err := prettyKVPair(&b, pair, *base64encode); err != nil {
c.Ui.Error(fmt.Sprintf("Error rendering KV pair: %s", err))
return 1
}
@ -209,7 +217,7 @@ func (c *KVGetCommand) Synopsis() string {
return "Retrieves or lists data from the KV store"
}
func prettyKVPair(w io.Writer, pair *api.KVPair) error {
func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool) error {
tw := tabwriter.NewWriter(w, 0, 2, 6, ' ', 0)
fmt.Fprintf(tw, "CreateIndex\t%d\n", pair.CreateIndex)
fmt.Fprintf(tw, "Flags\t%d\n", pair.Flags)
@ -217,10 +225,14 @@ func prettyKVPair(w io.Writer, pair *api.KVPair) error {
fmt.Fprintf(tw, "LockIndex\t%d\n", pair.LockIndex)
fmt.Fprintf(tw, "ModifyIndex\t%d\n", pair.ModifyIndex)
if pair.Session == "" {
fmt.Fprintf(tw, "Session\t-\n")
fmt.Fprint(tw, "Session\t-\n")
} else {
fmt.Fprintf(tw, "Session\t%s\n", pair.Session)
}
fmt.Fprintf(tw, "Value\t%s", pair.Value)
if base64EncodeValue {
fmt.Fprintf(tw, "Value\t%s", base64.StdEncoding.EncodeToString(pair.Value))
} else {
fmt.Fprintf(tw, "Value\t%s", pair.Value)
}
return tw.Flush()
}

View File

@ -1,6 +1,7 @@
package command
import (
"encoding/base64"
"strings"
"testing"
@ -250,3 +251,91 @@ func TestKVGetCommand_Recurse(t *testing.T) {
}
}
}
func TestKVGetCommand_RecurseBase64(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
keys := map[string]string{
"foo/a": "Hello World 1",
"foo/b": "Hello World 2",
"foo/c": "Hello World 3",
}
for k, v := range keys {
pair := &api.KVPair{Key: k, Value: []byte(v)}
if _, err := client.KV().Put(pair, nil); err != nil {
t.Fatalf("err: %#v", err)
}
}
args := []string{
"-http-addr=" + srv.httpAddr,
"-recurse",
"-base64",
"foo",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
for key, value := range keys {
if !strings.Contains(output, key+":"+base64.StdEncoding.EncodeToString([]byte(value))) {
t.Fatalf("bad %#v missing %q", output, key)
}
}
}
func TestKVGetCommand_DetailedBase64(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
pair := &api.KVPair{
Key: "foo",
Value: []byte("bar"),
}
_, err := client.KV().Put(pair, nil)
if err != nil {
t.Fatalf("err: %#v", err)
}
args := []string{
"-http-addr=" + srv.httpAddr,
"-detailed",
"-base64",
"foo",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
for _, key := range []string{
"CreateIndex",
"LockIndex",
"ModifyIndex",
"Flags",
"Session",
"Value",
} {
if !strings.Contains(output, key) {
t.Fatalf("bad %#v, missing %q", output, key)
}
}
if !strings.Contains(output, base64.StdEncoding.EncodeToString([]byte("bar"))) {
t.Fatalf("bad %#v, value is not base64 encoded", output)
}
}

165
command/kv_import.go Normal file
View File

@ -0,0 +1,165 @@
package command
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
)
// KVImportCommand is a Command implementation that is used to import
// a KV tree stored as JSON
type KVImportCommand struct {
Ui cli.Ui
// testStdin is the input for testing.
testStdin io.Reader
}
func (c *KVImportCommand) Synopsis() string {
return "Imports a tree stored as JSON to the KV store"
}
func (c *KVImportCommand) Help() string {
helpText := `
Usage: consul kv import [DATA]
Imports key-value pairs to the key-value store from the JSON representation
generated by the "consul kv export" command.
The data can be read from a file by prefixing the filename with the "@"
symbol. For example:
$ consul kv import @filename.json
Or it can be read from stdin using the "-" symbol:
$ cat filename.json | consul kv import config/program/license -
Alternatively the data may be provided as the final parameter to the command,
though care must be taken with regards to shell escaping.
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText + `
KV Import Options:
None.
`
return strings.TrimSpace(helpText)
}
func (c *KVImportCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("import", flag.ContinueOnError)
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Check for arg validation
args = cmdFlags.Args()
data, err := c.dataFromArgs(args)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! %s", err))
return 1
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
var entries []*kvExportEntry
if err := json.Unmarshal([]byte(data), &entries); err != nil {
c.Ui.Error(fmt.Sprintf("Cannot unmarshal data: %s", err))
return 1
}
for _, entry := range entries {
value, err := base64.StdEncoding.DecodeString(entry.Value)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error base 64 decoding value for key %s: %s", entry.Key, err))
return 1
}
pair := &api.KVPair{
Key: entry.Key,
Flags: entry.Flags,
Value: value,
}
wo := &api.WriteOptions{
Datacenter: *datacenter,
Token: *token,
}
if _, err := client.KV().Put(pair, wo); err != nil {
c.Ui.Error(fmt.Sprintf("Error! Failed writing data for key %s: %s", pair.Key, err))
return 1
}
c.Ui.Info(fmt.Sprintf("Imported: %s", pair.Key))
}
return 0
}
func (c *KVImportCommand) dataFromArgs(args []string) (string, error) {
var stdin io.Reader = os.Stdin
if c.testStdin != nil {
stdin = c.testStdin
}
switch len(args) {
case 0:
return "", errors.New("Missing DATA argument")
case 1:
default:
return "", fmt.Errorf("Too many arguments (expected 1 or 2, got %d)", len(args))
}
data := args[0]
if len(data) == 0 {
return "", errors.New("Empty DATA argument")
}
switch data[0] {
case '@':
data, err := ioutil.ReadFile(data[1:])
if err != nil {
return "", fmt.Errorf("Failed to read file: %s", err)
}
return string(data), nil
case '-':
if len(data) > 1 {
return data, nil
} else {
var b bytes.Buffer
if _, err := io.Copy(&b, stdin); err != nil {
return "", fmt.Errorf("Failed to read stdin: %s", err)
}
return b.String(), nil
}
default:
return data, nil
}
}

61
command/kv_import_test.go Normal file
View File

@ -0,0 +1,61 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestKVImportCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
const json = `[
{
"key": "foo",
"flags": 0,
"value": "YmFyCg=="
},
{
"key": "foo/a",
"flags": 0,
"value": "YmF6Cg=="
}
]`
ui := new(cli.MockUi)
c := &KVImportCommand{
Ui: ui,
testStdin: strings.NewReader(json),
}
args := []string{
"-http-addr=" + srv.httpAddr,
"-",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
pair, _, err := client.KV().Get("foo", nil)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(pair.Value)) != "bar" {
t.Fatalf("bad: expected: bar, got %s", pair.Value)
}
pair, _, err = client.KV().Get("foo/a", nil)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(pair.Value)) != "baz" {
t.Fatalf("bad: expected: baz, got %s", pair.Value)
}
}

View File

@ -2,6 +2,7 @@ package command
import (
"bytes"
"encoding/base64"
"flag"
"fmt"
"io"
@ -45,6 +46,9 @@ Usage: consul kv put [options] KEY [DATA]
$ consul kv put webapp/beta/active
If the -base64 flag is specified, the data will be treated as base 64
encoded.
To perform a Check-And-Set operation, specify the -cas flag with the
appropriate -modify-index flag corresponding to the key you want to perform
the CAS operation on:
@ -62,6 +66,9 @@ KV Put Options:
lock. The session must already exist and be specified
via the -session flag. The default value is false.
-base64 Treat the data as base 64 encoded. The default value
is false.
-cas Perform a Check-And-Set operation. Specifying this
value also requires the -modify-index flag to be set.
The default value is false.
@ -74,7 +81,7 @@ KV Put Options:
-modify-index=<int> Unsigned integer representing the ModifyIndex of the
key. This is used in combination with the -cas flag.
-release Forfeit the lock on the key at the givne path. This
-release Forfeit the lock on the key at the given path. This
requires the -session flag to be set. The key must be
held by the session in order to be unlocked. The
default value is false.
@ -95,6 +102,7 @@ func (c *KVPutCommand) Run(args []string) int {
token := cmdFlags.String("token", "", "")
cas := cmdFlags.Bool("cas", false, "")
flags := cmdFlags.Uint64("flags", 0, "")
base64encoded := cmdFlags.Bool("base64", false, "")
modifyIndex := cmdFlags.Uint64("modify-index", 0, "")
session := cmdFlags.String("session", "", "")
acquire := cmdFlags.Bool("acquire", false, "")
@ -111,6 +119,14 @@ func (c *KVPutCommand) Run(args []string) int {
return 1
}
dataBytes := []byte(data)
if *base64encoded {
dataBytes, err = base64.StdEncoding.DecodeString(data)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! Cannot base 64 decode data: %s", err))
}
}
// Session is reauired for release or acquire
if (*release || *acquire) && *session == "" {
c.Ui.Error("Error! Missing -session (required with -acquire and -release)")
@ -137,7 +153,7 @@ func (c *KVPutCommand) Run(args []string) int {
Key: key,
ModifyIndex: *modifyIndex,
Flags: *flags,
Value: []byte(data),
Value: dataBytes,
Session: *session,
}
@ -220,6 +236,11 @@ func (c *KVPutCommand) dataFromArgs(args []string) (string, string, error) {
key := args[0]
data := args[1]
// Handle empty quoted shell parameters
if len(data) == 0 {
return key, "", nil
}
switch data[0] {
case '@':
data, err := ioutil.ReadFile(data[1:])
@ -228,11 +249,15 @@ func (c *KVPutCommand) dataFromArgs(args []string) (string, string, error) {
}
return key, string(data), nil
case '-':
var b bytes.Buffer
if _, err := io.Copy(&b, stdin); err != nil {
return "", "", fmt.Errorf("Failed to read stdin: %s", err)
if len(data) > 1 {
return key, data, nil
} else {
var b bytes.Buffer
if _, err := io.Copy(&b, stdin); err != nil {
return "", "", fmt.Errorf("Failed to read stdin: %s", err)
}
return key, b.String(), nil
}
return key, b.String(), nil
default:
return key, data, nil
}

View File

@ -2,6 +2,7 @@ package command
import (
"bytes"
"encoding/base64"
"io"
"io/ioutil"
"os"
@ -100,6 +101,70 @@ func TestKVPutCommand_Run(t *testing.T) {
}
}
func TestKVPutCommand_RunEmptyDataQuoted(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
args := []string{
"-http-addr=" + srv.httpAddr,
"foo", "",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
data, _, err := client.KV().Get("foo", nil)
if err != nil {
t.Fatal(err)
}
if data.Value != nil {
t.Errorf("bad: %#v", data.Value)
}
}
func TestKVPutCommand_RunBase64(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
const encodedString = "aGVsbG8gd29ybGQK"
args := []string{
"-http-addr=" + srv.httpAddr,
"-base64",
"foo", encodedString,
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
data, _, err := client.KV().Get("foo", nil)
if err != nil {
t.Fatal(err)
}
expected, err := base64.StdEncoding.DecodeString(encodedString)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data.Value, []byte(expected)) {
t.Errorf("bad: %#v, %s", data.Value, data.Value)
}
}
func TestKVPutCommand_File(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
@ -194,6 +259,34 @@ func TestKVPutCommand_Stdin(t *testing.T) {
}
}
func TestKVPutCommand_NegativeVal(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
args := []string{
"-http-addr=" + srv.httpAddr,
"foo", "-2",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
data, _, err := client.KV().Get("foo", nil)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data.Value, []byte("-2")) {
t.Errorf("bad: %#v", data.Value)
}
}
func TestKVPutCommand_Flags(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()

View File

@ -24,7 +24,7 @@ Usage: consul operator <subcommand> [common options] [action] [options]
the Raft subsystem. NOTE: Use this command with extreme caution, as improper
use could lead to a Consul outage and even loss of data.
If ACLs are enabled then a token with operator privileges may required in
If ACLs are enabled then a token with operator privileges may be required in
order to use this command. Requests are forwarded internally to the leader
if required, so this can be run from any Consul node in a cluster.

View File

@ -8,20 +8,10 @@ import (
"github.com/hashicorp/consul/command/agent"
)
const (
// RPCAddrEnvName defines an environment variable name which sets
// an RPC address if there is no -rpc-addr specified.
RPCAddrEnvName = "CONSUL_RPC_ADDR"
// HTTPAddrEnvName defines an environment variable name which sets
// the HTTP address if there is no -http-addr specified.
HTTPAddrEnvName = "CONSUL_HTTP_ADDR"
)
// RPCAddrFlag returns a pointer to a string that will be populated
// when the given flagset is parsed with the RPC address of the Consul.
func RPCAddrFlag(f *flag.FlagSet) *string {
defaultRPCAddr := os.Getenv(RPCAddrEnvName)
defaultRPCAddr := os.Getenv(agent.RPCAddrEnvName)
if defaultRPCAddr == "" {
defaultRPCAddr = "127.0.0.1:8400"
}
@ -37,7 +27,7 @@ func RPCClient(addr string) (*agent.RPCClient, error) {
// HTTPAddrFlag returns a pointer to a string that will be populated
// when the given flagset is parsed with the HTTP address of the Consul.
func HTTPAddrFlag(f *flag.FlagSet) *string {
defaultHTTPAddr := os.Getenv(HTTPAddrEnvName)
defaultHTTPAddr := os.Getenv(consulapi.HTTPAddrEnvName)
if defaultHTTPAddr == "" {
defaultHTTPAddr = "127.0.0.1:8500"
}

View File

@ -4,6 +4,9 @@ import (
"flag"
"os"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/agent"
)
const (
@ -21,11 +24,11 @@ func getParsedAddr(t *testing.T, addrType, cliVal, envVal string) string {
switch addrType {
case "rpc":
fn = RPCAddrFlag
envVar = RPCAddrEnvName
envVar = agent.RPCAddrEnvName
cliFlag = "-rpc-addr"
case "http":
fn = HTTPAddrFlag
envVar = HTTPAddrEnvName
envVar = consulapi.HTTPAddrEnvName
cliFlag = "-http-addr"
default:
t.Fatalf("unknown address type %s", addrType)

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/serf/coordinate"
"github.com/mitchellh/cli"
)
@ -88,29 +89,32 @@ func TestRTTCommand_Run_LAN(t *testing.T) {
}
}
// Wait for the updates to get flushed to the data store.
time.Sleep(2 * updatePeriod)
// Ask for the RTT of two known nodes
ui := new(cli.MockUi)
c := &RTTCommand{Ui: ui}
args := []string{
"-http-addr=" + a.httpAddr,
a.config.NodeName,
"dogs",
}
// Try two known nodes.
{
ui := new(cli.MockUi)
c := &RTTCommand{Ui: ui}
args := []string{
"-http-addr=" + a.httpAddr,
a.config.NodeName,
"dogs",
}
// Wait for the updates to get flushed to the data store.
testutil.WaitForResult(func() (bool, error) {
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d: %#v", code, ui.ErrorWriter.String())
return false, fmt.Errorf("bad: %d: %#v", code, ui.ErrorWriter.String())
}
// Make sure the proper RTT was reported in the output.
expected := fmt.Sprintf("rtt: %s", dist_str)
if !strings.Contains(ui.OutputWriter.String(), expected) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
return false, fmt.Errorf("bad: %#v", ui.OutputWriter.String())
}
}
return true, nil
}, func(err error) {
t.Fatalf("failed to get proper RTT output: %v", err)
})
// Default to the agent's node.
{

View File

@ -0,0 +1,52 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
// SnapshotCommand is a Command implementation that just shows help for
// the subcommands nested below it.
type SnapshotCommand struct {
Ui cli.Ui
}
func (c *SnapshotCommand) Run(args []string) int {
return cli.RunResultHelp
}
func (c *SnapshotCommand) Help() string {
helpText := `
Usage: consul snapshot <subcommand> [options] [args]
This command has subcommands for saving, restoring, and inspecting the state
of the Consul servers for disaster recovery. These are atomic, point-in-time
snapshots which include key/value entries, service catalog, prepared queries,
sessions, and ACLs.
If ACLs are enabled, a management token must be supplied in order to perform
snapshot operations.
Create a snapshot:
$ consul snapshot save backup.snap
Restore a snapshot:
$ consul snapshot restore backup.snap
Inspect a snapshot:
$ consul snapshot inspect backup.snap
For more examples, ask for subcommand help or view the documentation.
`
return strings.TrimSpace(helpText)
}
func (c *SnapshotCommand) Synopsis() string {
return "Saves, restores and inspects snapshots of Consul server state"
}

View File

@ -0,0 +1,15 @@
package command
import (
"testing"
"github.com/mitchellh/cli"
)
func TestSnapshotCommand_implements(t *testing.T) {
var _ cli.Command = &SnapshotCommand{}
}
func TestSnapshotCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(SnapshotCommand))
}

View File

@ -0,0 +1,89 @@
package command
import (
"bytes"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/hashicorp/consul/snapshot"
"github.com/mitchellh/cli"
)
// SnapshotInspectCommand is a Command implementation that is used to display
// metadata about a snapshot file
type SnapshotInspectCommand struct {
Ui cli.Ui
}
func (c *SnapshotInspectCommand) Help() string {
helpText := `
Usage: consul snapshot inspect [options] FILE
Displays information about a snapshot file on disk.
To inspect the file "backup.snap":
$ consul snapshot inspect backup.snap
For a full list of options and examples, please see the Consul documentation.
`
return strings.TrimSpace(helpText)
}
func (c *SnapshotInspectCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var file string
args = cmdFlags.Args()
switch len(args) {
case 0:
c.Ui.Error("Missing FILE argument")
return 1
case 1:
file = args[0]
default:
c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// Open the file.
f, err := os.Open(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error opening snapshot file: %s", err))
return 1
}
defer f.Close()
meta, err := snapshot.Verify(f)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error verifying snapshot: %s", err))
}
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
fmt.Fprintf(tw, "ID\t%s\n", meta.ID)
fmt.Fprintf(tw, "Size\t%d\n", meta.Size)
fmt.Fprintf(tw, "Index\t%d\n", meta.Index)
fmt.Fprintf(tw, "Term\t%d\n", meta.Term)
fmt.Fprintf(tw, "Version\t%d\n", meta.Version)
if err = tw.Flush(); err != nil {
c.Ui.Error(fmt.Sprintf("Error rendering snapshot info: %s", err))
}
c.Ui.Info(b.String())
return 0
}
func (c *SnapshotInspectCommand) Synopsis() string {
return "Displays information about a Consul snapshot file"
}

View File

@ -0,0 +1,116 @@
package command
import (
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestSnapshotInspectCommand_implements(t *testing.T) {
var _ cli.Command = &SnapshotInspectCommand{}
}
func TestSnapshotInspectCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(SnapshotInspectCommand))
}
func TestSnapshotInspectCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &SnapshotInspectCommand{Ui: ui}
cases := map[string]struct {
args []string
output string
}{
"no file": {
[]string{},
"Missing FILE argument",
},
"extra args": {
[]string{"foo", "bar", "baz"},
"Too many arguments",
},
}
for name, tc := range cases {
// Ensure our buffer is always clear
if ui.ErrorWriter != nil {
ui.ErrorWriter.Reset()
}
if ui.OutputWriter != nil {
ui.OutputWriter.Reset()
}
code := c.Run(tc.args)
if code == 0 {
t.Errorf("%s: expected non-zero exit", name)
}
output := ui.ErrorWriter.String()
if !strings.Contains(output, tc.output) {
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
}
}
}
func TestSnapshotInspectCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
dir, err := ioutil.TempDir("", "snapshot")
if err != nil {
t.Fatalf("err: %v", err)
}
defer os.RemoveAll(dir)
file := path.Join(dir, "backup.tgz")
// Save a snapshot of the current Consul state
f, err := os.Create(file)
if err != nil {
t.Fatalf("err: %v", err)
}
snap, _, err := client.Snapshot().Save(nil)
if err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if _, err := io.Copy(f, snap); err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("err: %v", err)
}
// Inspect the snapshot
inspect := &SnapshotInspectCommand{Ui: ui}
args := []string{file}
code := inspect.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
for _, key := range []string{
"ID",
"Size",
"Index",
"Term",
"Version",
} {
if !strings.Contains(output, key) {
t.Fatalf("bad %#v, missing %q", output, key)
}
}
}

102
command/snapshot_restore.go Normal file
View File

@ -0,0 +1,102 @@
package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
)
// SnapshotRestoreCommand is a Command implementation that is used to restore
// the state of the Consul servers for disaster recovery.
type SnapshotRestoreCommand struct {
Ui cli.Ui
}
func (c *SnapshotRestoreCommand) Help() string {
helpText := `
Usage: consul snapshot restore [options] FILE
Restores an atomic, point-in-time snapshot of the state of the Consul servers
which includes key/value entries, service catalog, prepared queries, sessions,
and ACLs.
Restores involve a potentially dangerous low-level Raft operation that is not
designed to handle server failures during a restore. This command is primarily
intended to be used when recovering from a disaster, restoring into a fresh
cluster of Consul servers.
If ACLs are enabled, a management token must be supplied in order to perform
snapshot operations.
To restore a snapshot from the file "backup.snap":
$ consul snapshot restore backup.snap
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText
return strings.TrimSpace(helpText)
}
func (c *SnapshotRestoreCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var file string
args = cmdFlags.Args()
switch len(args) {
case 0:
c.Ui.Error("Missing FILE argument")
return 1
case 1:
file = args[0]
default:
c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Datacenter = *datacenter
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// Open the file.
f, err := os.Open(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error opening snapshot file: %s", err))
return 1
}
defer f.Close()
// Restore the snapshot.
err = client.Snapshot().Restore(nil, f)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error restoring snapshot: %s", err))
return 1
}
c.Ui.Info("Restored snapshot")
return 0
}
func (c *SnapshotRestoreCommand) Synopsis() string {
return "Restores snapshot of Consul server state"
}

View File

@ -0,0 +1,103 @@
package command
import (
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestSnapshotRestoreCommand_implements(t *testing.T) {
var _ cli.Command = &SnapshotRestoreCommand{}
}
func TestSnapshotRestoreCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(SnapshotRestoreCommand))
}
func TestSnapshotRestoreCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &SnapshotRestoreCommand{Ui: ui}
cases := map[string]struct {
args []string
output string
}{
"no file": {
[]string{},
"Missing FILE argument",
},
"extra args": {
[]string{"foo", "bar", "baz"},
"Too many arguments",
},
}
for name, tc := range cases {
// Ensure our buffer is always clear
if ui.ErrorWriter != nil {
ui.ErrorWriter.Reset()
}
if ui.OutputWriter != nil {
ui.OutputWriter.Reset()
}
code := c.Run(tc.args)
if code == 0 {
t.Errorf("%s: expected non-zero exit", name)
}
output := ui.ErrorWriter.String()
if !strings.Contains(output, tc.output) {
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
}
}
}
func TestSnapshotRestoreCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &SnapshotSaveCommand{Ui: ui}
dir, err := ioutil.TempDir("", "snapshot")
if err != nil {
t.Fatalf("err: %v", err)
}
defer os.RemoveAll(dir)
file := path.Join(dir, "backup.tgz")
args := []string{
"-http-addr=" + srv.httpAddr,
file,
}
f, err := os.Create(file)
if err != nil {
t.Fatalf("err: %v", err)
}
snap, _, err := client.Snapshot().Save(nil)
if err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if _, err := io.Copy(f, snap); err != nil {
f.Close()
t.Fatalf("err: %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("err: %v", err)
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}

132
command/snapshot_save.go Normal file
View File

@ -0,0 +1,132 @@
package command
import (
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/snapshot"
"github.com/mitchellh/cli"
)
// SnapshotSaveCommand is a Command implementation that is used to save the
// state of the Consul servers for disaster recovery.
type SnapshotSaveCommand struct {
Ui cli.Ui
}
func (c *SnapshotSaveCommand) Help() string {
helpText := `
Usage: consul snapshot save [options] FILE
Retrieves an atomic, point-in-time snapshot of the state of the Consul servers
which includes key/value entries, service catalog, prepared queries, sessions,
and ACLs.
If ACLs are enabled, a management token must be supplied in order to perform
snapshot operations.
To create a snapshot from the leader server and save it to "backup.snap":
$ consul snapshot save backup.snap
To create a potentially stale snapshot from any available server (useful if no
leader is available):
$ consul snapshot save -stale backup.snap
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText
return strings.TrimSpace(helpText)
}
func (c *SnapshotSaveCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
stale := cmdFlags.Bool("stale", false, "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var file string
args = cmdFlags.Args()
switch len(args) {
case 0:
c.Ui.Error("Missing FILE argument")
return 1
case 1:
file = args[0]
default:
c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Datacenter = *datacenter
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// Take the snapshot.
snap, qm, err := client.Snapshot().Save(&api.QueryOptions{
AllowStale: *stale,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error saving snapshot: %s", err))
return 1
}
defer snap.Close()
// Save the file.
f, err := os.Create(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error creating snapshot file: %s", err))
return 1
}
if _, err := io.Copy(f, snap); err != nil {
f.Close()
c.Ui.Error(fmt.Sprintf("Error writing snapshot file: %s", err))
return 1
}
if err := f.Close(); err != nil {
c.Ui.Error(fmt.Sprintf("Error closing snapshot file after writing: %s", err))
return 1
}
// Read it back to verify.
f, err = os.Open(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error opening snapshot file for verify: %s", err))
return 1
}
if _, err := snapshot.Verify(f); err != nil {
f.Close()
c.Ui.Error(fmt.Sprintf("Error verifying snapshot file: %s", err))
return 1
}
if err := f.Close(); err != nil {
c.Ui.Error(fmt.Sprintf("Error closing snapshot file after verify: %s", err))
return 1
}
c.Ui.Info(fmt.Sprintf("Saved and verified snapshot to index %d", qm.LastIndex))
return 0
}
func (c *SnapshotSaveCommand) Synopsis() string {
return "Saves snapshot of Consul server state"
}

View File

@ -0,0 +1,94 @@
package command
import (
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestSnapshotSaveCommand_implements(t *testing.T) {
var _ cli.Command = &SnapshotSaveCommand{}
}
func TestSnapshotSaveCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(SnapshotSaveCommand))
}
func TestSnapshotSaveCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &SnapshotSaveCommand{Ui: ui}
cases := map[string]struct {
args []string
output string
}{
"no file": {
[]string{},
"Missing FILE argument",
},
"extra args": {
[]string{"foo", "bar", "baz"},
"Too many arguments",
},
}
for name, tc := range cases {
// Ensure our buffer is always clear
if ui.ErrorWriter != nil {
ui.ErrorWriter.Reset()
}
if ui.OutputWriter != nil {
ui.OutputWriter.Reset()
}
code := c.Run(tc.args)
if code == 0 {
t.Errorf("%s: expected non-zero exit", name)
}
output := ui.ErrorWriter.String()
if !strings.Contains(output, tc.output) {
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
}
}
}
func TestSnapshotSaveCommand_Run(t *testing.T) {
srv, client := testAgentWithAPIClient(t)
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &SnapshotSaveCommand{Ui: ui}
dir, err := ioutil.TempDir("", "snapshot")
if err != nil {
t.Fatalf("err: %v", err)
}
defer os.RemoveAll(dir)
file := path.Join(dir, "backup.tgz")
args := []string{
"-http-addr=" + srv.httpAddr,
file,
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
f, err := os.Open(file)
if err != nil {
t.Fatalf("err: %v", err)
}
defer f.Close()
if err := client.Snapshot().Restore(nil, f); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/consul"
"github.com/hashicorp/consul/logger"
"github.com/mitchellh/cli"
)
@ -61,7 +62,7 @@ func testAgentWithConfig(t *testing.T, cb func(c *agent.Config)) *agentWrapper {
t.Fatalf("err: %s", err)
}
lw := agent.NewLogWriter(512)
lw := logger.NewLogWriter(512)
mult := io.MultiWriter(os.Stderr, lw)
conf := nextConfig()
@ -73,7 +74,7 @@ func testAgentWithConfig(t *testing.T, cb func(c *agent.Config)) *agentWrapper {
}
conf.DataDir = dir
a, err := agent.Create(conf, lw)
a, err := agent.Create(conf, lw, nil, nil)
if err != nil {
os.RemoveAll(dir)
t.Fatalf(fmt.Sprintf("err: %v", err))

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/consul/command"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/version"
"github.com/mitchellh/cli"
)
@ -19,10 +20,10 @@ func init() {
Commands = map[string]cli.CommandFactory{
"agent": func() (cli.Command, error) {
return &agent.Command{
Revision: GitCommit,
Version: Version,
VersionPrerelease: VersionPrerelease,
HumanVersion: GetHumanVersion(),
Revision: version.GitCommit,
Version: version.Version,
VersionPrerelease: version.VersionPrerelease,
HumanVersion: version.GetHumanVersion(),
Ui: ui,
ShutdownCh: make(chan struct{}),
}, nil
@ -77,6 +78,18 @@ func init() {
}, nil
},
"kv export": func() (cli.Command, error) {
return &command.KVExportCommand{
Ui: ui,
}, nil
},
"kv import": func() (cli.Command, error) {
return &command.KVImportCommand{
Ui: ui,
}, nil
},
"join": func() (cli.Command, error) {
return &command.JoinCommand{
Ui: ui,
@ -154,9 +167,33 @@ func init() {
}, nil
},
"snapshot": func() (cli.Command, error) {
return &command.SnapshotCommand{
Ui: ui,
}, nil
},
"snapshot restore": func() (cli.Command, error) {
return &command.SnapshotRestoreCommand{
Ui: ui,
}, nil
},
"snapshot save": func() (cli.Command, error) {
return &command.SnapshotSaveCommand{
Ui: ui,
}, nil
},
"snapshot inspect": func() (cli.Command, error) {
return &command.SnapshotInspectCommand{
Ui: ui,
}, nil
},
"version": func() (cli.Command, error) {
return &command.VersionCommand{
HumanVersion: GetHumanVersion(),
HumanVersion: version.GetHumanVersion(),
Ui: ui,
}, nil
},

View File

@ -14,29 +14,30 @@ import (
"github.com/hashicorp/golang-lru"
)
// These must be kept in sync with the constants in command/agent/acl.go.
const (
// aclNotFound indicates there is no matching ACL
// aclNotFound indicates there is no matching ACL.
aclNotFound = "ACL not found"
// rootDenied is returned when attempting to resolve a root ACL
// rootDenied is returned when attempting to resolve a root ACL.
rootDenied = "Cannot resolve root ACL"
// permissionDenied is returned when an ACL based rejection happens
// permissionDenied is returned when an ACL based rejection happens.
permissionDenied = "Permission denied"
// aclDisabled is returned when ACL changes are not permitted
// since they are disabled.
// aclDisabled is returned when ACL changes are not permitted since they
// are disabled.
aclDisabled = "ACL support disabled"
// anonymousToken is the token ID we re-write to if there
// is no token ID provided
// anonymousToken is the token ID we re-write to if there is no token ID
// provided.
anonymousToken = "anonymous"
// redactedToken is shown in structures with embedded tokens when they
// are not allowed to be displayed
// are not allowed to be displayed.
redactedToken = "<hidden>"
// Maximum number of cached ACL entries
// Maximum number of cached ACL entries.
aclCacheSize = 10 * 1024
)
@ -61,7 +62,7 @@ func (s *Server) aclLocalFault(id string) (string, string, error) {
// Query the state store.
state := s.fsm.State()
_, acl, err := state.ACLGet(id)
_, acl, err := state.ACLGet(nil, id)
if err != nil {
return "", "", err
}
@ -264,6 +265,8 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str
// Check if we can used the cached policy
if cached != nil && cached.ETag == p.ETag {
if p.TTL > 0 {
// TODO (slackpad) - This seems like it's an unsafe
// write.
cached.Expires = time.Now().Add(p.TTL)
}
return cached.ACL, nil
@ -311,33 +314,55 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str
// aclFilter is used to filter results from our state store based on ACL rules
// configured for the provided token.
type aclFilter struct {
acl acl.ACL
logger *log.Logger
acl acl.ACL
logger *log.Logger
enforceVersion8 bool
}
// newAclFilter constructs a new aclFilter.
func newAclFilter(acl acl.ACL, logger *log.Logger) *aclFilter {
func newAclFilter(acl acl.ACL, logger *log.Logger, enforceVersion8 bool) *aclFilter {
if logger == nil {
logger = log.New(os.Stdout, "", log.LstdFlags)
}
return &aclFilter{acl, logger}
return &aclFilter{
acl: acl,
logger: logger,
enforceVersion8: enforceVersion8,
}
}
// filterService is used to determine if a service is accessible for an ACL.
func (f *aclFilter) filterService(service string) bool {
// allowNode is used to determine if a node is accessible for an ACL.
func (f *aclFilter) allowNode(node string) bool {
if !f.enforceVersion8 {
return true
}
return f.acl.NodeRead(node)
}
// allowService is used to determine if a service is accessible for an ACL.
func (f *aclFilter) allowService(service string) bool {
if service == "" || service == ConsulServiceID {
return true
}
return f.acl.ServiceRead(service)
}
// allowSession is used to determine if a session for a node is accessible for
// an ACL.
func (f *aclFilter) allowSession(node string) bool {
if !f.enforceVersion8 {
return true
}
return f.acl.SessionRead(node)
}
// filterHealthChecks is used to filter a set of health checks down based on
// the configured ACL rules for a token.
func (f *aclFilter) filterHealthChecks(checks *structs.HealthChecks) {
hc := *checks
for i := 0; i < len(hc); i++ {
check := hc[i]
if f.filterService(check.ServiceName) {
if f.allowNode(check.Node) && f.allowService(check.ServiceName) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping check %q from result due to ACLs", check.CheckID)
@ -350,7 +375,7 @@ func (f *aclFilter) filterHealthChecks(checks *structs.HealthChecks) {
// filterServices is used to filter a set of services based on ACLs.
func (f *aclFilter) filterServices(services structs.Services) {
for svc, _ := range services {
if f.filterService(svc) {
if f.allowService(svc) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping service %q from result due to ACLs", svc)
@ -364,7 +389,7 @@ func (f *aclFilter) filterServiceNodes(nodes *structs.ServiceNodes) {
sn := *nodes
for i := 0; i < len(sn); i++ {
node := sn[i]
if f.filterService(node.ServiceName) {
if f.allowNode(node.Node) && f.allowService(node.ServiceName) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping node %q from result due to ACLs", node.Node)
@ -375,13 +400,22 @@ func (f *aclFilter) filterServiceNodes(nodes *structs.ServiceNodes) {
}
// filterNodeServices is used to filter services on a given node base on ACLs.
func (f *aclFilter) filterNodeServices(services *structs.NodeServices) {
for svc, _ := range services.Services {
if f.filterService(svc) {
func (f *aclFilter) filterNodeServices(services **structs.NodeServices) {
if *services == nil {
return
}
if !f.allowNode((*services).Node.Node) {
*services = nil
return
}
for svc, _ := range (*services).Services {
if f.allowService(svc) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping service %q from result due to ACLs", svc)
delete(services.Services, svc)
delete((*services).Services, svc)
}
}
@ -390,7 +424,7 @@ func (f *aclFilter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) {
csn := *nodes
for i := 0; i < len(csn); i++ {
node := csn[i]
if f.filterService(node.Service.Service) {
if f.allowNode(node.Node.Node) && f.allowService(node.Service.Service) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping node %q from result due to ACLs", node.Node.Node)
@ -400,6 +434,37 @@ func (f *aclFilter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) {
*nodes = csn
}
// filterSessions is used to filter a set of sessions based on ACLs.
func (f *aclFilter) filterSessions(sessions *structs.Sessions) {
s := *sessions
for i := 0; i < len(s); i++ {
session := s[i]
if f.allowSession(session.Node) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping session %q from result due to ACLs", session.ID)
s = append(s[:i], s[i+1:]...)
i--
}
*sessions = s
}
// filterCoordinates is used to filter nodes in a coordinate dump based on ACL
// rules.
func (f *aclFilter) filterCoordinates(coords *structs.Coordinates) {
c := *coords
for i := 0; i < len(c); i++ {
node := c[i].Node
if f.allowNode(node) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping node %q from result due to ACLs", node)
c = append(c[:i], c[i+1:]...)
i--
}
*coords = c
}
// filterNodeDump is used to filter through all parts of a node dump and
// remove elements the provided ACL token cannot access.
func (f *aclFilter) filterNodeDump(dump *structs.NodeDump) {
@ -407,31 +472,55 @@ func (f *aclFilter) filterNodeDump(dump *structs.NodeDump) {
for i := 0; i < len(nd); i++ {
info := nd[i]
// Filter nodes
if node := info.Node; !f.allowNode(node) {
f.logger.Printf("[DEBUG] consul: dropping node %q from result due to ACLs", node)
nd = append(nd[:i], nd[i+1:]...)
i--
continue
}
// Filter services
for i := 0; i < len(info.Services); i++ {
svc := info.Services[i].Service
if f.filterService(svc) {
for j := 0; j < len(info.Services); j++ {
svc := info.Services[j].Service
if f.allowService(svc) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping service %q from result due to ACLs", svc)
info.Services = append(info.Services[:i], info.Services[i+1:]...)
i--
info.Services = append(info.Services[:j], info.Services[j+1:]...)
j--
}
// Filter checks
for i := 0; i < len(info.Checks); i++ {
chk := info.Checks[i]
if f.filterService(chk.ServiceName) {
for j := 0; j < len(info.Checks); j++ {
chk := info.Checks[j]
if f.allowService(chk.ServiceName) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping check %q from result due to ACLs", chk.CheckID)
info.Checks = append(info.Checks[:i], info.Checks[i+1:]...)
i--
info.Checks = append(info.Checks[:j], info.Checks[j+1:]...)
j--
}
}
*dump = nd
}
// filterNodes is used to filter through all parts of a node list and remove
// elements the provided ACL token cannot access.
func (f *aclFilter) filterNodes(nodes *structs.Nodes) {
n := *nodes
for i := 0; i < len(n); i++ {
node := n[i].Node
if f.allowNode(node) {
continue
}
f.logger.Printf("[DEBUG] consul: dropping node %q from result due to ACLs", node)
n = append(n[:i], n[i+1:]...)
i--
}
*nodes = n
}
// redactPreparedQueryTokens will redact any tokens unless the client has a
// management token. This eases the transition to delegated authority over
// prepared queries, since it was easy to capture management tokens in Consul
@ -506,32 +595,39 @@ func (s *Server) filterACL(token string, subj interface{}) error {
}
// Create the filter
filt := newAclFilter(acl, s.logger)
filt := newAclFilter(acl, s.logger, s.config.ACLEnforceVersion8)
switch v := subj.(type) {
case *structs.IndexedHealthChecks:
filt.filterHealthChecks(&v.HealthChecks)
case *structs.IndexedServices:
filt.filterServices(v.Services)
case *structs.IndexedServiceNodes:
filt.filterServiceNodes(&v.ServiceNodes)
case *structs.IndexedNodeServices:
if v.NodeServices != nil {
filt.filterNodeServices(v.NodeServices)
}
case *structs.CheckServiceNodes:
filt.filterCheckServiceNodes(v)
case *structs.IndexedCheckServiceNodes:
filt.filterCheckServiceNodes(&v.Nodes)
case *structs.CheckServiceNodes:
filt.filterCheckServiceNodes(v)
case *structs.IndexedCoordinates:
filt.filterCoordinates(&v.Coordinates)
case *structs.IndexedHealthChecks:
filt.filterHealthChecks(&v.HealthChecks)
case *structs.IndexedNodeDump:
filt.filterNodeDump(&v.Dump)
case *structs.IndexedNodes:
filt.filterNodes(&v.Nodes)
case *structs.IndexedNodeServices:
filt.filterNodeServices(&v.NodeServices)
case *structs.IndexedServiceNodes:
filt.filterServiceNodes(&v.ServiceNodes)
case *structs.IndexedServices:
filt.filterServices(v.Services)
case *structs.IndexedSessions:
filt.filterSessions(&v.Sessions)
case *structs.IndexedPreparedQueries:
filt.filterPreparedQueries(&v.Queries)
@ -544,3 +640,149 @@ func (s *Server) filterACL(token string, subj interface{}) error {
return nil
}
// vetRegisterWithACL applies the given ACL's policy to the catalog update and
// determines if it is allowed. Since the catalog register request is so
// dynamic, this is a pretty complex algorithm and was worth breaking out of the
// endpoint. The NodeServices record for the node must be supplied, and can be
// nil.
//
// This is a bit racy because we have to check the state store outside of a
// transaction. It's the best we can do because we don't want to flow ACL
// checking down there. The node information doesn't change in practice, so this
// will be fine. If we expose ways to change node addresses in a later version,
// then we should split the catalog API at the node and service level so we can
// address this race better (even then it would be super rare, and would at
// worst let a service update revert a recent node update, so it doesn't open up
// too much abuse).
func vetRegisterWithACL(acl acl.ACL, subj *structs.RegisterRequest,
ns *structs.NodeServices) error {
// Fast path if ACLs are not enabled.
if acl == nil {
return nil
}
// Vet the node info. This allows service updates to re-post the required
// node info for each request without having to have node "write"
// privileges.
needsNode := ns == nil || subj.ChangesNode(ns.Node)
if needsNode && !acl.NodeWrite(subj.Node) {
return permissionDeniedErr
}
// Vet the service change. This includes making sure they can register
// the given service, and that we can write to any existing service that
// is being modified by id (if any).
if subj.Service != nil {
if !acl.ServiceWrite(subj.Service.Service) {
return permissionDeniedErr
}
if ns != nil {
other, ok := ns.Services[subj.Service.ID]
if ok && !acl.ServiceWrite(other.Service) {
return permissionDeniedErr
}
}
}
// Make sure that the member was flattened before we got there. This
// keeps us from having to verify this check as well.
if subj.Check != nil {
return fmt.Errorf("check member must be nil")
}
// Vet the checks. Node-level checks require node write, and
// service-level checks require service write.
for _, check := range subj.Checks {
// Make sure that the node matches - we don't allow you to mix
// checks from other nodes because we'd have to pull a bunch
// more state store data to check this. If ACLs are enabled then
// we simply require them to match in a given request. There's a
// note in state_store.go to ban this down there in Consul 0.8,
// but it's good to leave this here because it's required for
// correctness wrt. ACLs.
if check.Node != subj.Node {
return fmt.Errorf("Node '%s' for check '%s' doesn't match register request node '%s'",
check.Node, check.CheckID, subj.Node)
}
// Node-level check.
if check.ServiceID == "" {
if !acl.NodeWrite(subj.Node) {
return permissionDeniedErr
}
continue
}
// Service-level check, check the common case where it
// matches the service part of this request, which has
// already been vetted above, and might be being registered
// along with its checks.
if subj.Service != nil && subj.Service.ID == check.ServiceID {
continue
}
// Service-level check for some other service. Make sure they've
// got write permissions for that service.
if ns == nil {
return fmt.Errorf("Unknown service '%s' for check '%s'",
check.ServiceID, check.CheckID)
} else {
other, ok := ns.Services[check.ServiceID]
if !ok {
return fmt.Errorf("Unknown service '%s' for check '%s'",
check.ServiceID, check.CheckID)
}
if !acl.ServiceWrite(other.Service) {
return permissionDeniedErr
}
}
}
return nil
}
// vetDeregisterWithACL applies the given ACL's policy to the catalog update and
// determines if it is allowed. Since the catalog deregister request is so
// dynamic, this is a pretty complex algorithm and was worth breaking out of the
// endpoint. The NodeService for the referenced service must be supplied, and can
// be nil; similar for the HealthCheck for the referenced health check.
func vetDeregisterWithACL(acl acl.ACL, subj *structs.DeregisterRequest,
ns *structs.NodeService, nc *structs.HealthCheck) error {
// Fast path if ACLs are not enabled.
if acl == nil {
return nil
}
// This order must match the code in applyRegister() in fsm.go since it
// also evaluates things in this order, and will ignore fields based on
// this precedence. This lets us also ignore them from an ACL perspective.
if subj.ServiceID != "" {
if ns == nil {
return fmt.Errorf("Unknown service '%s'", subj.ServiceID)
}
if !acl.ServiceWrite(ns.Service) {
return permissionDeniedErr
}
} else if subj.CheckID != "" {
if nc == nil {
return fmt.Errorf("Unknown check '%s'", subj.CheckID)
}
if nc.ServiceID != "" {
if !acl.ServiceWrite(nc.ServiceName) {
return permissionDeniedErr
}
} else {
if !acl.NodeWrite(subj.Node) {
return permissionDeniedErr
}
}
} else {
if !acl.NodeWrite(subj.Node) {
return permissionDeniedErr
}
}
return nil
}

View File

@ -6,7 +6,9 @@ import (
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/consul/state"
"github.com/hashicorp/consul/consul/structs"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-uuid"
)
@ -108,7 +110,7 @@ func (a *ACL) Apply(args *structs.ACLRequest, reply *string) error {
return err
}
_, acl, err := state.ACLGet(args.ACL.ID)
_, acl, err := state.ACLGet(nil, args.ACL.ID)
if err != nil {
a.srv.logger.Printf("[ERR] consul.acl: ACL lookup failed: %v", err)
return err
@ -144,13 +146,10 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest,
return fmt.Errorf(aclDisabled)
}
// Get the local state
state := a.srv.fsm.State()
return a.srv.blockingRPC(&args.QueryOptions,
return a.srv.blockingQuery(&args.QueryOptions,
&reply.QueryMeta,
state.GetQueryWatch("ACLGet"),
func() error {
index, acl, err := state.ACLGet(args.ACL)
func(ws memdb.WatchSet, state *state.StateStore) error {
index, acl, err := state.ACLGet(ws, args.ACL)
if err != nil {
return err
}
@ -224,13 +223,10 @@ func (a *ACL) List(args *structs.DCSpecificRequest,
return permissionDeniedErr
}
// Get the local state
state := a.srv.fsm.State()
return a.srv.blockingRPC(&args.QueryOptions,
return a.srv.blockingQuery(&args.QueryOptions,
&reply.QueryMeta,
state.GetQueryWatch("ACLList"),
func() error {
index, acls, err := state.ACLList()
func(ws memdb.WatchSet, state *state.StateStore) error {
index, acls, err := state.ACLList(ws)
if err != nil {
return err
}

View File

@ -41,7 +41,7 @@ func TestACLEndpoint_Apply(t *testing.T) {
// Verify
state := s1.fsm.State()
_, s, err := state.ACLGet(out)
_, s, err := state.ACLGet(nil, out)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -63,7 +63,7 @@ func TestACLEndpoint_Apply(t *testing.T) {
}
// Verify
_, s, err = state.ACLGet(id)
_, s, err = state.ACLGet(nil, id)
if err != nil {
t.Fatalf("err: %v", err)
}
@ -182,7 +182,7 @@ func TestACLEndpoint_Apply_CustomID(t *testing.T) {
// Verify
state := s1.fsm.State()
_, s, err := state.ACLGet(out)
_, s, err := state.ACLGet(nil, out)
if err != nil {
t.Fatalf("err: %v", err)
}

View File

@ -139,7 +139,7 @@ func reconcileACLs(local, remote structs.ACLs, lastRemoteIndex uint64) structs.A
// FetchLocalACLs returns the ACLs in the local state store.
func (s *Server) fetchLocalACLs() (structs.ACLs, error) {
_, local, err := s.fsm.State().ACLList()
_, local, err := s.fsm.State().ACLList(nil)
if err != nil {
return nil, err
}

View File

@ -364,11 +364,11 @@ func TestACLReplication(t *testing.T) {
}
checkSame := func() (bool, error) {
index, remote, err := s1.fsm.State().ACLList()
index, remote, err := s1.fsm.State().ACLList(nil)
if err != nil {
return false, err
}
_, local, err := s2.fsm.State().ACLList()
_, local, err := s2.fsm.State().ACLList(nil)
if err != nil {
return false, err
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More