mirror of https://github.com/status-im/consul.git
Merge branch 'master' into f-cli-rework
This commit is contained in:
commit
3d09fb880f
|
@ -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
|
||||
|
|
96
CHANGELOG.md
96
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
|
31
GNUmakefile
31
GNUmakefile
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
444
acl/acl.go
444
acl/acl.go
|
@ -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)
|
||||
}
|
||||
|
|
469
acl/acl_test.go
469
acl/acl_test.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
60
api/agent.go
60
api/agent.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
46
api/api.go
46
api/api.go
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
29
api/kv.go
29
api/kv.go
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
125
bench/bench.json
125
bench/bench.json
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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).
|
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 678 KiB |
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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/")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
47
commands.go
47
commands.go
|
@ -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
|
||||
},
|
||||
|
|
342
consul/acl.go
342
consul/acl.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
1079
consul/acl_test.go
1079
consul/acl_test.go
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
Loading…
Reference in New Issue