Merge branch 'master' into 6074-allow-config-MaxHeaderBytes
This commit is contained in:
commit
ed719c978b
|
@ -0,0 +1,4 @@
|
|||
```release-note:bug
|
||||
api: Fixed a bug where the Check.GRPCUseTLS field could not be set using snake case.
|
||||
```
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
```release-note:improvement
|
||||
api: The `v1/connect/ca/roots` endpoint now accepts a `pem=true` query parameter and will return a PEM encoded certificate chain of
|
||||
all the certificates that would normally be in the JSON version of the response.
|
||||
```
|
||||
api: The `v1/connect/ca/roots` endpoint now accepts a `pem=true` query parameter and will return a PEM encoded certificate chain of all the certificates that would normally be in the JSON version of the response.
|
||||
```
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
connect: fix connect sidecars registered via the API not being automatically deregistered with their parent service after an agent restart by persisting the LocallyRegisteredAsSidecar property.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: add dashboard_url_template config option for external dashboard links
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
cli: snapshot inspect command supports JSON output
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
server: break up Intention.Apply monolithic method
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: only show topology tab for services that exist
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:security
|
||||
Fix Consul Enterprise Namespace Config Entry Replication DoS. Previously an operator with service:write ACL permissions in a Consul Enterprise cluster could write a malicious config entry that caused infinite raft writes due to issues with the namespace replication logic. [[CVE-2020-25201](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-25201)]
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
agent: add path_allowlist config option to restrict metrics proxy queries
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
```release-note:bug
|
||||
ui: hide metrics for ingress gateways until full support can be implemented
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
telemetry: add initialization and definition for non-expiring key metrics in Prometheus
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
cli: snapshot inspect command provides KV usage breakdown
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
agent: protect the metrics proxy behind ACLs
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
agent: return the default ACL policy to callers as a header
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
```release-note:bug
|
||||
autopilot: **(Enterprise Only)** Previously servers in other zones would not be promoted when all servers in a second zone had failed. Now the actual behavior matches the docs and autopilot will promote a healthy non-voter from any zone to replace failure of an entire zone.
|
||||
```
|
||||
```release-note:feature
|
||||
autopilot: A new `/v1/operator/autopilot/state` HTTP API was created to give greater visibility into what autopilot is doing and how it has classified all the servers it is tracking.
|
||||
```
|
||||
```release-note:improvement
|
||||
autopilot: **(Enterprise Only)** Autopilot now supports using both Redundancy Zones and Automated Upgrades together.
|
||||
```
|
||||
```release-note:breaking-change
|
||||
raft: Raft protocol v2 is no longer supported. If currently using protocol v2 then an intermediate upgrade to a version supporting both v2 and v3 protocols will be necessary (1.0.0 - 1.8.x). Note that the Raft protocol configured with the `raft_protocol` setting and the Consul RPC protocol configured with the `protocol` setting and output by the `consul version` command are distinct and supported Consul RPC protocol versions are not altered.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:breaking-change
|
||||
connect: Switch the default gateway port from 443 to 8443 to avoid assumption of Envoy running as root.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
chore: update to Go 1.14.11 with mitigation for [golang/go#42138](https://github.com/golang/go/issues/42138)
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
```release-note:improvement
|
||||
ui: Moves the Proxy health checks to be displayed with the Service health check under the Health Checks tab
|
||||
```
|
||||
|
||||
```release-note:improvement
|
||||
ui: Add the Upstreams and Exposed Paths tabs for services in mesh
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
autopilot: Added a new `consul operator autopilot state` command to retrieve and view the Autopilot state from consul.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
server: remove config entry CAS in legacy intention API bridge code
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
namespace: **(Enterprise Only)** Fixed a bug that could case snapshot restoration to fail when it contained a namespace marked for deletion while still containing other resources in that namespace.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:deprecation
|
||||
telemetry: the disable_compat_1.9 config will cover more metrics deprecations in future 1.9 point releases. These metrics will be emitted twice for backwards compatibility - if the flag is true, only the new metric name will be written.
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
```release-note:bug
|
||||
server: skip deleted and deleting namespaces when migrating intentions to config entries
|
||||
```
|
||||
|
||||
```release-note:breaking-change
|
||||
server: **(Enterprise only)** Pre-existing intentions defined with
|
||||
non-existent destination namespaces were non-functional and are erased during
|
||||
the upgrade process. This should not matter as these intentions had nothing to
|
||||
enforce.
|
||||
```
|
||||
|
||||
```release-note:breaking-change
|
||||
server: **(OSS only)** Pre-existing intentions defined with either a source or
|
||||
destination namespace value that is not "default" are rewritten or deleted
|
||||
during the upgrade process. Wildcards first attempt to downgrade to "default"
|
||||
unless an intention already exists, otherwise these non-functional intentions
|
||||
are deleted.
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
```release-note:deprecation
|
||||
cli: **(Enterprise only)** The `-non-voting-server` flag is deprecated in favor of the new `-read-replica` flag. The `-non-voting-server` flag is still present along side the new flag but it will be removed in a future release.
|
||||
```
|
||||
```release-note:improvement
|
||||
cli: **(Enterprise only)** A new `-read-replica` flag can now be used to enable running a server as a read only replica. Previously this was enabled with the now deprecated `-non-voting-server` flag.
|
||||
```
|
||||
```release-note:deprecation
|
||||
config: **(Enterprise only)** The `non_voting_server` configuration setting is deprecated in favor of the new `read_replica` setting. The `non_voting_server` configuration setting is still present but will be removed in a future release.
|
||||
```
|
||||
```release-note:improvement
|
||||
config: **(Enterprise only)** A new `read_replica` configuration setting can now be used to enable running a server as a read only replica. Previously this was enabled with the now deprecated `non_voting_server` setting.
|
||||
```
|
||||
```release-note:deprecation
|
||||
server: **(Enterprise only)** Addition of the `nonvoter` tag to the service registration made for read replicas is deprecated in favor of the new tag name of `read_replica`. Both are present in the registration but the `nonvoter` tag will be completely removed in a future release.
|
||||
```
|
||||
```release-note:deprecation
|
||||
gossip: **(Enterprise only)** Read replicas now advertise themselves by setting the `read_replica` tag. The old `nonvoter` tag is still present but is deprecated and will be removed in a future release.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
agent: All metrics should be present and available to prometheus scrapers when Consul starts. If any non-deprecated metrics are missing please submit an issue with its name.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
autopilot: Prevent panic when requesting the autopilot health immediately after a leader is elected.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:breaking-change
|
||||
connect: Update Envoy metrics names and labels for proxy listeners so that attributes like datacenter and namespace can be extracted.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
command: when generating envoy bootstrap configs use the datacenter returned from the agent services endpoint
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:security
|
||||
Increase the permissions to read from the `/connect/ca/configuration` endpoint to `operator:write`. Previously Connect CA configuration, including the private key, set via this endpoint could be read back by an operator with `operator:read` privileges. [CVE-2020-28053](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28053)
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
namespace: **(Enterprise Only)** Fixed an issue where namespaced services and checks were not being deleted when the containing namespace was deleted.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
license: **(Enterprise only)** Fixed an issue where warnings about Namespaces being unlicensed would be emitted erroneously.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:breaking-change
|
||||
sentinel: **(Consul Enterprise only)** update to v0.16.0, which replaces `whitelist` and `blacklist` with `allowlist` and `denylist`
|
||||
```
|
|
@ -3,15 +3,14 @@ version: 2
|
|||
|
||||
references:
|
||||
images:
|
||||
go: &GOLANG_IMAGE circleci/golang:1.14.9
|
||||
middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40
|
||||
ember: &EMBER_IMAGE circleci/node:12-browsers
|
||||
go: &GOLANG_IMAGE docker.mirror.hashicorp.services/circleci/golang:1.15.5
|
||||
ember: &EMBER_IMAGE docker.mirror.hashicorp.services/circleci/node:12-browsers
|
||||
|
||||
paths:
|
||||
test-results: &TEST_RESULTS_DIR /tmp/test-results
|
||||
|
||||
cache:
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v2-{{ checksum "ui-v2/yarn.lock" }}
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v3-{{ checksum "ui/yarn.lock" }}
|
||||
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
environment: &ENVIRONMENT
|
||||
|
@ -27,7 +26,7 @@ steps:
|
|||
install-gotestsum: &install-gotestsum
|
||||
name: install gotestsum
|
||||
environment:
|
||||
GOTESTSUM_RELEASE: 0.5.1
|
||||
GOTESTSUM_RELEASE: 0.6.0
|
||||
command: |
|
||||
url=https://github.com/gotestyourself/gotestsum/releases/download
|
||||
curl -sSL "${url}/v${GOTESTSUM_RELEASE}/gotestsum_${GOTESTSUM_RELEASE}_linux_amd64.tar.gz" | \
|
||||
|
@ -237,16 +236,15 @@ jobs:
|
|||
name: go test -race
|
||||
command: |
|
||||
mkdir -p $TEST_RESULTS_DIR /tmp/jsonfile
|
||||
pkgs="$(go list ./... | \
|
||||
grep -E -v '^github.com/hashicorp/consul/agent(/consul|/local|/xds|/routine-leak-checker)?$' | \
|
||||
grep -E -v '^github.com/hashicorp/consul/command/')"
|
||||
gotestsum \
|
||||
--format=short-verbose \
|
||||
--jsonfile /tmp/jsonfile/go-test-race.log \
|
||||
--junitfile $TEST_RESULTS_DIR/gotestsum-report.xml -- \
|
||||
-tags="$GOTAGS" -p 2 \
|
||||
-race -gcflags=all=-d=checkptr=0 \
|
||||
./agent/{ae,cache,cache-types,checks,config,pool,proxycfg,router}/... \
|
||||
./agent/consul/{authmethod,autopilot,fsm,state,stream}/... \
|
||||
./agent/{grpc,rpc,rpcclient,submatview}/... \
|
||||
./snapshot
|
||||
$pkgs
|
||||
|
||||
- store_test_results:
|
||||
path: *TEST_RESULTS_DIR
|
||||
|
@ -345,7 +343,7 @@ jobs:
|
|||
<<: *build-distros
|
||||
environment:
|
||||
<<: *build-env
|
||||
XC_OS: "darwin freebsd linux windows"
|
||||
XC_OS: "freebsd linux windows"
|
||||
XC_ARCH: "386"
|
||||
|
||||
# build all amd64 architecture supported OS binaries
|
||||
|
@ -440,7 +438,7 @@ jobs:
|
|||
# upload dev docker image
|
||||
dev-upload-docker:
|
||||
docker:
|
||||
- image: circleci/golang:latest # use a circleci image so the attach_workspace step works (has ca-certs installed)
|
||||
- image: *GOLANG_IMAGE # use a circleci image so the attach_workspace step works (has ca-certs installed)
|
||||
environment:
|
||||
<<: *ENVIRONMENT
|
||||
steps:
|
||||
|
@ -456,7 +454,7 @@ jobs:
|
|||
# Run integration tests on nomad/v0.8.7
|
||||
nomad-integration-0_8:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: docker.mirror.hashicorp.services/circleci/golang:1.10
|
||||
environment:
|
||||
<<: *ENVIRONMENT
|
||||
NOMAD_WORKING_DIR: &NOMAD_WORKING_DIR /go/src/github.com/hashicorp/nomad
|
||||
|
@ -505,7 +503,7 @@ jobs:
|
|||
|
||||
build-website-docker-image:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps
|
||||
- image: docker.mirror.hashicorp.services/circleci/buildpack-deps
|
||||
shell: /usr/bin/env bash -euo pipefail -c
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -531,7 +529,7 @@ jobs:
|
|||
|
||||
algolia-index:
|
||||
docker:
|
||||
- image: node:12
|
||||
- image: docker.mirror.hashicorp.services/node:12
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
|
@ -559,12 +557,13 @@ jobs:
|
|||
|
||||
- run:
|
||||
name: install yarn packages
|
||||
command: cd ui-v2 && yarn install
|
||||
command: cd ui/packages/consul-ui && yarn install --focus
|
||||
|
||||
- save_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
paths:
|
||||
- ui-v2/node_modules
|
||||
- ui/node_modules
|
||||
- ui/packages/consul-ui/node_modules
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# build ember so frontend tests run faster
|
||||
|
@ -578,13 +577,13 @@ jobs:
|
|||
- checkout
|
||||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- run: cd ui-v2 && make build-ci
|
||||
- run: cd ui/packages/consul-ui && make build-ci
|
||||
|
||||
# saves the build to a workspace to be passed to a downstream job
|
||||
- persist_to_workspace:
|
||||
root: ui-v2
|
||||
root: ui
|
||||
paths:
|
||||
- dist
|
||||
- packages/consul-ui/dist
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# build ember so frontend tests run faster
|
||||
|
@ -604,13 +603,13 @@ jobs:
|
|||
- checkout
|
||||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- run: cd ui-v2 && make
|
||||
- run: cd ui && make
|
||||
|
||||
# saves the build to a workspace to be passed to a downstream job
|
||||
- persist_to_workspace:
|
||||
root: ui-v2
|
||||
root: ui
|
||||
paths:
|
||||
- dist
|
||||
- packages/consul-ui/dist
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# build static-assets file
|
||||
|
@ -621,7 +620,7 @@ jobs:
|
|||
- checkout
|
||||
- attach_workspace:
|
||||
at: ./pkg
|
||||
- run: mv pkg/dist pkg/web_ui # 'make static-assets' looks for the 'pkg/web_ui' path
|
||||
- run: mv pkg/packages/consul-ui/dist pkg/web_ui # 'make static-assets' looks for the 'pkg/web_ui' path
|
||||
- run: make tools
|
||||
- run: make static-assets
|
||||
- persist_to_workspace:
|
||||
|
@ -644,10 +643,10 @@ jobs:
|
|||
- run:
|
||||
name: commit agent/uiserver/bindata_assetfs.go if there are UI changes
|
||||
command: |
|
||||
# check if there are any changes in ui-v2/
|
||||
# check if there are any changes in ui/
|
||||
# if there are, we commit the ui static asset file
|
||||
# HEAD^! is shorthand for HEAD^..HEAD (parent of HEAD and HEAD)
|
||||
if ! git diff --quiet --exit-code HEAD^! ui-v2/; then
|
||||
if ! git diff --quiet --exit-code HEAD^! ui/; then
|
||||
git config --local user.email "hashicorp-ci@users.noreply.github.com"
|
||||
git config --local user.name "hashicorp-ci"
|
||||
|
||||
|
@ -669,9 +668,9 @@ jobs:
|
|||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- attach_workspace:
|
||||
at: ui-v2
|
||||
at: ui
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
working_directory: ui/packages/consul-ui
|
||||
command: make test-node
|
||||
- run: *notify-slack-failure
|
||||
|
||||
|
@ -689,12 +688,12 @@ jobs:
|
|||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- attach_workspace:
|
||||
at: ui-v2
|
||||
at: ui
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
working_directory: ui/packages/consul-ui
|
||||
command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
|
||||
- store_test_results:
|
||||
path: ui-v2/test-results
|
||||
path: ui/packages/consul-ui/test-results
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# run ember frontend tests
|
||||
|
@ -711,12 +710,12 @@ jobs:
|
|||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- attach_workspace:
|
||||
at: ui-v2
|
||||
at: ui
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
working_directory: ui/packages/consul-ui
|
||||
command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
|
||||
- store_test_results:
|
||||
path: ui-v2/test-results
|
||||
path: ui/packages/consul-ui/test-results
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# run ember frontend unit tests to produce coverage report
|
||||
|
@ -728,23 +727,24 @@ jobs:
|
|||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- attach_workspace:
|
||||
at: ui-v2
|
||||
at: ui
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
working_directory: ui/packages/consul-ui
|
||||
command: make test-coverage-ci
|
||||
- run:
|
||||
name: codecov ui upload
|
||||
working_directory: ui-v2
|
||||
working_directory: ui/packages/consul-ui
|
||||
command: bash <(curl -s https://codecov.io/bash) -v -c -C $CIRCLE_SHA1 -F ui
|
||||
- run: *notify-slack-failure
|
||||
|
||||
envoy-integration-test-1.12.6:
|
||||
envoy-integration-test-1.13.6: &ENVOY_TESTS
|
||||
docker:
|
||||
# We only really need bash and docker-compose which is installed on all
|
||||
# Circle images but pick Go since we have to pick one of them.
|
||||
- image: *GOLANG_IMAGE
|
||||
parallelism: 2
|
||||
environment:
|
||||
ENVOY_VERSION: "1.12.6"
|
||||
ENVOY_VERSION: "1.13.6"
|
||||
steps: &ENVOY_INTEGRATION_TEST_STEPS
|
||||
- checkout
|
||||
# Get go binary from workspace
|
||||
|
@ -756,8 +756,12 @@ jobs:
|
|||
- run:
|
||||
name: Envoy Integration Tests
|
||||
command: |
|
||||
subtests=$(ls -d test/integration/connect/envoy/*/ | xargs -n 1 basename | circleci tests split)
|
||||
echo "Running $(echo $subtests | wc -w) subtests"
|
||||
echo "$subtests"
|
||||
subtests_pipe_sepr=$(echo "$subtests" | xargs | sed 's/ /|/g')
|
||||
mkdir -p /tmp/test-results/
|
||||
gotestsum -- -timeout=30m -tags integration ./test/integration/connect/envoy
|
||||
gotestsum -- -timeout=30m -tags integration ./test/integration/connect/envoy -run="TestEnvoy/($subtests_pipe_sepr)"
|
||||
environment:
|
||||
GOTESTSUM_JUNITFILE: /tmp/test-results/results.xml
|
||||
GOTESTSUM_FORMAT: standard-verbose
|
||||
|
@ -773,26 +777,20 @@ jobs:
|
|||
path: *TEST_RESULTS_DIR
|
||||
- run: *notify-slack-failure
|
||||
|
||||
envoy-integration-test-1.13.4:
|
||||
docker:
|
||||
- image: *GOLANG_IMAGE
|
||||
envoy-integration-test-1.14.5:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.13.4"
|
||||
steps: *ENVOY_INTEGRATION_TEST_STEPS
|
||||
ENVOY_VERSION: "1.14.5"
|
||||
|
||||
envoy-integration-test-1.14.4:
|
||||
docker:
|
||||
- image: *GOLANG_IMAGE
|
||||
envoy-integration-test-1.15.2:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.14.4"
|
||||
steps: *ENVOY_INTEGRATION_TEST_STEPS
|
||||
ENVOY_VERSION: "1.15.2"
|
||||
|
||||
envoy-integration-test-1.15.0:
|
||||
docker:
|
||||
- image: *GOLANG_IMAGE
|
||||
envoy-integration-test-1.16.0:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.15.0"
|
||||
steps: *ENVOY_INTEGRATION_TEST_STEPS
|
||||
ENVOY_VERSION: "1.16.0"
|
||||
|
||||
# run integration tests for the connect ca providers
|
||||
test-connect-ca-providers:
|
||||
|
@ -822,7 +820,7 @@ jobs:
|
|||
# only runs on master: checks latest commit to see if the PR associated has a backport/* or docs* label to cherry-pick
|
||||
cherry-picker:
|
||||
docker:
|
||||
- image: alpine:3.11
|
||||
- image: docker.mirror.hashicorp.services/alpine:3.12
|
||||
steps:
|
||||
- run: apk add --no-cache --no-progress git bash curl ncurses jq openssh-client
|
||||
- checkout
|
||||
|
@ -834,7 +832,7 @@ jobs:
|
|||
|
||||
trigger-oss-merge:
|
||||
docker:
|
||||
- image: alpine:3.11
|
||||
- image: docker.mirror.hashicorp.services/alpine:3.12
|
||||
steps:
|
||||
- run: apk add --no-cache --no-progress curl jq
|
||||
- run:
|
||||
|
@ -895,6 +893,7 @@ workflows:
|
|||
branches:
|
||||
only:
|
||||
- master
|
||||
- /release\/\d+\.\d+\.x$/
|
||||
requires:
|
||||
- build-static-assets
|
||||
- dev-build:
|
||||
|
@ -928,16 +927,16 @@ workflows:
|
|||
- nomad-integration-0_8:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1.12.6:
|
||||
- envoy-integration-test-1.13.6:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1.13.4:
|
||||
- envoy-integration-test-1.14.5:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1.14.4:
|
||||
- envoy-integration-test-1.15.2:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1.15.0:
|
||||
- envoy-integration-test-1.16.0:
|
||||
requires:
|
||||
- dev-build
|
||||
website:
|
||||
|
|
|
@ -131,6 +131,16 @@ fi
|
|||
# save PR number
|
||||
pr_number=$(echo "$resp" | jq '.items[].number')
|
||||
|
||||
# comment on the PR with the build number to make it easy to re-run the job when
|
||||
# cherry-pick labels are added in the future
|
||||
github_message=":cherries: If backport labels were added before merging, cherry-picking will start automatically.\n\nTo retroactively trigger a backport after merging, add backport labels and re-run ${CIRCLE_BUILD_URL}."
|
||||
curl -f -s -H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-X POST \
|
||||
-d "{ \"body\": \"${github_message}\"}" \
|
||||
"https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${pr_number}/comments"
|
||||
|
||||
|
||||
|
||||
# If the API returned a non-zero count, we have found a PR with that commit so we find
|
||||
# the labels from the PR
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ theme/testing:
|
|||
theme/tls:
|
||||
- '(mtls|mTLS|tls|TLS)'
|
||||
theme/ui:
|
||||
- '(ui|browser|chrome|firefox|IE|ie|Chrome)'
|
||||
- '(browser|chrome|firefox|IE|Chrome)'
|
||||
theme/windows:
|
||||
- '(windows|Windows|Microsoft|microsoft)'
|
||||
# thinking:
|
||||
|
|
|
@ -1,84 +1,63 @@
|
|||
# backport/1.6:
|
||||
# backport/1.7:
|
||||
# backport/1.8:
|
||||
# beta/1.8:
|
||||
# blocks-release:
|
||||
# crash:
|
||||
# do-not-merge:
|
||||
# help-wanted:
|
||||
# needs-discussion:
|
||||
# needs-investigation:
|
||||
# post-beta:
|
||||
pr/dependencies:
|
||||
- vendor/**/*
|
||||
- go.*
|
||||
# pr/needs-rebase:
|
||||
# pr/needs-tests:
|
||||
theme/acls:
|
||||
- acl/**/*
|
||||
theme/agent-cache:
|
||||
- agent/cache/**/*
|
||||
theme/api:
|
||||
- api/**/*
|
||||
theme/catalog:
|
||||
- agent/catalog/**/*
|
||||
theme/certificates:
|
||||
- tlsutil/**/*
|
||||
theme/cli:
|
||||
- command/**/*
|
||||
theme/config:
|
||||
- agent/config/**/*
|
||||
theme/connect:
|
||||
- connect/**/*
|
||||
- agent/connect/**/*
|
||||
# theme/consul-nomad:
|
||||
theme/consul-terraform-sync:
|
||||
- website/pages/docs/nia/**/*
|
||||
- website/pages/docs/integrate/nia*
|
||||
# theme/consul-vault:
|
||||
theme/contributing:
|
||||
- .github/**/*
|
||||
theme/dns:
|
||||
- dns/**/*
|
||||
theme/envoy/xds:
|
||||
- agent/xds/**/*
|
||||
# theme/federation-usability:
|
||||
theme/health-checks:
|
||||
- agent/health*
|
||||
- api/health*
|
||||
# theme/ingress-gw:
|
||||
# theme/internal-cleanup:
|
||||
theme/internals:
|
||||
- lib/**/*
|
||||
- types/**/*
|
||||
# theme/kubernetes:
|
||||
# theme/mesh-gw:
|
||||
# theme/operator-usability:
|
||||
# theme/performance:
|
||||
# theme/service-metadata:
|
||||
# theme/streaming:
|
||||
theme/telemetry:
|
||||
- logging/**/*
|
||||
# theme/terminating-gw:
|
||||
theme/testing:
|
||||
- ./*test*/**/*
|
||||
theme/tls:
|
||||
- tlsutil/**/*
|
||||
theme/ui:
|
||||
- ui-v2/**/*
|
||||
# theme/windows:
|
||||
# thinking:
|
||||
# type/bug:
|
||||
type/ci:
|
||||
- .circleci/*
|
||||
# type/crash:
|
||||
type/docs:
|
||||
- website/**/*
|
||||
# type/enhancement:
|
||||
# type/good-first-issue:
|
||||
# type/question:
|
||||
# type/umbrella-☂️:
|
||||
# version/0.8.3:
|
||||
# waiting-pr-merge:
|
||||
# waiting-reply:
|
||||
# waiting-reply-🤖:
|
||||
pr/dependencies:
|
||||
- vendor/**/*
|
||||
- go.*
|
||||
theme/acls:
|
||||
- acl/**/*
|
||||
theme/agent-cache:
|
||||
- agent/cache/**/*
|
||||
theme/api:
|
||||
- api/**/*
|
||||
theme/catalog:
|
||||
- agent/catalog/**/*
|
||||
theme/certificates:
|
||||
- tlsutil/**/*
|
||||
theme/cli:
|
||||
- command/**/*
|
||||
theme/config:
|
||||
- agent/config/**/*
|
||||
theme/connect:
|
||||
- connect/**/*
|
||||
- agent/connect/**/*
|
||||
# theme/consul-nomad:
|
||||
theme/consul-terraform-sync:
|
||||
- website/pages/docs/nia/**/*
|
||||
- website/pages/docs/integrate/nia*
|
||||
# theme/consul-vault:
|
||||
theme/contributing:
|
||||
- .github/**/*
|
||||
theme/dns:
|
||||
- dns/**/*
|
||||
theme/envoy/xds:
|
||||
- agent/xds/**/*
|
||||
# theme/federation-usability:
|
||||
theme/health-checks:
|
||||
- agent/health*
|
||||
- api/health*
|
||||
# theme/ingress-gw:
|
||||
# theme/internal-cleanup:
|
||||
theme/internals:
|
||||
- lib/**/*
|
||||
- types/**/*
|
||||
# theme/kubernetes:
|
||||
# theme/mesh-gw:
|
||||
# theme/operator-usability:
|
||||
# theme/performance:
|
||||
# theme/service-metadata:
|
||||
# theme/streaming:
|
||||
theme/telemetry:
|
||||
- logging/**/*
|
||||
# theme/terminating-gw:
|
||||
theme/testing:
|
||||
- ./*test*/**/*
|
||||
theme/tls:
|
||||
- tlsutil/**/*
|
||||
theme/ui:
|
||||
- ui/**/*
|
||||
# theme/windows:
|
||||
# thinking:
|
||||
# type/bug:
|
||||
type/ci:
|
||||
- .circleci/*
|
||||
# type/crash:
|
||||
type/docs:
|
||||
- website/**/*
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
name: "Issue Labeler"
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: github/issue-labeler@v2.2
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: .github/issue-labeler.yml
|
||||
not-before: "2020-09-14T08:23:00Z"
|
||||
enable-versioned-regex: 0
|
141
CHANGELOG.md
141
CHANGELOG.md
|
@ -1,5 +1,93 @@
|
|||
## UNRELEASED
|
||||
|
||||
## 1.9.0-rc1 (November 17, 2020)
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
||||
* connect: Update Envoy metrics names and labels for proxy listeners so that attributes like datacenter and namespace can be extracted. [[GH-9207](https://github.com/hashicorp/consul/issues/9207)]
|
||||
* server: **(Enterprise only)** Pre-existing intentions defined with
|
||||
non-existent destination namespaces were non-functional and are erased during
|
||||
the upgrade process. This should not matter as these intentions had nothing to
|
||||
enforce. [[GH-9186](https://github.com/hashicorp/consul/issues/9186)]
|
||||
* server: **(OSS only)** Pre-existing intentions defined with either a source or
|
||||
destination namespace value that is not "default" are rewritten or deleted
|
||||
during the upgrade process. Wildcards first attempt to downgrade to "default"
|
||||
unless an intention already exists, otherwise these non-functional intentions
|
||||
are deleted. [[GH-9186](https://github.com/hashicorp/consul/issues/9186)]
|
||||
|
||||
FEATURES:
|
||||
|
||||
* agent: return the default ACL policy to callers as a header [[GH-9101](https://github.com/hashicorp/consul/issues/9101)]
|
||||
* autopilot: Added a new `consul operator autopilot state` command to retrieve and view the Autopilot state from consul. [[GH-9142](https://github.com/hashicorp/consul/issues/9142)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* agent: All metrics should be present and available to prometheus scrapers when Consul starts. If any non-deprecated metrics are missing please submit an issue with its name. [[GH-9198](https://github.com/hashicorp/consul/issues/9198)]
|
||||
* server: break up Intention.Apply monolithic method [[GH-9007](https://github.com/hashicorp/consul/issues/9007)]
|
||||
* server: remove config entry CAS in legacy intention API bridge code [[GH-9151](https://github.com/hashicorp/consul/issues/9151)]
|
||||
* ui: Add the Upstreams and Exposed Paths tabs for services in mesh [[GH-9141](https://github.com/hashicorp/consul/issues/9141)]
|
||||
* ui: Moves the Proxy health checks to be displayed with the Service health check under the Health Checks tab [[GH-9141](https://github.com/hashicorp/consul/issues/9141)]
|
||||
|
||||
DEPRECATIONS:
|
||||
|
||||
* telemetry: the disable_compat_1.9 config will cover more metrics deprecations in future 1.9 point releases. These metrics will be emitted twice for backwards compatibility - if the flag is true, only the new metric name will be written. [[GH-9181](https://github.com/hashicorp/consul/issues/9181)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* autopilot: Prevent panic when requesting the autopilot health immediately after a leader is elected. [[GH-9204](https://github.com/hashicorp/consul/issues/9204)]
|
||||
* license: **(Enterprise only)** Fixed an issue where warnings about Namespaces being unlicensed would be emitted erroneously.
|
||||
* namespace: **(Enterprise Only)** Fixed a bug that could case snapshot restoration to fail when it contained a namespace marked for deletion while still containing other resources in that namespace. [[GH-9156](https://github.com/hashicorp/consul/issues/9156)]
|
||||
* namespace: **(Enterprise Only)** Fixed an issue where namespaced services and checks were not being deleted when the containing namespace was deleted.
|
||||
* server: skip deleted and deleting namespaces when migrating intentions to config entries [[GH-9186](https://github.com/hashicorp/consul/issues/9186)]
|
||||
|
||||
## 1.9.0-beta3 (November 10, 2020)
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
||||
* connect: Switch the default gateway port from 443 to 8443 to avoid assumption of Envoy running as root. [[GH-9113](https://github.com/hashicorp/consul/issues/9113)]
|
||||
* raft: Raft protocol v2 is no longer supported. If currently using protocol v2 then an intermediate upgrade to a version supporting both v2 and v3 protocols will be necessary (1.0.0 - 1.8.x). Note that the Raft protocol configured with the `raft_protocol` setting and the Consul RPC protocol configured with the `protocol` setting and output by the `consul version` command are distinct and supported Consul RPC protocol versions are not altered. [[GH-9103](https://github.com/hashicorp/consul/issues/9103)]
|
||||
|
||||
FEATURES:
|
||||
|
||||
* autopilot: A new `/v1/operator/autopilot/state` HTTP API was created to give greater visibility into what autopilot is doing and how it has classified all the servers it is tracking. [[GH-9103](https://github.com/hashicorp/consul/issues/9103)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* autopilot: **(Enterprise Only)** Autopilot now supports using both Redundancy Zones and Automated Upgrades together. [[GH-9103](https://github.com/hashicorp/consul/issues/9103)]
|
||||
* chore: update to Go 1.14.11 with mitigation for [golang/go#42138](https://github.com/golang/go/issues/42138) [[GH-9119](https://github.com/hashicorp/consul/issues/9119)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* autopilot: **(Enterprise Only)** Previously servers in other zones would not be promoted when all servers in a second zone had failed. Now the actual behavior matches the docs and autopilot will promote a healthy non-voter from any zone to replace failure of an entire zone. [[GH-9103](https://github.com/hashicorp/consul/issues/9103)]
|
||||
|
||||
## 1.9.0-beta2 (November 07, 2020)
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
||||
* sentinel: **(Consul Enterprise only)** update to v0.16.0, which replaces `whitelist` and `blacklist` with `allowlist` and `denylist`
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Fix Consul Enterprise Namespace Config Entry Replication DoS. Previously an operator with service:write ACL permissions in a Consul Enterprise cluster could write a malicious config entry that caused infinite raft writes due to issues with the namespace replication logic. [[CVE-2020-25201](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-25201)] [[GH-9024](https://github.com/hashicorp/consul/issues/9024)]
|
||||
|
||||
FEATURES:
|
||||
|
||||
* agent: Add a new RPC endpoint for streaming cluster state change events to clients.
|
||||
* telemetry: add initialization and definition for non-expiring key metrics in Prometheus [[GH-9088](https://github.com/hashicorp/consul/issues/9088)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* agent: add path_allowlist config option to restrict metrics proxy queries [[GH-9059](https://github.com/hashicorp/consul/issues/9059)]
|
||||
* agent: protect the metrics proxy behind ACLs [[GH-9099](https://github.com/hashicorp/consul/issues/9099)]
|
||||
* ui: add dashboard_url_template config option for external dashboard links [[GH-9002](https://github.com/hashicorp/consul/issues/9002)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* api: Fixed a bug where the Check.GRPCUseTLS field could not be set using snake case. [[GH-8771](https://github.com/hashicorp/consul/issues/8771)]
|
||||
* connect: fix connect sidecars registered via the API not being automatically deregistered with their parent service after an agent restart by persisting the LocallyRegisteredAsSidecar property. [[GH-8924](https://github.com/hashicorp/consul/issues/8924)]
|
||||
* ui: hide metrics for ingress gateways until full support can be implemented [[GH-9081](https://github.com/hashicorp/consul/issues/9081)]
|
||||
* ui: only show topology tab for services that exist [[GH-9008](https://github.com/hashicorp/consul/issues/9008)]
|
||||
|
||||
## 1.9.0-beta1 (October 12, 2020)
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
@ -57,6 +145,33 @@ BUG FIXES:
|
|||
* raft: (Enterprise only) properly update consul server meta non_voter for non-voting Enterprise Consul servers [[GH-8731](https://github.com/hashicorp/consul/issues/8731)]
|
||||
* ui: show correct datacenter for gateways [[GH-8704](https://github.com/hashicorp/consul/issues/8704)]
|
||||
|
||||
## 1.8.6 (November 19, 2020)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Increase the permissions to read from the `/connect/ca/configuration` endpoint to `operator:write`. Previously Connect CA configuration, including the private key, set via this endpoint could be read back by an operator with `operator:read` privileges. [CVE-2020-28053](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28053) [[GH-9240](https://github.com/hashicorp/consul/issues/9240)]
|
||||
|
||||
## 1.8.5 (October 23, 2020)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Fix Consul Enterprise Namespace Config Entry Replication DoS. Previously an operator with service:write ACL permissions in a Consul Enterprise cluster could write a malicious config entry that caused infinite raft writes due to issues with the namespace replication logic. [[CVE-2020-25201](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-25201)] [[GH-9024](https://github.com/hashicorp/consul/issues/9024)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* api: The `v1/connect/ca/roots` endpoint now accepts a `pem=true` query parameter and will return a PEM encoded certificate chain of all the certificates that would normally be in the JSON version of the response. [[GH-8774](https://github.com/hashicorp/consul/issues/8774)]
|
||||
* connect: The Vault provider will now automatically renew the lease of the token used, if supported. [[GH-8560](https://github.com/hashicorp/consul/issues/8560)]
|
||||
* connect: update supported envoy releases to 1.14.5, 1.13.6, 1.12.7, 1.11.2 for 1.8.x [[GH-8999](https://github.com/hashicorp/consul/issues/8999)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* agent: when enable_central_service_config is enabled ensure agent reload doesn't revert check state to critical [[GH-8747](https://github.com/hashicorp/consul/issues/8747)]
|
||||
* connect: Fixed an issue where the Vault intermediate was not renewed in the primary datacenter. [[GH-8784](https://github.com/hashicorp/consul/issues/8784)]
|
||||
* connect: fix Vault provider not respecting IntermediateCertTTL [[GH-8646](https://github.com/hashicorp/consul/issues/8646)]
|
||||
* connect: fix connect sidecars registered via the API not being automatically deregistered with their parent service after an agent restart by persisting the LocallyRegisteredAsSidecar property. [[GH-8924](https://github.com/hashicorp/consul/issues/8924)]
|
||||
* fixed a bug that caused logs to be flooded with `[WARN] agent.router: Non-server in server-only area` [[GH-8685](https://github.com/hashicorp/consul/issues/8685)]
|
||||
* ui: show correct datacenter for gateways [[GH-8704](https://github.com/hashicorp/consul/issues/8704)]
|
||||
|
||||
## 1.8.4 (September 11, 2020)
|
||||
|
||||
FEATURES:
|
||||
|
@ -202,6 +317,26 @@ BUGFIXES:
|
|||
* ui: Miscellaneous amends for Safari and Firefox [[GH-7904](https://github.com/hashicorp/consul/issues/7904)] [[GH-7907](https://github.com/hashicorp/consul/pull/7907)]
|
||||
* ui: Ensure a value is always passed to CONSUL_SSO_ENABLED [[GH-7913](https://github.com/hashicorp/consul/pull/7913)]
|
||||
|
||||
## 1.7.10 (November 19, 2020)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Increase the permissions to read from the `/connect/ca/configuration` endpoint to `operator:write`. Previously Connect CA configuration, including the private key, set via this endpoint could be read back by an operator with `operator:read` privileges. [CVE-2020-28053](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28053) [[GH-9240](https://github.com/hashicorp/consul/issues/9240)]
|
||||
|
||||
## 1.7.9 (October 26, 2020)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Fix Consul Enterprise Namespace Config Entry Replication DoS. Previously an operator with service:write ACL permissions in a Consul Enterprise cluster could write a malicious config entry that caused infinite raft writes due to issues with the namespace replication logic. [CVE-2020-25201] (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-25201) [[GH-9024](https://github.com/hashicorp/consul/issues/9024)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* connect: update supported envoy releases to 1.13.6, 1.12.7, 1.11.2, 1.10.0 for 1.7.x [[GH-9000](https://github.com/hashicorp/consul/issues/9000)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* agent: when enable_central_service_config is enabled ensure agent reload doesn't revert check state to critical [[GH-8747](https://github.com/hashicorp/consul/issues/8747)]
|
||||
|
||||
## 1.7.8 (September 11, 2020)
|
||||
|
||||
FEATURES:
|
||||
|
@ -437,6 +572,12 @@ BUGFIXES:
|
|||
* ui: Discovery-Chain: Improve parsing of redirects [[GH-7174](https://github.com/hashicorp/consul/pull/7174)]
|
||||
* ui: Fix styling of ‘duplicate intention’ error message [[GH6936]](https://github.com/hashicorp/consul/pull/6936)
|
||||
|
||||
## 1.6.10 (November 19, 2020)
|
||||
|
||||
SECURITY:
|
||||
|
||||
* Increase the permissions to read from the `/connect/ca/configuration` endpoint to `operator:write`. Previously Connect CA configuration, including the private key, set via this endpoint could be read back by an operator with `operator:read` privileges. [CVE-2020-28053](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28053) [[GH-9240](https://github.com/hashicorp/consul/issues/9240)]
|
||||
|
||||
## 1.6.9 (September 11, 2020)
|
||||
|
||||
BUG FIXES:
|
||||
|
|
112
acl/acl_test.go
112
acl/acl_test.go
|
@ -100,6 +100,10 @@ func checkAllowNodeRead(t *testing.T, authz Authorizer, prefix string, entCtx *A
|
|||
require.Equal(t, Allow, authz.NodeRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkAllowNodeReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Allow, authz.NodeReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkAllowNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Allow, authz.NodeWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -124,6 +128,10 @@ func checkAllowServiceRead(t *testing.T, authz Authorizer, prefix string, entCtx
|
|||
require.Equal(t, Allow, authz.ServiceRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkAllowServiceReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Allow, authz.ServiceReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkAllowServiceWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Allow, authz.ServiceWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -204,6 +212,10 @@ func checkDenyNodeRead(t *testing.T, authz Authorizer, prefix string, entCtx *Au
|
|||
require.Equal(t, Deny, authz.NodeRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkDenyNodeReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Deny, authz.NodeReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkDenyNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Deny, authz.NodeWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -228,6 +240,10 @@ func checkDenyServiceRead(t *testing.T, authz Authorizer, prefix string, entCtx
|
|||
require.Equal(t, Deny, authz.ServiceRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkDenyServiceReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Deny, authz.ServiceReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkDenyServiceWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Deny, authz.ServiceWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -308,6 +324,10 @@ func checkDefaultNodeRead(t *testing.T, authz Authorizer, prefix string, entCtx
|
|||
require.Equal(t, Default, authz.NodeRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkDefaultNodeReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Default, authz.NodeReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkDefaultNodeWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Default, authz.NodeWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -332,6 +352,10 @@ func checkDefaultServiceRead(t *testing.T, authz Authorizer, prefix string, entC
|
|||
require.Equal(t, Default, authz.ServiceRead(prefix, entCtx))
|
||||
}
|
||||
|
||||
func checkDefaultServiceReadAll(t *testing.T, authz Authorizer, _ string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Default, authz.ServiceReadAll(entCtx))
|
||||
}
|
||||
|
||||
func checkDefaultServiceWrite(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext) {
|
||||
require.Equal(t, Default, authz.ServiceWrite(prefix, entCtx))
|
||||
}
|
||||
|
@ -381,12 +405,14 @@ func TestACL(t *testing.T) {
|
|||
{name: "DenyKeyringWrite", check: checkDenyKeyringWrite},
|
||||
{name: "DenyKeyWrite", check: checkDenyKeyWrite},
|
||||
{name: "DenyNodeRead", check: checkDenyNodeRead},
|
||||
{name: "DenyNodeReadAll", check: checkDenyNodeReadAll},
|
||||
{name: "DenyNodeWrite", check: checkDenyNodeWrite},
|
||||
{name: "DenyOperatorRead", check: checkDenyOperatorRead},
|
||||
{name: "DenyOperatorWrite", check: checkDenyOperatorWrite},
|
||||
{name: "DenyPreparedQueryRead", check: checkDenyPreparedQueryRead},
|
||||
{name: "DenyPreparedQueryWrite", check: checkDenyPreparedQueryWrite},
|
||||
{name: "DenyServiceRead", check: checkDenyServiceRead},
|
||||
{name: "DenyServiceReadAll", check: checkDenyServiceReadAll},
|
||||
{name: "DenyServiceWrite", check: checkDenyServiceWrite},
|
||||
{name: "DenySessionRead", check: checkDenySessionRead},
|
||||
{name: "DenySessionWrite", check: checkDenySessionWrite},
|
||||
|
@ -411,12 +437,14 @@ func TestACL(t *testing.T) {
|
|||
{name: "AllowKeyringWrite", check: checkAllowKeyringWrite},
|
||||
{name: "AllowKeyWrite", check: checkAllowKeyWrite},
|
||||
{name: "AllowNodeRead", check: checkAllowNodeRead},
|
||||
{name: "AllowNodeReadAll", check: checkAllowNodeReadAll},
|
||||
{name: "AllowNodeWrite", check: checkAllowNodeWrite},
|
||||
{name: "AllowOperatorRead", check: checkAllowOperatorRead},
|
||||
{name: "AllowOperatorWrite", check: checkAllowOperatorWrite},
|
||||
{name: "AllowPreparedQueryRead", check: checkAllowPreparedQueryRead},
|
||||
{name: "AllowPreparedQueryWrite", check: checkAllowPreparedQueryWrite},
|
||||
{name: "AllowServiceRead", check: checkAllowServiceRead},
|
||||
{name: "AllowServiceReadAll", check: checkAllowServiceReadAll},
|
||||
{name: "AllowServiceWrite", check: checkAllowServiceWrite},
|
||||
{name: "AllowSessionRead", check: checkAllowSessionRead},
|
||||
{name: "AllowSessionWrite", check: checkAllowSessionWrite},
|
||||
|
@ -441,12 +469,14 @@ func TestACL(t *testing.T) {
|
|||
{name: "AllowKeyringWrite", check: checkAllowKeyringWrite},
|
||||
{name: "AllowKeyWrite", check: checkAllowKeyWrite},
|
||||
{name: "AllowNodeRead", check: checkAllowNodeRead},
|
||||
{name: "AllowNodeReadAll", check: checkAllowNodeReadAll},
|
||||
{name: "AllowNodeWrite", check: checkAllowNodeWrite},
|
||||
{name: "AllowOperatorRead", check: checkAllowOperatorRead},
|
||||
{name: "AllowOperatorWrite", check: checkAllowOperatorWrite},
|
||||
{name: "AllowPreparedQueryRead", check: checkAllowPreparedQueryRead},
|
||||
{name: "AllowPreparedQueryWrite", check: checkAllowPreparedQueryWrite},
|
||||
{name: "AllowServiceRead", check: checkAllowServiceRead},
|
||||
{name: "AllowServiceReadAll", check: checkAllowServiceReadAll},
|
||||
{name: "AllowServiceWrite", check: checkAllowServiceWrite},
|
||||
{name: "AllowSessionRead", check: checkAllowSessionRead},
|
||||
{name: "AllowSessionWrite", check: checkAllowSessionWrite},
|
||||
|
@ -995,6 +1025,7 @@ func TestACL(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
checks: []aclCheck{
|
||||
{name: "ReadAllDenied", prefix: "", check: checkDenyNodeReadAll},
|
||||
{name: "DefaultReadDenied", prefix: "nope", check: checkDenyNodeRead},
|
||||
{name: "DefaultWriteDenied", prefix: "nope", check: checkDenyNodeWrite},
|
||||
{name: "DenyReadDenied", prefix: "root-nope", check: checkDenyNodeRead},
|
||||
|
@ -1075,6 +1106,7 @@ func TestACL(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
checks: []aclCheck{
|
||||
{name: "ReadAllDenied", prefix: "", check: checkDenyNodeReadAll},
|
||||
{name: "DefaultReadAllowed", prefix: "nope", check: checkAllowNodeRead},
|
||||
{name: "DefaultWriteAllowed", prefix: "nope", check: checkAllowNodeWrite},
|
||||
{name: "DenyReadDenied", prefix: "root-nope", check: checkDenyNodeRead},
|
||||
|
@ -1335,6 +1367,7 @@ func TestACL(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
checks: []aclCheck{
|
||||
{name: "ServiceReadAllDenied", prefix: "", check: checkDenyServiceReadAll},
|
||||
{name: "KeyReadDenied", prefix: "other", check: checkDenyKeyRead},
|
||||
{name: "KeyWriteDenied", prefix: "other", check: checkDenyKeyWrite},
|
||||
{name: "KeyWritePrefixDenied", prefix: "other", check: checkDenyKeyWritePrefix},
|
||||
|
@ -1464,6 +1497,7 @@ func TestACL(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
checks: []aclCheck{
|
||||
{name: "ServiceReadAllDenied", prefix: "", check: checkDenyServiceReadAll},
|
||||
{name: "KeyReadAllowed", prefix: "other", check: checkAllowKeyRead},
|
||||
{name: "KeyWriteAllowed", prefix: "other", check: checkAllowKeyWrite},
|
||||
{name: "KeyWritePrefixAllowed", prefix: "other", check: checkAllowKeyWritePrefix},
|
||||
|
@ -1708,6 +1742,9 @@ func TestACL(t *testing.T) {
|
|||
},
|
||||
},
|
||||
checks: []aclCheck{
|
||||
{name: "NodeReadAllDenied", prefix: "", check: checkDenyNodeReadAll},
|
||||
{name: "ServiceReadAllDenied", prefix: "", check: checkDenyServiceReadAll},
|
||||
|
||||
{name: "AgentReadPrefixAllowed", prefix: "fo", check: checkAllowAgentRead},
|
||||
{name: "AgentWritePrefixDenied", prefix: "fo", check: checkDenyAgentWrite},
|
||||
{name: "AgentReadPrefixAllowed", prefix: "for", check: checkAllowAgentRead},
|
||||
|
@ -2101,3 +2138,78 @@ func TestACLEnforce(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_ReadAll(t *testing.T) {
|
||||
type testcase struct {
|
||||
name string
|
||||
rules string
|
||||
check func(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext)
|
||||
}
|
||||
|
||||
tests := []testcase{
|
||||
{
|
||||
name: "node:bar:read",
|
||||
rules: `node "bar" { policy = "read" }`,
|
||||
check: checkDenyNodeReadAll,
|
||||
},
|
||||
{
|
||||
name: "node:bar:write",
|
||||
rules: `node "bar" { policy = "write" }`,
|
||||
check: checkDenyNodeReadAll,
|
||||
},
|
||||
{
|
||||
name: "node:*:read",
|
||||
rules: `node_prefix "" { policy = "read" }`,
|
||||
check: checkAllowNodeReadAll,
|
||||
},
|
||||
{
|
||||
name: "node:*:write",
|
||||
rules: `node_prefix "" { policy = "write" }`,
|
||||
check: checkAllowNodeReadAll,
|
||||
},
|
||||
{
|
||||
name: "service:bar:read",
|
||||
rules: `service "bar" { policy = "read" }`,
|
||||
check: checkDenyServiceReadAll,
|
||||
},
|
||||
{
|
||||
name: "service:bar:write",
|
||||
rules: `service "bar" { policy = "write" }`,
|
||||
check: checkDenyServiceReadAll,
|
||||
},
|
||||
{
|
||||
name: "service:*:read",
|
||||
rules: `service_prefix "" { policy = "read" }`,
|
||||
check: checkAllowServiceReadAll,
|
||||
},
|
||||
{
|
||||
name: "service:*:write",
|
||||
rules: `service_prefix "" { policy = "write" }`,
|
||||
check: checkAllowServiceReadAll,
|
||||
},
|
||||
}
|
||||
|
||||
body := func(t *testing.T, rules string, defaultPolicy Authorizer, check func(t *testing.T, authz Authorizer, prefix string, entCtx *AuthorizerContext)) {
|
||||
t.Helper()
|
||||
|
||||
policy, err := NewPolicyFromSource("", 0, rules, SyntaxCurrent, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
acl, err := NewPolicyAuthorizerWithDefaults(defaultPolicy, []*Policy{policy}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
check(t, acl, "", nil)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("default deny", func(t *testing.T) {
|
||||
body(t, tc.rules, DenyAll(), tc.check)
|
||||
})
|
||||
t.Run("default allow", func(t *testing.T) {
|
||||
body(t, tc.rules, AllowAll(), checkAllowNodeReadAll)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,6 +107,9 @@ type Authorizer interface {
|
|||
// NodeRead checks for permission to read (discover) a given node.
|
||||
NodeRead(string, *AuthorizerContext) EnforcementDecision
|
||||
|
||||
// NodeReadAll checks for permission to read (discover) all nodes.
|
||||
NodeReadAll(*AuthorizerContext) EnforcementDecision
|
||||
|
||||
// NodeWrite checks for permission to create or update (register) a
|
||||
// given node.
|
||||
NodeWrite(string, *AuthorizerContext) EnforcementDecision
|
||||
|
@ -130,6 +133,9 @@ type Authorizer interface {
|
|||
// ServiceRead checks for permission to read a given service
|
||||
ServiceRead(string, *AuthorizerContext) EnforcementDecision
|
||||
|
||||
// ServiceReadAll checks for permission to read all services
|
||||
ServiceReadAll(*AuthorizerContext) EnforcementDecision
|
||||
|
||||
// ServiceWrite checks for permission to create or update a given
|
||||
// service
|
||||
ServiceWrite(string, *AuthorizerContext) EnforcementDecision
|
||||
|
|
|
@ -12,6 +12,8 @@ type mockAuthorizer struct {
|
|||
mock.Mock
|
||||
}
|
||||
|
||||
var _ Authorizer = (*mockAuthorizer)(nil)
|
||||
|
||||
// ACLRead checks for permission to list all the ACLs
|
||||
func (m *mockAuthorizer) ACLRead(ctx *AuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
|
@ -115,6 +117,11 @@ func (m *mockAuthorizer) NodeRead(segment string, ctx *AuthorizerContext) Enforc
|
|||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
func (m *mockAuthorizer) NodeReadAll(ctx *AuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// NodeWrite checks for permission to create or update (register) a
|
||||
// given node.
|
||||
func (m *mockAuthorizer) NodeWrite(segment string, ctx *AuthorizerContext) EnforcementDecision {
|
||||
|
@ -156,6 +163,11 @@ func (m *mockAuthorizer) ServiceRead(segment string, ctx *AuthorizerContext) Enf
|
|||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
func (m *mockAuthorizer) ServiceReadAll(ctx *AuthorizerContext) EnforcementDecision {
|
||||
ret := m.Called(ctx)
|
||||
return ret.Get(0).(EnforcementDecision)
|
||||
}
|
||||
|
||||
// ServiceWrite checks for permission to create or update a given
|
||||
// service
|
||||
func (m *mockAuthorizer) ServiceWrite(segment string, ctx *AuthorizerContext) EnforcementDecision {
|
||||
|
@ -183,8 +195,6 @@ func (m *mockAuthorizer) Snapshot(ctx *AuthorizerContext) EnforcementDecision {
|
|||
}
|
||||
|
||||
func TestACL_Enforce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
method string
|
||||
resource Resource
|
||||
|
|
|
@ -152,6 +152,12 @@ func (c *ChainedAuthorizer) NodeRead(node string, entCtx *AuthorizerContext) Enf
|
|||
})
|
||||
}
|
||||
|
||||
func (c *ChainedAuthorizer) NodeReadAll(entCtx *AuthorizerContext) EnforcementDecision {
|
||||
return c.executeChain(func(authz Authorizer) EnforcementDecision {
|
||||
return authz.NodeReadAll(entCtx)
|
||||
})
|
||||
}
|
||||
|
||||
// NodeWrite checks for permission to create or update (register) a
|
||||
// given node.
|
||||
func (c *ChainedAuthorizer) NodeWrite(node string, entCtx *AuthorizerContext) EnforcementDecision {
|
||||
|
@ -199,6 +205,12 @@ func (c *ChainedAuthorizer) ServiceRead(name string, entCtx *AuthorizerContext)
|
|||
})
|
||||
}
|
||||
|
||||
func (c *ChainedAuthorizer) ServiceReadAll(entCtx *AuthorizerContext) EnforcementDecision {
|
||||
return c.executeChain(func(authz Authorizer) EnforcementDecision {
|
||||
return authz.ServiceReadAll(entCtx)
|
||||
})
|
||||
}
|
||||
|
||||
// ServiceWrite checks for permission to create or update a given
|
||||
// service
|
||||
func (c *ChainedAuthorizer) ServiceWrite(name string, entCtx *AuthorizerContext) EnforcementDecision {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
|
||||
type testAuthorizer EnforcementDecision
|
||||
|
||||
var _ Authorizer = testAuthorizer(Allow)
|
||||
|
||||
func (authz testAuthorizer) ACLRead(*AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
|
@ -54,6 +56,9 @@ func (authz testAuthorizer) KeyringWrite(*AuthorizerContext) EnforcementDecision
|
|||
func (authz testAuthorizer) NodeRead(string, *AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
func (authz testAuthorizer) NodeReadAll(*AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
func (authz testAuthorizer) NodeWrite(string, *AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
|
@ -72,6 +77,9 @@ func (authz testAuthorizer) PreparedQueryWrite(string, *AuthorizerContext) Enfor
|
|||
func (authz testAuthorizer) ServiceRead(string, *AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
func (authz testAuthorizer) ServiceReadAll(*AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
func (authz testAuthorizer) ServiceWrite(string, *AuthorizerContext) EnforcementDecision {
|
||||
return EnforcementDecision(authz)
|
||||
}
|
||||
|
@ -86,11 +94,7 @@ func (authz testAuthorizer) Snapshot(*AuthorizerContext) EnforcementDecision {
|
|||
}
|
||||
|
||||
func TestChainedAuthorizer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("No Authorizers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz := NewChainedAuthorizer([]Authorizer{})
|
||||
checkDenyACLRead(t, authz, "foo", nil)
|
||||
checkDenyACLWrite(t, authz, "foo", nil)
|
||||
|
@ -121,8 +125,6 @@ func TestChainedAuthorizer(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Authorizer Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Default)})
|
||||
checkDenyACLRead(t, authz, "foo", nil)
|
||||
checkDenyACLWrite(t, authz, "foo", nil)
|
||||
|
@ -153,8 +155,6 @@ func TestChainedAuthorizer(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Authorizer No Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Allow)})
|
||||
checkAllowACLRead(t, authz, "foo", nil)
|
||||
checkAllowACLWrite(t, authz, "foo", nil)
|
||||
|
@ -185,8 +185,6 @@ func TestChainedAuthorizer(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("First Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz := NewChainedAuthorizer([]Authorizer{testAuthorizer(Deny), testAuthorizer(Allow)})
|
||||
checkDenyACLRead(t, authz, "foo", nil)
|
||||
checkDenyACLWrite(t, authz, "foo", nil)
|
||||
|
|
|
@ -350,7 +350,7 @@ type enforceCallback func(raw interface{}, prefixOnly bool) EnforcementDecision
|
|||
func anyAllowed(tree *radix.Tree, enforceFn enforceCallback) EnforcementDecision {
|
||||
decision := Default
|
||||
|
||||
// special case for handling a catch-all prefix rule. If the rule woul Deny access then our default decision
|
||||
// special case for handling a catch-all prefix rule. If the rule would Deny access then our default decision
|
||||
// should be to Deny, but this decision should still be overridable with other more specific rules.
|
||||
if raw, found := tree.Get(""); found {
|
||||
decision = enforceFn(raw, true)
|
||||
|
@ -686,6 +686,10 @@ func (p *policyAuthorizer) NodeRead(name string, _ *AuthorizerContext) Enforceme
|
|||
return Default
|
||||
}
|
||||
|
||||
func (p *policyAuthorizer) NodeReadAll(_ *AuthorizerContext) EnforcementDecision {
|
||||
return p.allAllowed(p.nodeRules, AccessRead)
|
||||
}
|
||||
|
||||
// NodeWrite checks if writing (registering) a node is allowed
|
||||
func (p *policyAuthorizer) NodeWrite(name string, _ *AuthorizerContext) EnforcementDecision {
|
||||
if rule, ok := getPolicy(name, p.nodeRules); ok {
|
||||
|
@ -720,6 +724,10 @@ func (p *policyAuthorizer) ServiceRead(name string, _ *AuthorizerContext) Enforc
|
|||
return Default
|
||||
}
|
||||
|
||||
func (p *policyAuthorizer) ServiceReadAll(_ *AuthorizerContext) EnforcementDecision {
|
||||
return p.allAllowed(p.serviceRules, AccessRead)
|
||||
}
|
||||
|
||||
// ServiceWrite checks if writing (registering) a service is allowed
|
||||
func (p *policyAuthorizer) ServiceWrite(name string, _ *AuthorizerContext) EnforcementDecision {
|
||||
if rule, ok := getPolicy(name, p.serviceRules); ok {
|
||||
|
|
|
@ -13,8 +13,6 @@ import (
|
|||
// ensure compatibility from version to version those tests have been only minimally altered. The tests in this
|
||||
// file are specific to the newer functionality.
|
||||
func TestPolicyAuthorizer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type aclCheck struct {
|
||||
name string
|
||||
prefix string
|
||||
|
@ -446,8 +444,6 @@ func TestPolicyAuthorizer(t *testing.T) {
|
|||
name := name
|
||||
tcase := tcase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz, err := NewPolicyAuthorizer([]*Policy{tcase.policy}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -458,7 +454,6 @@ func TestPolicyAuthorizer(t *testing.T) {
|
|||
}
|
||||
t.Run(checkName, func(t *testing.T) {
|
||||
check := check
|
||||
t.Parallel()
|
||||
|
||||
check.check(t, authz, check.prefix, nil)
|
||||
})
|
||||
|
@ -468,8 +463,6 @@ func TestPolicyAuthorizer(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAnyAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type radixInsertion struct {
|
||||
segment string
|
||||
value *policyAuthorizerRadixLeaf
|
||||
|
@ -719,8 +712,6 @@ func TestAnyAllowed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAllAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type radixInsertion struct {
|
||||
segment string
|
||||
value *policyAuthorizerRadixLeaf
|
||||
|
|
|
@ -142,6 +142,13 @@ func (s *staticAuthorizer) NodeRead(string, *AuthorizerContext) EnforcementDecis
|
|||
return Deny
|
||||
}
|
||||
|
||||
func (s *staticAuthorizer) NodeReadAll(*AuthorizerContext) EnforcementDecision {
|
||||
if s.defaultAllow {
|
||||
return Allow
|
||||
}
|
||||
return Deny
|
||||
}
|
||||
|
||||
func (s *staticAuthorizer) NodeWrite(string, *AuthorizerContext) EnforcementDecision {
|
||||
if s.defaultAllow {
|
||||
return Allow
|
||||
|
@ -184,6 +191,13 @@ func (s *staticAuthorizer) ServiceRead(string, *AuthorizerContext) EnforcementDe
|
|||
return Deny
|
||||
}
|
||||
|
||||
func (s *staticAuthorizer) ServiceReadAll(*AuthorizerContext) EnforcementDecision {
|
||||
if s.defaultAllow {
|
||||
return Allow
|
||||
}
|
||||
return Deny
|
||||
}
|
||||
|
||||
func (s *staticAuthorizer) ServiceWrite(string, *AuthorizerContext) EnforcementDecision {
|
||||
if s.defaultAllow {
|
||||
return Allow
|
||||
|
|
|
@ -5,11 +5,7 @@ import (
|
|||
)
|
||||
|
||||
func TestStaticAuthorizer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("AllowAll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
authz := AllowAll()
|
||||
checkDenyACLRead(t, authz, "foo", nil)
|
||||
checkDenyACLWrite(t, authz, "foo", nil)
|
||||
|
@ -40,7 +36,6 @@ func TestStaticAuthorizer(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("DenyAll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
authz := DenyAll()
|
||||
checkDenyACLRead(t, authz, "foo", nil)
|
||||
checkDenyACLWrite(t, authz, "foo", nil)
|
||||
|
@ -71,7 +66,6 @@ func TestStaticAuthorizer(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("ManageAll", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
authz := ManageAll()
|
||||
checkAllowACLRead(t, authz, "foo", nil)
|
||||
checkAllowACLWrite(t, authz, "foo", nil)
|
||||
|
|
|
@ -360,10 +360,16 @@ func New(bd BaseDeps) (*Agent, error) {
|
|||
}
|
||||
|
||||
cacheName := cachetype.HealthServicesName
|
||||
if bd.RuntimeConfig.CacheUseStreamingBackend {
|
||||
if bd.RuntimeConfig.UseStreamingBackend {
|
||||
cacheName = cachetype.StreamingHealthServicesName
|
||||
}
|
||||
a.rpcClientHealth = &health.Client{Cache: bd.Cache, NetRPC: &a, CacheName: cacheName}
|
||||
a.rpcClientHealth = &health.Client{
|
||||
Cache: bd.Cache,
|
||||
NetRPC: &a,
|
||||
CacheName: cacheName,
|
||||
// Temporarily until streaming supports all connect events
|
||||
CacheNameConnect: cachetype.HealthServicesName,
|
||||
}
|
||||
|
||||
a.serviceManager = NewServiceManager(&a)
|
||||
|
||||
|
@ -792,19 +798,7 @@ func (a *Agent) listenHTTP() ([]apiServer, error) {
|
|||
httpServer.ConnState = connLimitFn
|
||||
}
|
||||
|
||||
servers = append(servers, apiServer{
|
||||
Protocol: proto,
|
||||
Addr: l.Addr(),
|
||||
Shutdown: httpServer.Shutdown,
|
||||
Run: func() error {
|
||||
err := httpServer.Serve(l)
|
||||
if err == nil || err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s server %s failed: %w", proto, l.Addr(), err)
|
||||
},
|
||||
MaxHeaderBytes: a.config.HTTPMaxHeaderBytes,
|
||||
})
|
||||
servers = append(servers, newAPIServerHTTP(proto, l, httpServer))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1106,8 +1100,8 @@ func newConsulConfig(runtimeCfg *config.RuntimeConfig, logger hclog.Logger) (*co
|
|||
if runtimeCfg.SessionTTLMin != 0 {
|
||||
cfg.SessionTTLMin = runtimeCfg.SessionTTLMin
|
||||
}
|
||||
if runtimeCfg.NonVotingServer {
|
||||
cfg.NonVoter = runtimeCfg.NonVotingServer
|
||||
if runtimeCfg.ReadReplica {
|
||||
cfg.ReadReplica = runtimeCfg.ReadReplica
|
||||
}
|
||||
|
||||
// These are fully specified in the agent defaults, so we can simply
|
||||
|
@ -1723,6 +1717,11 @@ type persistedService struct {
|
|||
Token string
|
||||
Service *structs.NodeService
|
||||
Source string
|
||||
// whether this service was registered as a sidecar, see structs.NodeService
|
||||
// we store this field here because it is excluded from json serialization
|
||||
// to exclude it from API output, but we need it to properly deregister
|
||||
// persisted sidecars.
|
||||
LocallyRegisteredAsSidecar bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// persistService saves a service definition to a JSON file in the data dir
|
||||
|
@ -1731,9 +1730,10 @@ func (a *Agent) persistService(service *structs.NodeService, source configSource
|
|||
svcPath := filepath.Join(a.config.DataDir, servicesDir, svcID.StringHash())
|
||||
|
||||
wrapped := persistedService{
|
||||
Token: a.State.ServiceToken(service.CompoundServiceID()),
|
||||
Service: service,
|
||||
Source: source.String(),
|
||||
Token: a.State.ServiceToken(service.CompoundServiceID()),
|
||||
Service: service,
|
||||
Source: source.String(),
|
||||
LocallyRegisteredAsSidecar: service.LocallyRegisteredAsSidecar,
|
||||
}
|
||||
encoded, err := json.Marshal(wrapped)
|
||||
if err != nil {
|
||||
|
@ -3182,6 +3182,10 @@ func (a *Agent) loadServices(conf *config.RuntimeConfig, snap map[structs.CheckI
|
|||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Restore LocallyRegisteredAsSidecar, see persistedService.LocallyRegisteredAsSidecar
|
||||
p.Service.LocallyRegisteredAsSidecar = p.LocallyRegisteredAsSidecar
|
||||
|
||||
serviceID := p.Service.CompoundServiceID()
|
||||
|
||||
source, ok := ConfigSourceFromName(p.Source)
|
||||
|
|
|
@ -136,7 +136,7 @@ func (s *HTTPHandlers) AgentMetrics(resp http.ResponseWriter, req *http.Request)
|
|||
return nil, acl.ErrPermissionDenied
|
||||
}
|
||||
if enablePrometheusOutput(req) {
|
||||
if s.agent.config.Telemetry.PrometheusRetentionTime < 1 {
|
||||
if s.agent.config.Telemetry.PrometheusOpts.Expiration < 1 {
|
||||
resp.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
fmt.Fprint(resp, "Prometheus is not enabled since its retention time is not positive")
|
||||
return nil, nil
|
||||
|
@ -170,7 +170,7 @@ func (s *HTTPHandlers) AgentReload(resp http.ResponseWriter, req *http.Request)
|
|||
return nil, s.agent.ReloadConfig()
|
||||
}
|
||||
|
||||
func buildAgentService(s *structs.NodeService) api.AgentService {
|
||||
func buildAgentService(s *structs.NodeService, dc string) api.AgentService {
|
||||
weights := api.AgentWeights{Passing: 1, Warning: 1}
|
||||
if s.Weights != nil {
|
||||
if s.Weights.Passing > 0 {
|
||||
|
@ -200,6 +200,7 @@ func buildAgentService(s *structs.NodeService) api.AgentService {
|
|||
CreateIndex: s.CreateIndex,
|
||||
ModifyIndex: s.ModifyIndex,
|
||||
Weights: weights,
|
||||
Datacenter: dc,
|
||||
}
|
||||
|
||||
if as.Tags == nil {
|
||||
|
@ -253,9 +254,11 @@ func (s *HTTPHandlers) AgentServices(resp http.ResponseWriter, req *http.Request
|
|||
// anyway.
|
||||
agentSvcs := make(map[string]*api.AgentService)
|
||||
|
||||
dc := s.agent.config.Datacenter
|
||||
|
||||
// Use empty list instead of nil
|
||||
for id, s := range services {
|
||||
agentService := buildAgentService(s)
|
||||
agentService := buildAgentService(s, dc)
|
||||
agentSvcs[id.ID] = &agentService
|
||||
}
|
||||
|
||||
|
@ -303,6 +306,8 @@ func (s *HTTPHandlers) AgentService(resp http.ResponseWriter, req *http.Request)
|
|||
|
||||
sid := structs.NewServiceID(id, &entMeta)
|
||||
|
||||
dc := s.agent.config.Datacenter
|
||||
|
||||
resultHash, service, err := s.agent.LocalBlockingQuery(false, hash, queryOpts.MaxQueryTime,
|
||||
func(ws memdb.WatchSet) (string, interface{}, error) {
|
||||
|
||||
|
@ -330,7 +335,7 @@ func (s *HTTPHandlers) AgentService(resp http.ResponseWriter, req *http.Request)
|
|||
}
|
||||
|
||||
// Calculate the content hash over the response, minus the hash field
|
||||
aSvc := buildAgentService(svc)
|
||||
aSvc := buildAgentService(svc, dc)
|
||||
reply := &aSvc
|
||||
|
||||
rawHash, err := hashstructure.Hash(reply, nil)
|
||||
|
@ -768,6 +773,8 @@ func (s *HTTPHandlers) AgentHealthServiceByID(resp http.ResponseWriter, req *htt
|
|||
|
||||
sid := structs.NewServiceID(serviceID, &entMeta)
|
||||
|
||||
dc := s.agent.config.Datacenter
|
||||
|
||||
if service := s.agent.State.Service(sid); service != nil {
|
||||
if authz != nil && authz.ServiceRead(service.Service, &authzContext) != acl.Allow {
|
||||
return nil, acl.ErrPermissionDenied
|
||||
|
@ -776,7 +783,7 @@ func (s *HTTPHandlers) AgentHealthServiceByID(resp http.ResponseWriter, req *htt
|
|||
if returnTextPlain(req) {
|
||||
return status, CodeWithPayloadError{StatusCode: code, Reason: status, ContentType: "text/plain"}
|
||||
}
|
||||
serviceInfo := buildAgentService(service)
|
||||
serviceInfo := buildAgentService(service, dc)
|
||||
result := &api.AgentServiceChecksInfo{
|
||||
AggregatedStatus: status,
|
||||
Checks: healthChecks,
|
||||
|
@ -822,6 +829,8 @@ func (s *HTTPHandlers) AgentHealthServiceByName(resp http.ResponseWriter, req *h
|
|||
return nil, acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
dc := s.agent.config.Datacenter
|
||||
|
||||
code := http.StatusNotFound
|
||||
status := fmt.Sprintf("ServiceName %s Not Found", serviceName)
|
||||
services := s.agent.State.Services(&entMeta)
|
||||
|
@ -831,7 +840,7 @@ func (s *HTTPHandlers) AgentHealthServiceByName(resp http.ResponseWriter, req *h
|
|||
sid := structs.NewServiceID(service.ID, &entMeta)
|
||||
|
||||
scode, sstatus, healthChecks := agentHealthService(sid, s)
|
||||
serviceInfo := buildAgentService(service)
|
||||
serviceInfo := buildAgentService(service, dc)
|
||||
res := api.AgentServiceChecksInfo{
|
||||
AggregatedStatus: sstatus,
|
||||
Checks: healthChecks,
|
||||
|
|
|
@ -365,8 +365,9 @@ func TestAgent_Service(t *testing.T) {
|
|||
Passing: 1,
|
||||
Warning: 1,
|
||||
},
|
||||
Meta: map[string]string{},
|
||||
Tags: []string{},
|
||||
Meta: map[string]string{},
|
||||
Tags: []string{},
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
fillAgentServiceEnterpriseMeta(expectedResponse, structs.DefaultEnterpriseMeta())
|
||||
|
||||
|
@ -391,8 +392,9 @@ func TestAgent_Service(t *testing.T) {
|
|||
Port: 1818,
|
||||
},
|
||||
},
|
||||
Meta: map[string]string{},
|
||||
Tags: []string{},
|
||||
Meta: map[string]string{},
|
||||
Tags: []string{},
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
fillAgentServiceEnterpriseMeta(expectWebResponse, structs.DefaultEnterpriseMeta())
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
@ -23,12 +24,23 @@ import (
|
|||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/google/tcpproxy"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/serf/coordinate"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/time/rate"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||
"github.com/hashicorp/consul/agent/checks"
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||
"github.com/hashicorp/consul/ipaddr"
|
||||
|
@ -38,13 +50,8 @@ import (
|
|||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
"github.com/hashicorp/consul/types"
|
||||
"github.com/hashicorp/serf/coordinate"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/time/rate"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
func getService(a *TestAgent, id string) *structs.NodeService {
|
||||
|
@ -2499,6 +2506,75 @@ func TestAgent_PurgeCheckOnDuplicate(t *testing.T) {
|
|||
require.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestAgent_DeregisterPersistedSidecarAfterRestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
nodeID := NodeID()
|
||||
a := StartTestAgent(t, TestAgent{
|
||||
HCL: `
|
||||
node_id = "` + nodeID + `"
|
||||
node_name = "Node ` + nodeID + `"
|
||||
server = false
|
||||
bootstrap = false
|
||||
enable_central_service_config = false
|
||||
`})
|
||||
defer a.Shutdown()
|
||||
|
||||
srv := &structs.NodeService{
|
||||
ID: "svc",
|
||||
Service: "svc",
|
||||
Weights: &structs.Weights{
|
||||
Passing: 2,
|
||||
Warning: 1,
|
||||
},
|
||||
Tags: []string{"tag2"},
|
||||
Port: 8200,
|
||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||
|
||||
Connect: structs.ServiceConnect{
|
||||
SidecarService: &structs.ServiceDefinition{},
|
||||
},
|
||||
}
|
||||
|
||||
connectSrv, _, _, err := a.sidecarServiceFromNodeService(srv, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// First persist the check
|
||||
err = a.AddService(srv, nil, true, "", ConfigSourceLocal)
|
||||
require.NoError(t, err)
|
||||
err = a.AddService(connectSrv, nil, true, "", ConfigSourceLocal)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check both services were registered
|
||||
require.NotNil(t, a.State.Service(srv.CompoundServiceID()))
|
||||
require.NotNil(t, a.State.Service(connectSrv.CompoundServiceID()))
|
||||
|
||||
a.Shutdown()
|
||||
|
||||
// Start again with the check registered in config
|
||||
a2 := StartTestAgent(t, TestAgent{
|
||||
Name: "Agent2",
|
||||
DataDir: a.DataDir,
|
||||
HCL: `
|
||||
node_id = "` + nodeID + `"
|
||||
node_name = "Node ` + nodeID + `"
|
||||
server = false
|
||||
bootstrap = false
|
||||
enable_central_service_config = false
|
||||
`})
|
||||
defer a2.Shutdown()
|
||||
|
||||
// check both services were restored
|
||||
require.NotNil(t, a2.State.Service(srv.CompoundServiceID()))
|
||||
require.NotNil(t, a2.State.Service(connectSrv.CompoundServiceID()))
|
||||
|
||||
err = a2.RemoveService(srv.CompoundServiceID())
|
||||
require.NoError(t, err)
|
||||
|
||||
// check both services were deregistered
|
||||
require.Nil(t, a2.State.Service(srv.CompoundServiceID()))
|
||||
require.Nil(t, a2.State.Service(connectSrv.CompoundServiceID()))
|
||||
}
|
||||
|
||||
func TestAgent_loadChecks_token(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := NewTestAgent(t, `
|
||||
|
@ -4693,3 +4769,68 @@ func TestSharedRPCRouter(t *testing.T) {
|
|||
require.NotNil(t, mgr)
|
||||
require.NotNil(t, server)
|
||||
}
|
||||
|
||||
func TestAgent_ListenHTTP_MultipleAddresses(t *testing.T) {
|
||||
ports, err := freeport.Take(2)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { freeport.Return(ports) })
|
||||
|
||||
caConfig := tlsutil.Config{}
|
||||
tlsConf, err := tlsutil.NewConfigurator(caConfig, hclog.New(nil))
|
||||
require.NoError(t, err)
|
||||
bd := BaseDeps{
|
||||
Deps: consul.Deps{
|
||||
Logger: hclog.NewInterceptLogger(nil),
|
||||
Tokens: new(token.Store),
|
||||
TLSConfigurator: tlsConf,
|
||||
},
|
||||
RuntimeConfig: &config.RuntimeConfig{
|
||||
HTTPAddrs: []net.Addr{
|
||||
&net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[0]},
|
||||
&net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: ports[1]},
|
||||
},
|
||||
},
|
||||
Cache: cache.New(cache.Options{}),
|
||||
}
|
||||
agent, err := New(bd)
|
||||
require.NoError(t, err)
|
||||
|
||||
srvs, err := agent.listenHTTP()
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
for _, srv := range srvs {
|
||||
srv.Shutdown(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
g := new(errgroup.Group)
|
||||
for _, s := range srvs {
|
||||
g.Go(s.Run)
|
||||
}
|
||||
|
||||
require.Len(t, srvs, 2)
|
||||
require.Len(t, uniqueAddrs(srvs), 2)
|
||||
|
||||
client := &http.Client{}
|
||||
for _, s := range srvs {
|
||||
u := url.URL{Scheme: s.Protocol, Host: s.Addr.String()}
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueAddrs(srvs []apiServer) map[string]struct{} {
|
||||
result := make(map[string]struct{}, len(srvs))
|
||||
for _, s := range srvs {
|
||||
result[s.Addr.String()] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package agent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -37,8 +39,6 @@ type apiServer struct {
|
|||
Run func() error
|
||||
// Shutdown function used to stop the server
|
||||
Shutdown func(context.Context) error
|
||||
|
||||
MaxHeaderBytes int
|
||||
}
|
||||
|
||||
// NewAPIServers returns an empty apiServers that is ready to Start servers.
|
||||
|
@ -94,3 +94,18 @@ func (s *apiServers) Shutdown(ctx context.Context) {
|
|||
func (s *apiServers) WaitForShutdown() error {
|
||||
return s.group.Wait()
|
||||
}
|
||||
|
||||
func newAPIServerHTTP(proto string, l net.Listener, httpServer *http.Server) apiServer {
|
||||
return apiServer{
|
||||
Protocol: proto,
|
||||
Addr: l.Addr(),
|
||||
Shutdown: httpServer.Shutdown,
|
||||
Run: func() error {
|
||||
err := httpServer.Serve(l)
|
||||
if err == nil || err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s server %s failed: %w", proto, l.Addr(), err)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -634,6 +634,7 @@ type testAutoConfig struct {
|
|||
ac *AutoConfig
|
||||
tokenUpdates chan struct{}
|
||||
originalToken string
|
||||
stop func()
|
||||
|
||||
initialRoots *structs.IndexedCARoots
|
||||
initialCert *structs.IssuedCert
|
||||
|
@ -835,6 +836,7 @@ func startedAutoConfig(t *testing.T, autoEncrypt bool) testAutoConfig {
|
|||
initialRoots: indexedRoots,
|
||||
initialCert: cert,
|
||||
extraCerts: extraCerts,
|
||||
stop: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1098,16 +1100,15 @@ func TestFallback(t *testing.T) {
|
|||
// now wait for the fallback routine to be invoked
|
||||
require.True(t, waitForChans(100*time.Millisecond, fallbackCtx.Done()), "fallback routines did not get invoked within the alotted time")
|
||||
|
||||
// persisting these to disk happens after the RPC we waited on above will have fired
|
||||
// There is no deterministic way to know once its been written so we wrap this in a retry.
|
||||
testretry.Run(t, func(r *testretry.R) {
|
||||
resp, err := testAC.ac.readPersistedAutoConfig()
|
||||
require.NoError(r, err)
|
||||
testAC.stop()
|
||||
<-testAC.ac.done
|
||||
|
||||
// ensure the roots got persisted to disk
|
||||
require.Equal(r, thirdCert.CertPEM, resp.Certificate.GetCertPEM())
|
||||
require.Equal(r, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID())
|
||||
})
|
||||
resp, err := testAC.ac.readPersistedAutoConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
// ensure the roots got persisted to disk
|
||||
require.Equal(t, thirdCert.CertPEM, resp.Certificate.GetCertPEM())
|
||||
require.Equal(t, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID())
|
||||
}
|
||||
|
||||
func TestIntroToken(t *testing.T) {
|
||||
|
|
|
@ -9,17 +9,15 @@ import (
|
|||
"github.com/hashicorp/consul/types"
|
||||
)
|
||||
|
||||
func newEndOfSnapshotEvent(topic pbsubscribe.Topic, index uint64) *pbsubscribe.Event {
|
||||
func newEndOfSnapshotEvent(index uint64) *pbsubscribe.Event {
|
||||
return &pbsubscribe.Event{
|
||||
Topic: topic,
|
||||
Index: index,
|
||||
Payload: &pbsubscribe.Event_EndOfSnapshot{EndOfSnapshot: true},
|
||||
}
|
||||
}
|
||||
|
||||
func newNewSnapshotToFollowEvent(topic pbsubscribe.Topic) *pbsubscribe.Event {
|
||||
func newNewSnapshotToFollowEvent() *pbsubscribe.Event {
|
||||
return &pbsubscribe.Event{
|
||||
Topic: topic,
|
||||
Payload: &pbsubscribe.Event_NewSnapshotToFollow{NewSnapshotToFollow: true},
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +35,6 @@ func newEventServiceHealthRegister(index uint64, nodeNum int, svc string) *pbsub
|
|||
addr := fmt.Sprintf("10.10.%d.%d", nodeNum/256, nodeNum%256)
|
||||
|
||||
return &pbsubscribe.Event{
|
||||
Topic: pbsubscribe.Topic_ServiceHealth,
|
||||
Key: svc,
|
||||
Index: index,
|
||||
Payload: &pbsubscribe.Event_ServiceHealth{
|
||||
ServiceHealth: &pbsubscribe.ServiceHealthUpdate{
|
||||
|
@ -117,8 +113,6 @@ func newEventServiceHealthDeregister(index uint64, nodeNum int, svc string) *pbs
|
|||
node := fmt.Sprintf("node%d", nodeNum)
|
||||
|
||||
return &pbsubscribe.Event{
|
||||
Topic: pbsubscribe.Topic_ServiceHealth,
|
||||
Key: svc,
|
||||
Index: index,
|
||||
Payload: &pbsubscribe.Event_ServiceHealth{
|
||||
ServiceHealth: &pbsubscribe.ServiceHealthUpdate{
|
||||
|
@ -164,7 +158,6 @@ func newEventBatchWithEvents(first *pbsubscribe.Event, evs ...*pbsubscribe.Event
|
|||
events[i+1] = evs[i]
|
||||
}
|
||||
return &pbsubscribe.Event{
|
||||
Topic: first.Topic,
|
||||
Index: first.Index,
|
||||
Payload: &pbsubscribe.Event_EventBatch{
|
||||
EventBatch: &pbsubscribe.EventBatch{Events: events},
|
||||
|
|
|
@ -3,6 +3,7 @@ package cachetype
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
|
@ -72,7 +73,7 @@ func (c *StreamingHealthServices) Fetch(opts cache.FetchOptions, req cache.Reque
|
|||
Token: srvReq.Token,
|
||||
Datacenter: srvReq.Datacenter,
|
||||
Index: index,
|
||||
// TODO(streaming): set Namespace from srvReq.EnterpriseMeta.Namespace
|
||||
Namespace: srvReq.EnterpriseMeta.GetNamespace(),
|
||||
}
|
||||
if srvReq.Connect {
|
||||
req.Topic = pbsubscribe.Topic_ServiceHealthConnect
|
||||
|
@ -138,15 +139,14 @@ func (s *streamingHealthState) Fetch(opts cache.FetchOptions) (cache.FetchResult
|
|||
}
|
||||
|
||||
func newHealthView(filterExpr string) (*healthView, error) {
|
||||
s := &healthView{state: make(map[string]structs.CheckServiceNode)}
|
||||
|
||||
// We apply filtering to the raw CheckServiceNodes before we are done mutating
|
||||
// state in Update to save from storing stuff in memory we'll only filter
|
||||
// later. Because the state is just a map of those types, we can simply run
|
||||
// that map through filter and it will remove any entries that don't match.
|
||||
var err error
|
||||
s.filter, err = bexpr.CreateFilter(filterExpr, nil, s.state)
|
||||
return s, err
|
||||
fe, err := newFilterEvaluator(filterExpr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &healthView{
|
||||
state: make(map[string]structs.CheckServiceNode),
|
||||
filter: fe,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// healthView implements submatview.View for storing the view state
|
||||
|
@ -156,7 +156,7 @@ func newHealthView(filterExpr string) (*healthView, error) {
|
|||
// involves re-sorting each time etc. though.
|
||||
type healthView struct {
|
||||
state map[string]structs.CheckServiceNode
|
||||
filter *bexpr.Filter
|
||||
filter filterEvaluator
|
||||
}
|
||||
|
||||
// Update implements View
|
||||
|
@ -171,24 +171,41 @@ func (s *healthView) Update(events []*pbsubscribe.Event) error {
|
|||
id := serviceHealth.CheckServiceNode.UniqueID()
|
||||
switch serviceHealth.Op {
|
||||
case pbsubscribe.CatalogOp_Register:
|
||||
csn := pbservice.CheckServiceNodeToStructs(serviceHealth.CheckServiceNode)
|
||||
s.state[id] = *csn
|
||||
csn := *pbservice.CheckServiceNodeToStructs(serviceHealth.CheckServiceNode)
|
||||
passed, err := s.filter.Evaluate(csn)
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case passed:
|
||||
s.state[id] = csn
|
||||
}
|
||||
|
||||
case pbsubscribe.CatalogOp_Deregister:
|
||||
delete(s.state, id)
|
||||
}
|
||||
}
|
||||
// TODO(streaming): should this filter be applied to only the new CheckServiceNode
|
||||
// instead of the full map, which should already be filtered.
|
||||
if s.filter != nil {
|
||||
filtered, err := s.filter.Execute(s.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.state = filtered.(map[string]structs.CheckServiceNode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type filterEvaluator interface {
|
||||
Evaluate(datum interface{}) (bool, error)
|
||||
}
|
||||
|
||||
func newFilterEvaluator(expr string) (filterEvaluator, error) {
|
||||
if expr == "" {
|
||||
return noopFilterEvaluator{}, nil
|
||||
}
|
||||
return bexpr.CreateEvaluatorForType(expr, nil, reflect.TypeOf(structs.CheckServiceNode{}))
|
||||
}
|
||||
|
||||
// noopFilterEvaluator may be used in place of a bexpr.Evaluator. The Evaluate
|
||||
// method always return true, so no items will be filtered out.
|
||||
type noopFilterEvaluator struct{}
|
||||
|
||||
func (noopFilterEvaluator) Evaluate(_ interface{}) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Result returns the structs.IndexedCheckServiceNodes stored by this view.
|
||||
func (s *healthView) Result(index uint64) (interface{}, error) {
|
||||
result := structs.IndexedCheckServiceNodes{
|
||||
|
|
|
@ -14,11 +14,13 @@ import (
|
|||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/proto/pbcommon"
|
||||
"github.com/hashicorp/consul/proto/pbsubscribe"
|
||||
)
|
||||
|
||||
func TestStreamingHealthServices_EmptySnapshot(t *testing.T) {
|
||||
client := NewTestStreamingClient()
|
||||
namespace := pbcommon.DefaultEnterpriseMeta.Namespace
|
||||
client := NewTestStreamingClient(namespace)
|
||||
typ := StreamingHealthServices{deps: MaterializerDeps{
|
||||
Client: client,
|
||||
Logger: hclog.Default(),
|
||||
|
@ -26,15 +28,16 @@ func TestStreamingHealthServices_EmptySnapshot(t *testing.T) {
|
|||
|
||||
// Initially there are no services registered. Server should send an
|
||||
// EndOfSnapshot message immediately with index of 1.
|
||||
client.QueueEvents(newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 1))
|
||||
client.QueueEvents(newEndOfSnapshotEvent(1))
|
||||
|
||||
opts := cache.FetchOptions{
|
||||
MinIndex: 0,
|
||||
Timeout: time.Second,
|
||||
}
|
||||
req := &structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
EnterpriseMeta: structs.EnterpriseMetaInitializer(namespace),
|
||||
}
|
||||
empty := &structs.IndexedCheckServiceNodes{
|
||||
Nodes: structs.CheckServiceNodes{},
|
||||
|
@ -215,8 +218,17 @@ func requireResultsSame(t *testing.T, want, got *structs.IndexedCheckServiceNode
|
|||
require.ElementsMatch(t, wantIDs, gotIDs)
|
||||
}
|
||||
|
||||
// getNamespace returns a namespace if namespace support exists, otherwise
|
||||
// returns the empty string. It allows the same tests to work in both oss and ent
|
||||
// without duplicating the tests.
|
||||
func getNamespace(ns string) string {
|
||||
meta := structs.EnterpriseMetaInitializer(ns)
|
||||
return meta.GetNamespace()
|
||||
}
|
||||
|
||||
func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
||||
client := NewTestStreamingClient()
|
||||
namespace := getNamespace("ns2")
|
||||
client := NewTestStreamingClient(namespace)
|
||||
typ := StreamingHealthServices{deps: MaterializerDeps{
|
||||
Client: client,
|
||||
Logger: hclog.Default(),
|
||||
|
@ -230,7 +242,7 @@ func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
|||
registerServiceWeb(5, 1),
|
||||
registerServiceWeb(5, 2),
|
||||
registerServiceWeb(5, 3),
|
||||
newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 5))
|
||||
newEndOfSnapshotEvent(5))
|
||||
|
||||
// This contains the view state so important we share it between calls.
|
||||
opts := cache.FetchOptions{
|
||||
|
@ -238,8 +250,9 @@ func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
|||
Timeout: 1 * time.Second,
|
||||
}
|
||||
req := &structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
EnterpriseMeta: structs.EnterpriseMetaInitializer(namespace),
|
||||
}
|
||||
|
||||
gatherNodes := func(res interface{}) []string {
|
||||
|
@ -301,7 +314,7 @@ func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
|||
registerServiceWeb(50, 3), // overlap existing node
|
||||
registerServiceWeb(50, 4),
|
||||
registerServiceWeb(50, 5),
|
||||
newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 50))
|
||||
newEndOfSnapshotEvent(50))
|
||||
|
||||
// Make another blocking query with THE SAME index. It should immediately
|
||||
// return the new snapshot.
|
||||
|
@ -324,11 +337,11 @@ func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
|||
client.QueueErr(tempError("temporary connection error"))
|
||||
|
||||
client.QueueEvents(
|
||||
newNewSnapshotToFollowEvent(pbsubscribe.Topic_ServiceHealth),
|
||||
newNewSnapshotToFollowEvent(),
|
||||
registerServiceWeb(50, 3), // overlap existing node
|
||||
registerServiceWeb(50, 4),
|
||||
registerServiceWeb(50, 5),
|
||||
newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 50))
|
||||
newEndOfSnapshotEvent(50))
|
||||
|
||||
start := time.Now()
|
||||
opts.MinIndex = 49
|
||||
|
@ -345,7 +358,8 @@ func TestStreamingHealthServices_FullSnapshot(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStreamingHealthServices_EventBatches(t *testing.T) {
|
||||
client := NewTestStreamingClient()
|
||||
namespace := getNamespace("ns3")
|
||||
client := NewTestStreamingClient(namespace)
|
||||
typ := StreamingHealthServices{deps: MaterializerDeps{
|
||||
Client: client,
|
||||
Logger: hclog.Default(),
|
||||
|
@ -358,7 +372,7 @@ func TestStreamingHealthServices_EventBatches(t *testing.T) {
|
|||
newEventServiceHealthRegister(5, 3, "web"))
|
||||
client.QueueEvents(
|
||||
batchEv,
|
||||
newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 5))
|
||||
newEndOfSnapshotEvent(5))
|
||||
|
||||
// This contains the view state so important we share it between calls.
|
||||
opts := cache.FetchOptions{
|
||||
|
@ -366,8 +380,9 @@ func TestStreamingHealthServices_EventBatches(t *testing.T) {
|
|||
Timeout: 1 * time.Second,
|
||||
}
|
||||
req := &structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
EnterpriseMeta: structs.EnterpriseMetaInitializer(namespace),
|
||||
}
|
||||
|
||||
gatherNodes := func(res interface{}) []string {
|
||||
|
@ -415,7 +430,8 @@ func TestStreamingHealthServices_EventBatches(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestStreamingHealthServices_Filtering(t *testing.T) {
|
||||
client := NewTestStreamingClient()
|
||||
namespace := getNamespace("ns3")
|
||||
client := NewTestStreamingClient(namespace)
|
||||
typ := StreamingHealthServices{deps: MaterializerDeps{
|
||||
Client: client,
|
||||
Logger: hclog.Default(),
|
||||
|
@ -428,7 +444,7 @@ func TestStreamingHealthServices_Filtering(t *testing.T) {
|
|||
newEventServiceHealthRegister(5, 3, "web"))
|
||||
client.QueueEvents(
|
||||
batchEv,
|
||||
newEndOfSnapshotEvent(pbsubscribe.Topic_ServiceHealth, 5))
|
||||
newEndOfSnapshotEvent(5))
|
||||
|
||||
// This contains the view state so important we share it between calls.
|
||||
opts := cache.FetchOptions{
|
||||
|
@ -436,8 +452,9 @@ func TestStreamingHealthServices_Filtering(t *testing.T) {
|
|||
Timeout: 1 * time.Second,
|
||||
}
|
||||
req := &structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
EnterpriseMeta: structs.EnterpriseMetaInitializer(namespace),
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Filter: `Node.Node == "node2"`,
|
||||
},
|
||||
|
|
|
@ -2,6 +2,7 @@ package cachetype
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
|
@ -12,8 +13,9 @@ import (
|
|||
// for queueing up custom events to a subscriber.
|
||||
type TestStreamingClient struct {
|
||||
pbsubscribe.StateChangeSubscription_SubscribeClient
|
||||
events chan eventOrErr
|
||||
ctx context.Context
|
||||
events chan eventOrErr
|
||||
ctx context.Context
|
||||
expectedNamespace string
|
||||
}
|
||||
|
||||
type eventOrErr struct {
|
||||
|
@ -21,17 +23,22 @@ type eventOrErr struct {
|
|||
Event *pbsubscribe.Event
|
||||
}
|
||||
|
||||
func NewTestStreamingClient() *TestStreamingClient {
|
||||
func NewTestStreamingClient(ns string) *TestStreamingClient {
|
||||
return &TestStreamingClient{
|
||||
events: make(chan eventOrErr, 32),
|
||||
events: make(chan eventOrErr, 32),
|
||||
expectedNamespace: ns,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TestStreamingClient) Subscribe(
|
||||
ctx context.Context,
|
||||
_ *pbsubscribe.SubscribeRequest,
|
||||
req *pbsubscribe.SubscribeRequest,
|
||||
_ ...grpc.CallOption,
|
||||
) (pbsubscribe.StateChangeSubscription_SubscribeClient, error) {
|
||||
if req.Namespace != t.expectedNamespace {
|
||||
return nil, fmt.Errorf("wrong SubscribeRequest.Namespace %v, expected %v",
|
||||
req.Namespace, t.expectedNamespace)
|
||||
}
|
||||
t.ctx = ctx
|
||||
return t, nil
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -25,13 +24,43 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/lib/ttlcache"
|
||||
)
|
||||
|
||||
//go:generate mockery -all -inpkg
|
||||
|
||||
// TODO(kit): remove the namespace from these once the metrics themselves change
|
||||
var Gauges = []prometheus.GaugeDefinition{
|
||||
{
|
||||
Name: []string{"consul", "cache", "entries_count"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
// TODO(kit): remove the namespace from these once the metrics themselves change
|
||||
var Counters = []prometheus.CounterDefinition{
|
||||
{
|
||||
Name: []string{"consul", "cache", "bypass"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"consul", "cache", "fetch_success"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"consul", "cache", "fetch_error"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"consul", "cache", "evict_expired"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
// Constants related to refresh backoff. We probably don't ever need to
|
||||
// make these configurable knobs since they primarily exist to lower load.
|
||||
const (
|
||||
|
@ -88,7 +117,7 @@ type Cache struct {
|
|||
// internal storage format so changing this should be possible safely.
|
||||
entriesLock sync.RWMutex
|
||||
entries map[string]cacheEntry
|
||||
entriesExpiryHeap *expiryHeap
|
||||
entriesExpiryHeap *ttlcache.ExpiryHeap
|
||||
|
||||
// stopped is used as an atomic flag to signal that the Cache has been
|
||||
// discarded so background fetches and expiry processing should stop.
|
||||
|
@ -166,16 +195,11 @@ func applyDefaultValuesOnOptions(options Options) Options {
|
|||
// Further settings can be tweaked on the returned value.
|
||||
func New(options Options) *Cache {
|
||||
options = applyDefaultValuesOnOptions(options)
|
||||
// Initialize the heap. The buffer of 1 is really important because
|
||||
// its possible for the expiry loop to trigger the heap to update
|
||||
// itself and it'd block forever otherwise.
|
||||
h := &expiryHeap{NotifyCh: make(chan struct{}, 1)}
|
||||
heap.Init(h)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Cache{
|
||||
types: make(map[string]typeEntry),
|
||||
entries: make(map[string]cacheEntry),
|
||||
entriesExpiryHeap: h,
|
||||
entriesExpiryHeap: ttlcache.NewExpiryHeap(),
|
||||
stopCh: make(chan struct{}),
|
||||
options: options,
|
||||
rateLimitContext: ctx,
|
||||
|
@ -406,8 +430,7 @@ RETRY_GET:
|
|||
|
||||
// Touch the expiration and fix the heap.
|
||||
c.entriesLock.Lock()
|
||||
entry.Expiry.Update(r.TypeEntry.Opts.LastGetTTL)
|
||||
c.entriesExpiryHeap.Fix(entry.Expiry)
|
||||
c.entriesExpiryHeap.Update(entry.Expiry.Index(), r.TypeEntry.Opts.LastGetTTL)
|
||||
c.entriesLock.Unlock()
|
||||
|
||||
// We purposely do not return an error here since the cache only works with
|
||||
|
@ -635,6 +658,7 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign
|
|||
// Error handling
|
||||
if err == nil {
|
||||
labels := []metrics.Label{{Name: "result_not_modified", Value: strconv.FormatBool(result.NotModified)}}
|
||||
// TODO(kit): move tEntry.Name to a label on the first write here and deprecate the second write
|
||||
metrics.IncrCounterWithLabels([]string{"consul", "cache", "fetch_success"}, 1, labels)
|
||||
metrics.IncrCounterWithLabels([]string{"consul", "cache", tEntry.Name, "fetch_success"}, 1, labels)
|
||||
|
||||
|
@ -664,6 +688,7 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign
|
|||
newEntry.RefreshLostContact = time.Time{}
|
||||
}
|
||||
} else {
|
||||
// TODO(kit): Add tEntry.Name to label on fetch_error and deprecate second write
|
||||
metrics.IncrCounter([]string{"consul", "cache", "fetch_error"}, 1)
|
||||
metrics.IncrCounter([]string{"consul", "cache", tEntry.Name, "fetch_error"}, 1)
|
||||
|
||||
|
@ -688,10 +713,8 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign
|
|||
// If this is a new entry (not in the heap yet), then setup the
|
||||
// initial expiry information and insert. If we're already in
|
||||
// the heap we do nothing since we're reusing the same entry.
|
||||
if newEntry.Expiry == nil || newEntry.Expiry.HeapIndex == -1 {
|
||||
newEntry.Expiry = &cacheEntryExpiry{Key: key}
|
||||
newEntry.Expiry.Update(tEntry.Opts.LastGetTTL)
|
||||
heap.Push(c.entriesExpiryHeap, newEntry.Expiry)
|
||||
if newEntry.Expiry == nil || newEntry.Expiry.Index() == ttlcache.NotIndexed {
|
||||
newEntry.Expiry = c.entriesExpiryHeap.Add(key, tEntry.Opts.LastGetTTL)
|
||||
}
|
||||
|
||||
c.entries[key] = newEntry
|
||||
|
@ -748,47 +771,30 @@ func backOffWait(failures uint) time.Duration {
|
|||
// runExpiryLoop is a blocking function that watches the expiration
|
||||
// heap and invalidates entries that have expired.
|
||||
func (c *Cache) runExpiryLoop() {
|
||||
var expiryTimer *time.Timer
|
||||
for {
|
||||
// If we have a previous timer, stop it.
|
||||
if expiryTimer != nil {
|
||||
expiryTimer.Stop()
|
||||
}
|
||||
|
||||
// Get the entry expiring soonest
|
||||
var entry *cacheEntryExpiry
|
||||
var expiryCh <-chan time.Time
|
||||
c.entriesLock.RLock()
|
||||
if len(c.entriesExpiryHeap.Entries) > 0 {
|
||||
entry = c.entriesExpiryHeap.Entries[0]
|
||||
expiryTimer = time.NewTimer(time.Until(entry.Expires))
|
||||
expiryCh = expiryTimer.C
|
||||
}
|
||||
timer := c.entriesExpiryHeap.Next()
|
||||
c.entriesLock.RUnlock()
|
||||
|
||||
select {
|
||||
case <-c.stopCh:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-c.entriesExpiryHeap.NotifyCh:
|
||||
// Entries changed, so the heap may have changed. Restart loop.
|
||||
timer.Stop()
|
||||
continue
|
||||
|
||||
case <-expiryCh:
|
||||
case <-timer.Wait():
|
||||
c.entriesLock.Lock()
|
||||
|
||||
// Perform cleanup operations on the entry's state, if applicable.
|
||||
state := c.entries[entry.Key].State
|
||||
if closer, ok := state.(io.Closer); ok {
|
||||
entry := timer.Entry
|
||||
if closer, ok := c.entries[entry.Key()].State.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
|
||||
// Entry expired! Remove it.
|
||||
delete(c.entries, entry.Key)
|
||||
heap.Remove(c.entriesExpiryHeap, entry.HeapIndex)
|
||||
|
||||
// This is subtle but important: if we race and simultaneously
|
||||
// evict and fetch a new value, then we set this to -1 to
|
||||
// have it treated as a new value so that the TTL is extended.
|
||||
entry.HeapIndex = -1
|
||||
delete(c.entries, entry.Key())
|
||||
c.entriesExpiryHeap.Remove(entry.Index())
|
||||
|
||||
// Set some metrics
|
||||
metrics.IncrCounter([]string{"consul", "cache", "evict_expired"}, 1)
|
||||
|
@ -829,7 +835,6 @@ func (c *Cache) Prepopulate(t string, res FetchResult, dc, token, k string) erro
|
|||
Index: res.Index,
|
||||
FetchedAt: time.Now(),
|
||||
Waiter: make(chan struct{}),
|
||||
Expiry: &cacheEntryExpiry{Key: key},
|
||||
FetchRateLimiter: rate.NewLimiter(
|
||||
c.options.EntryFetchRate,
|
||||
c.options.EntryFetchMaxBurst,
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/lib/ttlcache"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
)
|
||||
|
||||
|
@ -1000,6 +1001,9 @@ func (t *testPartitionType) RegisterOptions() RegisterOptions {
|
|||
// Test that background refreshing reports correct Age in failure and happy
|
||||
// states.
|
||||
func TestCacheGet_refreshAge(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for -short run")
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
|
@ -1402,3 +1406,73 @@ OUT:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_ExpiryLoop_ExitsWhenStopped(t *testing.T) {
|
||||
c := &Cache{
|
||||
stopCh: make(chan struct{}),
|
||||
entries: make(map[string]cacheEntry),
|
||||
entriesExpiryHeap: ttlcache.NewExpiryHeap(),
|
||||
}
|
||||
chStart := make(chan struct{})
|
||||
chDone := make(chan struct{})
|
||||
go func() {
|
||||
close(chStart)
|
||||
c.runExpiryLoop()
|
||||
close(chDone)
|
||||
}()
|
||||
|
||||
<-chStart
|
||||
close(c.stopCh)
|
||||
|
||||
select {
|
||||
case <-chDone:
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
t.Fatalf("expected loop to exit when stopped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_Prepopulate(t *testing.T) {
|
||||
typ := &fakeType{index: 5}
|
||||
c := New(Options{})
|
||||
c.RegisterType("t", typ)
|
||||
|
||||
c.Prepopulate("t", FetchResult{Value: 17, Index: 1}, "dc1", "token", "v1")
|
||||
|
||||
ctx := context.Background()
|
||||
req := fakeRequest{
|
||||
info: RequestInfo{
|
||||
Key: "v1",
|
||||
Token: "token",
|
||||
Datacenter: "dc1",
|
||||
MinIndex: 1,
|
||||
},
|
||||
}
|
||||
result, _, err := c.Get(ctx, "t", req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 17, result)
|
||||
}
|
||||
|
||||
type fakeType struct {
|
||||
index uint64
|
||||
}
|
||||
|
||||
func (f fakeType) Fetch(_ FetchOptions, _ Request) (FetchResult, error) {
|
||||
idx := atomic.LoadUint64(&f.index)
|
||||
return FetchResult{Value: int(idx * 2), Index: idx}, nil
|
||||
}
|
||||
|
||||
func (f fakeType) RegisterOptions() RegisterOptions {
|
||||
return RegisterOptions{Refresh: true}
|
||||
}
|
||||
|
||||
var _ Type = (*fakeType)(nil)
|
||||
|
||||
type fakeRequest struct {
|
||||
info RequestInfo
|
||||
}
|
||||
|
||||
func (f fakeRequest) CacheInfo() RequestInfo {
|
||||
return f.info
|
||||
}
|
||||
|
||||
var _ Request = (*fakeRequest)(nil)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/lib/ttlcache"
|
||||
)
|
||||
|
||||
// cacheEntry stores a single cache entry.
|
||||
|
@ -31,8 +32,8 @@ type cacheEntry struct {
|
|||
|
||||
// Expiry contains information about the expiration of this
|
||||
// entry. This is a pointer as its shared as a value in the
|
||||
// expiryHeap as well.
|
||||
Expiry *cacheEntryExpiry
|
||||
// ExpiryHeap as well.
|
||||
Expiry *ttlcache.Entry
|
||||
|
||||
// FetchedAt stores the time the cache entry was retrieved for determining
|
||||
// it's age later.
|
||||
|
@ -46,118 +47,3 @@ type cacheEntry struct {
|
|||
// FetchRateLimiter limits the rate at which fetch is called for this entry.
|
||||
FetchRateLimiter *rate.Limiter
|
||||
}
|
||||
|
||||
// cacheEntryExpiry contains the expiration information for a cache
|
||||
// entry. Any modifications to this struct should be done only while
|
||||
// the Cache entriesLock is held.
|
||||
type cacheEntryExpiry struct {
|
||||
Key string // Key in the cache map
|
||||
Expires time.Time // Time when entry expires (monotonic clock)
|
||||
HeapIndex int // Index in the heap
|
||||
}
|
||||
|
||||
// Update the expiry to d time from now.
|
||||
func (e *cacheEntryExpiry) Update(d time.Duration) {
|
||||
e.Expires = time.Now().Add(d)
|
||||
}
|
||||
|
||||
// expiryHeap is a heap implementation that stores information about
|
||||
// when entries expire. Implements container/heap.Interface.
|
||||
//
|
||||
// All operations on the heap and read/write of the heap contents require
|
||||
// the proper entriesLock to be held on Cache.
|
||||
type expiryHeap struct {
|
||||
Entries []*cacheEntryExpiry
|
||||
|
||||
// NotifyCh is sent a value whenever the 0 index value of the heap
|
||||
// changes. This can be used to detect when the earliest value
|
||||
// changes.
|
||||
//
|
||||
// There is a single edge case where the heap will not automatically
|
||||
// send a notification: if heap.Fix is called manually and the index
|
||||
// changed is 0 and the change doesn't result in any moves (stays at index
|
||||
// 0), then we won't detect the change. To work around this, please
|
||||
// always call the expiryHeap.Fix method instead.
|
||||
NotifyCh chan struct{}
|
||||
}
|
||||
|
||||
// Identical to heap.Fix for this heap instance but will properly handle
|
||||
// the edge case where idx == 0 and no heap modification is necessary,
|
||||
// and still notify the NotifyCh.
|
||||
//
|
||||
// This is important for cache expiry since the expiry time may have been
|
||||
// extended and if we don't send a message to the NotifyCh then we'll never
|
||||
// reset the timer and the entry will be evicted early.
|
||||
func (h *expiryHeap) Fix(entry *cacheEntryExpiry) {
|
||||
idx := entry.HeapIndex
|
||||
heap.Fix(h, idx)
|
||||
|
||||
// This is the edge case we handle: if the prev (idx) and current (HeapIndex)
|
||||
// is zero, it means the head-of-line didn't change while the value
|
||||
// changed. Notify to reset our expiry worker.
|
||||
if idx == 0 && entry.HeapIndex == 0 {
|
||||
h.notify()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *expiryHeap) Len() int { return len(h.Entries) }
|
||||
|
||||
func (h *expiryHeap) Swap(i, j int) {
|
||||
h.Entries[i], h.Entries[j] = h.Entries[j], h.Entries[i]
|
||||
h.Entries[i].HeapIndex = i
|
||||
h.Entries[j].HeapIndex = j
|
||||
|
||||
// If we're moving the 0 index, update the channel since we need
|
||||
// to re-update the timer we're waiting on for the soonest expiring
|
||||
// value.
|
||||
if i == 0 || j == 0 {
|
||||
h.notify()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *expiryHeap) Less(i, j int) bool {
|
||||
// The usage of Before here is important (despite being obvious):
|
||||
// this function uses the monotonic time that should be available
|
||||
// on the time.Time value so the heap is immune to wall clock changes.
|
||||
return h.Entries[i].Expires.Before(h.Entries[j].Expires)
|
||||
}
|
||||
|
||||
// heap.Interface, this isn't expected to be called directly.
|
||||
func (h *expiryHeap) Push(x interface{}) {
|
||||
entry := x.(*cacheEntryExpiry)
|
||||
|
||||
// Set initial heap index, if we're going to the end then Swap
|
||||
// won't be called so we need to initialize
|
||||
entry.HeapIndex = len(h.Entries)
|
||||
|
||||
// For the first entry, we need to trigger a channel send because
|
||||
// Swap won't be called; nothing to swap! We can call it right away
|
||||
// because all heap operations are within a lock.
|
||||
if len(h.Entries) == 0 {
|
||||
h.notify()
|
||||
}
|
||||
|
||||
h.Entries = append(h.Entries, entry)
|
||||
}
|
||||
|
||||
// heap.Interface, this isn't expected to be called directly.
|
||||
func (h *expiryHeap) Pop() interface{} {
|
||||
old := h.Entries
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
h.Entries = old[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (h *expiryHeap) notify() {
|
||||
select {
|
||||
case h.NotifyCh <- struct{}{}:
|
||||
// Good
|
||||
|
||||
default:
|
||||
// If the send would've blocked, we just ignore it. The reason this
|
||||
// is safe is because NotifyCh should always be a buffered channel.
|
||||
// If this blocks, it means that there is a pending message anyways
|
||||
// so the receiver will restart regardless.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExpiryHeap_impl(t *testing.T) {
|
||||
var _ heap.Interface = new(expiryHeap)
|
||||
}
|
||||
|
||||
func TestExpiryHeap(t *testing.T) {
|
||||
require := require.New(t)
|
||||
now := time.Now()
|
||||
ch := make(chan struct{}, 10) // buffered to prevent blocking in tests
|
||||
h := &expiryHeap{NotifyCh: ch}
|
||||
|
||||
// Init, shouldn't trigger anything
|
||||
heap.Init(h)
|
||||
testNoMessage(t, ch)
|
||||
|
||||
// Push an initial value, expect one message
|
||||
entry := &cacheEntryExpiry{Key: "foo", HeapIndex: -1, Expires: now.Add(100)}
|
||||
heap.Push(h, entry)
|
||||
require.Equal(0, entry.HeapIndex)
|
||||
testMessage(t, ch)
|
||||
testNoMessage(t, ch) // exactly one asserted above
|
||||
|
||||
// Push another that goes earlier than entry
|
||||
entry2 := &cacheEntryExpiry{Key: "bar", HeapIndex: -1, Expires: now.Add(50)}
|
||||
heap.Push(h, entry2)
|
||||
require.Equal(0, entry2.HeapIndex)
|
||||
require.Equal(1, entry.HeapIndex)
|
||||
testMessage(t, ch)
|
||||
testNoMessage(t, ch) // exactly one asserted above
|
||||
|
||||
// Push another that goes at the end
|
||||
entry3 := &cacheEntryExpiry{Key: "bar", HeapIndex: -1, Expires: now.Add(1000)}
|
||||
heap.Push(h, entry3)
|
||||
require.Equal(2, entry3.HeapIndex)
|
||||
testNoMessage(t, ch) // no notify cause index 0 stayed the same
|
||||
|
||||
// Remove the first entry (not Pop, since we don't use Pop, but that works too)
|
||||
remove := h.Entries[0]
|
||||
heap.Remove(h, remove.HeapIndex)
|
||||
require.Equal(0, entry.HeapIndex)
|
||||
require.Equal(1, entry3.HeapIndex)
|
||||
testMessage(t, ch)
|
||||
testMessage(t, ch) // we have two because two swaps happen
|
||||
testNoMessage(t, ch)
|
||||
|
||||
// Let's change entry 3 to be early, and fix it
|
||||
entry3.Expires = now.Add(10)
|
||||
h.Fix(entry3)
|
||||
require.Equal(1, entry.HeapIndex)
|
||||
require.Equal(0, entry3.HeapIndex)
|
||||
testMessage(t, ch)
|
||||
testNoMessage(t, ch)
|
||||
|
||||
// Let's change entry 3 again, this is an edge case where if the 0th
|
||||
// element changed, we didn't trigger the channel. Our Fix func should.
|
||||
entry.Expires = now.Add(20)
|
||||
h.Fix(entry3)
|
||||
require.Equal(1, entry.HeapIndex) // no move
|
||||
require.Equal(0, entry3.HeapIndex)
|
||||
testMessage(t, ch)
|
||||
testNoMessage(t, ch) // one message
|
||||
}
|
||||
|
||||
func testNoMessage(t *testing.T, ch <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("should not have a message")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func testMessage(t *testing.T, ch <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
t.Fatal("should have a message")
|
||||
}
|
||||
}
|
|
@ -5,11 +5,127 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
var CatalogCounters = []prometheus.CounterDefinition{
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_register"},
|
||||
Help: "Increments whenever a Consul agent receives a catalog register request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_register"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a catalog register request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_register"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a catalog register request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_deregister"},
|
||||
Help: "Increments whenever a Consul agent receives a catalog deregister request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_datacenters"},
|
||||
Help: "Increments whenever a Consul agent receives a request to list datacenters in the catalog.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_deregister"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a catalog deregister request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_nodes"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list nodes.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_nodes"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list nodes.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_deregister"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a catalog deregister request.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_datacenters"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list datacenters.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_datacenters"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list datacenters.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_nodes"},
|
||||
Help: "Increments whenever a Consul agent receives a request to list nodes from the catalog.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_services"},
|
||||
Help: "Increments whenever a Consul agent receives a request to list services from the catalog.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_services"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list services.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_services"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list services.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_service_nodes"},
|
||||
Help: "Increments whenever a Consul agent receives a request to list nodes offering a service.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_service_nodes"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list nodes offering a service.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_service_nodes"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list nodes offering a service.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "error", "catalog_service_nodes"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_node_services"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list nodes offering a service.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_node_services"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list services in a node.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_node_services"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list services in a node.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_node_service_list"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_node_service_list"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_node_service_list"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "catalog_gateway_services"},
|
||||
Help: "Increments whenever a Consul agent receives a request to list services associated with a gateway.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "error", "catalog_gateway_services"},
|
||||
Help: "Increments whenever a Consul agent receives an RPC error for a request to list services associated with a gateway.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "api", "success", "catalog_gateway_services"},
|
||||
Help: "Increments whenever a Consul agent successfully responds to a request to list services associated with a gateway.",
|
||||
},
|
||||
}
|
||||
|
||||
func (s *HTTPHandlers) CatalogRegister(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
metrics.IncrCounterWithLabels([]string{"client", "api", "catalog_register"}, 1,
|
||||
[]metrics.Label{{Name: "node", Value: s.nodeName()}})
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
@ -942,13 +944,15 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
|||
DisableHostname: b.boolVal(c.Telemetry.DisableHostname),
|
||||
DogstatsdAddr: b.stringVal(c.Telemetry.DogstatsdAddr),
|
||||
DogstatsdTags: c.Telemetry.DogstatsdTags,
|
||||
PrometheusRetentionTime: b.durationVal("prometheus_retention_time", c.Telemetry.PrometheusRetentionTime),
|
||||
FilterDefault: b.boolVal(c.Telemetry.FilterDefault),
|
||||
AllowedPrefixes: telemetryAllowedPrefixes,
|
||||
BlockedPrefixes: telemetryBlockedPrefixes,
|
||||
MetricsPrefix: b.stringVal(c.Telemetry.MetricsPrefix),
|
||||
StatsdAddr: b.stringVal(c.Telemetry.StatsdAddr),
|
||||
StatsiteAddr: b.stringVal(c.Telemetry.StatsiteAddr),
|
||||
PrometheusOpts: prometheus.PrometheusOpts{
|
||||
Expiration: b.durationVal("prometheus_retention_time", c.Telemetry.PrometheusRetentionTime),
|
||||
},
|
||||
},
|
||||
|
||||
// Agent
|
||||
|
@ -1031,7 +1035,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
|||
NodeID: types.NodeID(b.stringVal(c.NodeID)),
|
||||
NodeMeta: c.NodeMeta,
|
||||
NodeName: b.nodeName(c.NodeName),
|
||||
NonVotingServer: b.boolVal(c.NonVotingServer),
|
||||
ReadReplica: b.boolVal(c.ReadReplica),
|
||||
PidFile: b.stringVal(c.PidFile),
|
||||
PrimaryDatacenter: primaryDatacenter,
|
||||
PrimaryGateways: b.expandAllOptionalAddrs("primary_gateways", c.PrimaryGateways),
|
||||
|
@ -1094,7 +1098,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
|||
Watches: c.Watches,
|
||||
}
|
||||
|
||||
rt.CacheUseStreamingBackend = b.boolVal(c.Cache.UseStreamingBackend)
|
||||
rt.UseStreamingBackend = b.boolVal(c.UseStreamingBackend)
|
||||
|
||||
if rt.Cache.EntryFetchMaxBurst <= 0 {
|
||||
return RuntimeConfig{}, fmt.Errorf("cache.entry_fetch_max_burst must be strictly positive, was: %v", rt.Cache.EntryFetchMaxBurst)
|
||||
|
@ -1103,6 +1107,16 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
|||
return RuntimeConfig{}, fmt.Errorf("cache.entry_fetch_rate must be strictly positive, was: %v", rt.Cache.EntryFetchRate)
|
||||
}
|
||||
|
||||
if rt.UIConfig.MetricsProvider == "prometheus" {
|
||||
// Handle defaulting for the built-in version of prometheus.
|
||||
if len(rt.UIConfig.MetricsProxy.PathAllowlist) == 0 {
|
||||
rt.UIConfig.MetricsProxy.PathAllowlist = []string{
|
||||
"/api/v1/query",
|
||||
"/api/v1/query_range",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.BuildEnterpriseRuntimeConfig(&rt, &c); err != nil {
|
||||
return rt, err
|
||||
}
|
||||
|
@ -1144,6 +1158,10 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
|
|||
// check required params we cannot recover from first
|
||||
//
|
||||
|
||||
if rt.RaftProtocol != 3 {
|
||||
return fmt.Errorf("raft_protocol version %d is not supported by this version of Consul", rt.RaftProtocol)
|
||||
}
|
||||
|
||||
if err := validateBasicName("datacenter", rt.Datacenter, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1181,6 +1199,11 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
|
|||
rt.UIConfig.MetricsProxy.BaseURL)
|
||||
}
|
||||
}
|
||||
for _, allowedPath := range rt.UIConfig.MetricsProxy.PathAllowlist {
|
||||
if err := validateAbsoluteURLPath(allowedPath); err != nil {
|
||||
return fmt.Errorf("ui_config.metrics_proxy.path_allowlist: %v", err)
|
||||
}
|
||||
}
|
||||
for k, v := range rt.UIConfig.DashboardURLTemplates {
|
||||
if err := validateBasicName("ui_config.dashboard_url_templates key names", k, false); err != nil {
|
||||
return err
|
||||
|
@ -1747,8 +1770,9 @@ func (b *Builder) uiMetricsProxyVal(v RawUIMetricsProxy) UIMetricsProxy {
|
|||
}
|
||||
|
||||
return UIMetricsProxy{
|
||||
BaseURL: b.stringVal(v.BaseURL),
|
||||
AddHeaders: hdrs,
|
||||
BaseURL: b.stringVal(v.BaseURL),
|
||||
AddHeaders: hdrs,
|
||||
PathAllowlist: v.PathAllowlist,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2326,3 +2350,24 @@ func validateRemoteScriptsChecks(conf RuntimeConfig) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAbsoluteURLPath(p string) error {
|
||||
if !path.IsAbs(p) {
|
||||
return fmt.Errorf("path %q is not an absolute path", p)
|
||||
}
|
||||
|
||||
// A bit more extra validation that these are actually paths.
|
||||
u, err := url.Parse(p)
|
||||
if err != nil ||
|
||||
u.Scheme != "" ||
|
||||
u.Opaque != "" ||
|
||||
u.User != nil ||
|
||||
u.Host != "" ||
|
||||
u.RawQuery != "" ||
|
||||
u.Fragment != "" ||
|
||||
u.Path != p {
|
||||
return fmt.Errorf("path %q is not an absolute path", p)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ var (
|
|||
"non_voting_server": func(c *Config) {
|
||||
// to maintain existing compatibility we don't nullify the value
|
||||
},
|
||||
"read_replica": func(c *Config) {
|
||||
// to maintain existing compatibility we don't nullify the value
|
||||
},
|
||||
"segment": func(c *Config) {
|
||||
// to maintain existing compatibility we don't nullify the value
|
||||
},
|
||||
|
|
|
@ -24,11 +24,18 @@ func TestBuilder_validateEnterpriseConfigKeys(t *testing.T) {
|
|||
cases := map[string]testCase{
|
||||
"non_voting_server": {
|
||||
config: Config{
|
||||
NonVotingServer: &boolVal,
|
||||
ReadReplica: &boolVal,
|
||||
},
|
||||
keys: []string{"non_voting_server"},
|
||||
badKeys: []string{"non_voting_server"},
|
||||
},
|
||||
"read_replica": {
|
||||
config: Config{
|
||||
ReadReplica: &boolVal,
|
||||
},
|
||||
keys: []string{"read_replica"},
|
||||
badKeys: []string{"read_replica"},
|
||||
},
|
||||
"segment": {
|
||||
config: Config{
|
||||
SegmentName: &stringVal,
|
||||
|
@ -118,11 +125,11 @@ func TestBuilder_validateEnterpriseConfigKeys(t *testing.T) {
|
|||
},
|
||||
"multi": {
|
||||
config: Config{
|
||||
NonVotingServer: &boolVal,
|
||||
SegmentName: &stringVal,
|
||||
ReadReplica: &boolVal,
|
||||
SegmentName: &stringVal,
|
||||
},
|
||||
keys: []string{"non_voting_server", "segment", "acl.tokens.agent_master"},
|
||||
badKeys: []string{"non_voting_server", "segment"},
|
||||
keys: []string{"non_voting_server", "read_replica", "segment", "acl.tokens.agent_master"},
|
||||
badKeys: []string{"non_voting_server", "read_replica", "segment"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -103,9 +103,6 @@ type Cache struct {
|
|||
EntryFetchMaxBurst *int `json:"entry_fetch_max_burst,omitempty" hcl:"entry_fetch_max_burst" mapstructure:"entry_fetch_max_burst"`
|
||||
// EntryFetchRate represents the max calls/sec for a single cache entry
|
||||
EntryFetchRate *float64 `json:"entry_fetch_rate,omitempty" hcl:"entry_fetch_rate" mapstructure:"entry_fetch_rate"`
|
||||
// UseStreamingBackend instead of blocking queries to populate the cache.
|
||||
// Only supported by some cache types.
|
||||
UseStreamingBackend *bool `json:"use_streaming_backend" hcl:"use_streaming_backend" mapstructure:"use_streaming_backend"`
|
||||
}
|
||||
|
||||
// Config defines the format of a configuration file in either JSON or
|
||||
|
@ -264,6 +261,10 @@ type Config struct {
|
|||
|
||||
RPC RPC `mapstructure:"rpc"`
|
||||
|
||||
// UseStreamingBackend instead of blocking queries for service health and
|
||||
// any other endpoints which support streaming.
|
||||
UseStreamingBackend *bool `json:"use_streaming_backend" hcl:"use_streaming_backend" mapstructure:"use_streaming_backend"`
|
||||
|
||||
// This isn't used by Consul but we've documented a feature where users
|
||||
// can deploy their snapshot agent configs alongside their Consul configs
|
||||
// so we have a placeholder here so it can be parsed but this doesn't
|
||||
|
@ -288,7 +289,7 @@ type Config struct {
|
|||
// Enterprise Only
|
||||
Audit *Audit `json:"audit,omitempty" hcl:"audit" mapstructure:"audit"`
|
||||
// Enterprise Only
|
||||
NonVotingServer *bool `json:"non_voting_server,omitempty" hcl:"non_voting_server" mapstructure:"non_voting_server"`
|
||||
ReadReplica *bool `json:"read_replica,omitempty" hcl:"read_replica" mapstructure:"read_replica" alias:"non_voting_server"`
|
||||
// Enterprise Only
|
||||
SegmentName *string `json:"segment,omitempty" hcl:"segment" mapstructure:"segment"`
|
||||
// Enterprise Only
|
||||
|
@ -797,8 +798,9 @@ type RawUIConfig struct {
|
|||
}
|
||||
|
||||
type RawUIMetricsProxy struct {
|
||||
BaseURL *string `json:"base_url,omitempty" hcl:"base_url" mapstructure:"base_url"`
|
||||
AddHeaders []RawUIMetricsProxyAddHeader `json:"add_headers,omitempty" hcl:"add_headers" mapstructure:"add_headers"`
|
||||
BaseURL *string `json:"base_url,omitempty" hcl:"base_url" mapstructure:"base_url"`
|
||||
AddHeaders []RawUIMetricsProxyAddHeader `json:"add_headers,omitempty" hcl:"add_headers" mapstructure:"add_headers"`
|
||||
PathAllowlist []string `json:"path_allowlist,omitempty" hcl:"path_allowlist" mapstructure:"path_allowlist"`
|
||||
}
|
||||
|
||||
type RawUIMetricsProxyAddHeader struct {
|
||||
|
|
|
@ -119,6 +119,7 @@ func DefaultSource() Source {
|
|||
expose_min_port = 21500
|
||||
expose_max_port = 21755
|
||||
}
|
||||
raft_protocol = 3
|
||||
telemetry = {
|
||||
metrics_prefix = "consul"
|
||||
filter_default = true
|
||||
|
|
|
@ -90,7 +90,8 @@ func AddFlags(fs *flag.FlagSet, f *BuilderOpts) {
|
|||
add(&f.Config.NodeName, "node", "Name of this node. Must be unique in the cluster.")
|
||||
add(&f.Config.NodeID, "node-id", "A unique ID for this node across space and time. Defaults to a randomly-generated ID that persists in the data-dir.")
|
||||
add(&f.Config.NodeMeta, "node-meta", "An arbitrary metadata key/value pair for this node, of the format `key:value`. Can be specified multiple times.")
|
||||
add(&f.Config.NonVotingServer, "non-voting-server", "(Enterprise-only) This flag is used to make the server not participate in the Raft quorum, and have it only receive the data replication stream. This can be used to add read scalability to a cluster in cases where a high volume of reads to servers are needed.")
|
||||
add(&f.Config.ReadReplica, "non-voting-server", "(Enterprise-only) DEPRECATED: -read-replica should be used instead")
|
||||
add(&f.Config.ReadReplica, "read-replica", "(Enterprise-only) This flag is used to make the server not participate in the Raft quorum, and have it only receive the data replication stream. This can be used to add read scalability to a cluster in cases where a high volume of reads to servers are needed.")
|
||||
add(&f.Config.PidFile, "pid-file", "Path to file to store agent PID.")
|
||||
add(&f.Config.RPCProtocol, "protocol", "Sets the protocol version. Defaults to latest.")
|
||||
add(&f.Config.RaftProtocol, "raft-protocol", "Sets the Raft protocol version. Defaults to latest.")
|
||||
|
|
|
@ -77,8 +77,8 @@ type RuntimeConfig struct {
|
|||
|
||||
// ACLDefaultPolicy is used to control the ACL interaction when
|
||||
// there is no defined policy. This can be "allow" which means
|
||||
// ACLs are used to black-list, or "deny" which means ACLs are
|
||||
// white-lists.
|
||||
// ACLs are used to deny-list, or "deny" which means ACLs are
|
||||
// allow-lists.
|
||||
//
|
||||
// hcl: acl.default_policy = ("allow"|"deny")
|
||||
ACLDefaultPolicy string
|
||||
|
@ -853,12 +853,12 @@ type RuntimeConfig struct {
|
|||
// flag: -node-meta "key:value" -node-meta "key:value" ...
|
||||
NodeMeta map[string]string
|
||||
|
||||
// NonVotingServer is whether this server will act as a non-voting member
|
||||
// ReadReplica is whether this server will act as a non-voting member
|
||||
// of the cluster to help provide read scalability. (Enterprise-only)
|
||||
//
|
||||
// hcl: non_voting_server = (true|false)
|
||||
// flag: -non-voting-server
|
||||
NonVotingServer bool
|
||||
ReadReplica bool
|
||||
|
||||
// PidFile is the file to store our PID in.
|
||||
//
|
||||
|
@ -945,7 +945,9 @@ type RuntimeConfig struct {
|
|||
|
||||
RPCConfig consul.RPCConfig
|
||||
|
||||
CacheUseStreamingBackend bool
|
||||
// UseStreamingBackend enables streaming as a replacement for agent/cache
|
||||
// in the client agent for endpoints which support streaming.
|
||||
UseStreamingBackend bool
|
||||
|
||||
// RaftProtocol sets the Raft protocol version to use on this server.
|
||||
// Defaults to 3.
|
||||
|
@ -1546,8 +1548,9 @@ type UIConfig struct {
|
|||
}
|
||||
|
||||
type UIMetricsProxy struct {
|
||||
BaseURL string
|
||||
AddHeaders []UIMetricsProxyAddHeader
|
||||
BaseURL string
|
||||
AddHeaders []UIMetricsProxyAddHeader
|
||||
PathAllowlist []string
|
||||
}
|
||||
|
||||
type UIMetricsProxyAddHeader struct {
|
||||
|
@ -1848,6 +1851,21 @@ func sanitize(name string, v reflect.Value) reflect.Value {
|
|||
|
||||
case isArray(typ) || isSlice(typ):
|
||||
ma := make([]interface{}, 0, v.Len())
|
||||
|
||||
if name == "AddHeaders" {
|
||||
// must be UIConfig.MetricsProxy.AddHeaders
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
addr := v.Index(i).Addr()
|
||||
hdr := addr.Interface().(*UIMetricsProxyAddHeader)
|
||||
hm := map[string]interface{}{
|
||||
"Name": hdr.Name,
|
||||
"Value": "hidden",
|
||||
}
|
||||
ma = append(ma, hm)
|
||||
}
|
||||
return reflect.ValueOf(ma)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "SerfAllowedCIDRs") {
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
addr := v.Index(i).Addr()
|
||||
|
|
|
@ -12,12 +12,16 @@ var entTokenConfigSanitize = `"EnterpriseConfig": {},`
|
|||
|
||||
func entFullRuntimeConfig(rt *RuntimeConfig) {}
|
||||
|
||||
var enterpriseNonVotingServerWarnings []string = []string{enterpriseConfigKeyError{key: "non_voting_server"}.Error()}
|
||||
var enterpriseReadReplicaWarnings []string = []string{enterpriseConfigKeyError{key: "read_replica"}.Error()}
|
||||
|
||||
var enterpriseConfigKeyWarnings []string
|
||||
|
||||
func init() {
|
||||
for k := range enterpriseConfigMap {
|
||||
if k == "non_voting_server" {
|
||||
// this is an alias for "read_replica" so we shouldn't see it in warnings
|
||||
continue
|
||||
}
|
||||
enterpriseConfigKeyWarnings = append(enterpriseConfigKeyWarnings, enterpriseConfigKeyError{key: k}.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
|
@ -620,10 +621,10 @@ func TestBuilder_BuildAndValidate_ConfigFlagsAndEdgecases(t *testing.T) {
|
|||
`-data-dir=` + dataDir,
|
||||
},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.NonVotingServer = true
|
||||
rt.ReadReplica = true
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
warns: enterpriseNonVotingServerWarnings,
|
||||
warns: enterpriseReadReplicaWarnings,
|
||||
},
|
||||
{
|
||||
desc: "-pid-file",
|
||||
|
@ -673,14 +674,22 @@ func TestBuilder_BuildAndValidate_ConfigFlagsAndEdgecases(t *testing.T) {
|
|||
{
|
||||
desc: "-raft-protocol",
|
||||
args: []string{
|
||||
`-raft-protocol=1`,
|
||||
`-raft-protocol=3`,
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.RaftProtocol = 1
|
||||
rt.RaftProtocol = 3
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "-raft-protocol unsupported",
|
||||
args: []string{
|
||||
`-raft-protocol=2`,
|
||||
`-data-dir=` + dataDir,
|
||||
},
|
||||
err: "raft_protocol version 2 is not supported by this version of Consul",
|
||||
},
|
||||
{
|
||||
desc: "-recursor",
|
||||
args: []string{
|
||||
|
@ -4556,6 +4565,189 @@ func TestBuilder_BuildAndValidate_ConfigFlagsAndEdgecases(t *testing.T) {
|
|||
`},
|
||||
err: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (empty)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (relative)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "bar/baz" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (weird)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["://bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["://bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "://bar/baz" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (fragment)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["/bar/baz#stuff", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["/bar/baz#stuff", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz#stuff" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (querystring)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["/bar/baz?stu=ff", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["/bar/baz?stu=ff", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz?stu=ff" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist invalid (encoded slash)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["/bar%2fbaz", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["/bar%2fbaz", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
err: `ui_config.metrics_proxy.path_allowlist: path "/bar%2fbaz" is not an absolute path`,
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist ok",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["/bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_proxy {
|
||||
path_allowlist = ["/bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"}
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist defaulted for prometheus",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_provider": "prometheus"
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_provider = "prometheus"
|
||||
}
|
||||
`},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.UIConfig.MetricsProvider = "prometheus"
|
||||
rt.UIConfig.MetricsProxy.PathAllowlist = []string{
|
||||
"/api/v1/query",
|
||||
"/api/v1/query_range",
|
||||
}
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.path_allowlist not overridden with defaults for prometheus",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
json: []string{`{
|
||||
"ui_config": {
|
||||
"metrics_provider": "prometheus",
|
||||
"metrics_proxy": {
|
||||
"path_allowlist": ["/bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
}`},
|
||||
hcl: []string{`
|
||||
ui_config {
|
||||
metrics_provider = "prometheus"
|
||||
metrics_proxy {
|
||||
path_allowlist = ["/bar/baz", "/foo"]
|
||||
}
|
||||
}
|
||||
`},
|
||||
patch: func(rt *RuntimeConfig) {
|
||||
rt.UIConfig.MetricsProvider = "prometheus"
|
||||
rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"}
|
||||
rt.DataDir = dataDir
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "metrics_proxy.base_url http(s)",
|
||||
args: []string{`-data-dir=` + dataDir},
|
||||
|
@ -4906,9 +5098,9 @@ func TestFullConfig(t *testing.T) {
|
|||
"bootstrap_expect": 53,
|
||||
"cache": {
|
||||
"entry_fetch_max_burst": 42,
|
||||
"entry_fetch_rate": 0.334,
|
||||
"use_streaming_backend": true
|
||||
"entry_fetch_rate": 0.334
|
||||
},
|
||||
"use_streaming_backend": true,
|
||||
"ca_file": "erA7T0PM",
|
||||
"ca_path": "mQEN1Mfp",
|
||||
"cert_file": "7s4QAzDk",
|
||||
|
@ -5131,10 +5323,11 @@ func TestFullConfig(t *testing.T) {
|
|||
"primary_datacenter": "ejtmd43d",
|
||||
"primary_gateways": [ "aej8eeZo", "roh2KahS" ],
|
||||
"primary_gateways_interval": "18866s",
|
||||
"raft_protocol": 19016,
|
||||
"raft_protocol": 3,
|
||||
"raft_snapshot_threshold": 16384,
|
||||
"raft_snapshot_interval": "30s",
|
||||
"raft_trailing_logs": 83749,
|
||||
"read_replica": true,
|
||||
"reconnect_timeout": "23739s",
|
||||
"reconnect_timeout_wan": "26694s",
|
||||
"recursors": [ "63.38.39.58", "92.49.18.18" ],
|
||||
|
@ -5471,7 +5664,8 @@ func TestFullConfig(t *testing.T) {
|
|||
"name": "p3nynwc9",
|
||||
"value": "TYBgnN2F"
|
||||
}
|
||||
]
|
||||
],
|
||||
"path_allowlist": ["/aSh3cu", "/eiK/2Th"]
|
||||
},
|
||||
"dashboard_url_templates": {
|
||||
"u2eziu2n_lower_case": "http://lkjasd.otr"
|
||||
|
@ -5593,8 +5787,8 @@ func TestFullConfig(t *testing.T) {
|
|||
cache = {
|
||||
entry_fetch_max_burst = 42
|
||||
entry_fetch_rate = 0.334
|
||||
use_streaming_backend = true
|
||||
},
|
||||
use_streaming_backend = true
|
||||
ca_file = "erA7T0PM"
|
||||
ca_path = "mQEN1Mfp"
|
||||
cert_file = "7s4QAzDk"
|
||||
|
@ -5820,10 +6014,11 @@ func TestFullConfig(t *testing.T) {
|
|||
primary_datacenter = "ejtmd43d"
|
||||
primary_gateways = [ "aej8eeZo", "roh2KahS" ]
|
||||
primary_gateways_interval = "18866s"
|
||||
raft_protocol = 19016
|
||||
raft_protocol = 3
|
||||
raft_snapshot_threshold = 16384
|
||||
raft_snapshot_interval = "30s"
|
||||
raft_trailing_logs = 83749
|
||||
read_replica = true
|
||||
reconnect_timeout = "23739s"
|
||||
reconnect_timeout_wan = "26694s"
|
||||
recursors = [ "63.38.39.58", "92.49.18.18" ]
|
||||
|
@ -6162,6 +6357,7 @@ func TestFullConfig(t *testing.T) {
|
|||
value = "TYBgnN2F"
|
||||
}
|
||||
]
|
||||
path_allowlist = ["/aSh3cu", "/eiK/2Th"]
|
||||
}
|
||||
dashboard_url_templates {
|
||||
u2eziu2n_lower_case = "http://lkjasd.otr"
|
||||
|
@ -6569,7 +6765,7 @@ func TestFullConfig(t *testing.T) {
|
|||
NodeID: types.NodeID("AsUIlw99"),
|
||||
NodeMeta: map[string]string{"5mgGQMBk": "mJLtVMSG", "A7ynFMJB": "0Nx6RGab"},
|
||||
NodeName: "otlLxGaI",
|
||||
NonVotingServer: true,
|
||||
ReadReplica: true,
|
||||
PidFile: "43xN80Km",
|
||||
PrimaryDatacenter: "ejtmd43d",
|
||||
PrimaryGateways: []string{"aej8eeZo", "roh2KahS"},
|
||||
|
@ -6582,7 +6778,7 @@ func TestFullConfig(t *testing.T) {
|
|||
RPCRateLimit: 12029.43,
|
||||
RPCMaxBurst: 44848,
|
||||
RPCMaxConnsPerClient: 2954,
|
||||
RaftProtocol: 19016,
|
||||
RaftProtocol: 3,
|
||||
RaftSnapshotThreshold: 16384,
|
||||
RaftSnapshotInterval: 30 * time.Second,
|
||||
RaftTrailingLogs: 83749,
|
||||
|
@ -6891,17 +7087,17 @@ func TestFullConfig(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
CacheUseStreamingBackend: true,
|
||||
SerfAdvertiseAddrLAN: tcpAddr("17.99.29.16:8301"),
|
||||
SerfAdvertiseAddrWAN: tcpAddr("78.63.37.19:8302"),
|
||||
SerfBindAddrLAN: tcpAddr("99.43.63.15:8301"),
|
||||
SerfBindAddrWAN: tcpAddr("67.88.33.19:8302"),
|
||||
SerfAllowedCIDRsLAN: []net.IPNet{},
|
||||
SerfAllowedCIDRsWAN: []net.IPNet{},
|
||||
SessionTTLMin: 26627 * time.Second,
|
||||
SkipLeaveOnInt: true,
|
||||
StartJoinAddrsLAN: []string{"LR3hGDoG", "MwVpZ4Up"},
|
||||
StartJoinAddrsWAN: []string{"EbFSc3nA", "kwXTh623"},
|
||||
UseStreamingBackend: true,
|
||||
SerfAdvertiseAddrLAN: tcpAddr("17.99.29.16:8301"),
|
||||
SerfAdvertiseAddrWAN: tcpAddr("78.63.37.19:8302"),
|
||||
SerfBindAddrLAN: tcpAddr("99.43.63.15:8301"),
|
||||
SerfBindAddrWAN: tcpAddr("67.88.33.19:8302"),
|
||||
SerfAllowedCIDRsLAN: []net.IPNet{},
|
||||
SerfAllowedCIDRsWAN: []net.IPNet{},
|
||||
SessionTTLMin: 26627 * time.Second,
|
||||
SkipLeaveOnInt: true,
|
||||
StartJoinAddrsLAN: []string{"LR3hGDoG", "MwVpZ4Up"},
|
||||
StartJoinAddrsWAN: []string{"EbFSc3nA", "kwXTh623"},
|
||||
Telemetry: lib.TelemetryConfig{
|
||||
CirconusAPIApp: "p4QOTe9j",
|
||||
CirconusAPIToken: "E3j35V23",
|
||||
|
@ -6924,9 +7120,11 @@ func TestFullConfig(t *testing.T) {
|
|||
AllowedPrefixes: []string{"oJotS8XJ"},
|
||||
BlockedPrefixes: []string{"cazlEhGn"},
|
||||
MetricsPrefix: "ftO6DySn",
|
||||
PrometheusRetentionTime: 15 * time.Second,
|
||||
StatsdAddr: "drce87cy",
|
||||
StatsiteAddr: "HpFwKB8R",
|
||||
PrometheusOpts: prometheus.PrometheusOpts{
|
||||
Expiration: 15 * time.Second,
|
||||
},
|
||||
},
|
||||
TLSCipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256},
|
||||
TLSMinVersion: "pAOWafkR",
|
||||
|
@ -6956,6 +7154,7 @@ func TestFullConfig(t *testing.T) {
|
|||
Value: "TYBgnN2F",
|
||||
},
|
||||
},
|
||||
PathAllowlist: []string{"/aSh3cu", "/eiK/2Th"},
|
||||
},
|
||||
DashboardURLTemplates: map[string]string{"u2eziu2n_lower_case": "http://lkjasd.otr"},
|
||||
},
|
||||
|
@ -7262,6 +7461,7 @@ func TestSanitize(t *testing.T) {
|
|||
EntryFetchRate: 0.334,
|
||||
},
|
||||
ConsulCoordinateUpdatePeriod: 15 * time.Second,
|
||||
RaftProtocol: 3,
|
||||
RetryJoinLAN: []string{
|
||||
"foo=bar key=baz secret=boom bang=bar",
|
||||
},
|
||||
|
@ -7298,6 +7498,13 @@ func TestSanitize(t *testing.T) {
|
|||
*parseCIDR(t, "127.0.0.0/8"),
|
||||
},
|
||||
TxnMaxReqLen: 5678000000000000,
|
||||
UIConfig: UIConfig{
|
||||
MetricsProxy: UIMetricsProxy{
|
||||
AddHeaders: []UIMetricsProxyAddHeader{
|
||||
{Name: "foo", Value: "secret"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rtJSON := `{
|
||||
|
@ -7493,13 +7700,13 @@ func TestSanitize(t *testing.T) {
|
|||
"NodeID": "",
|
||||
"NodeMeta": {},
|
||||
"NodeName": "",
|
||||
"NonVotingServer": false,
|
||||
"PidFile": "",
|
||||
"PrimaryDatacenter": "",
|
||||
"PrimaryGateways": [
|
||||
"pmgw_foo=bar pmgw_key=baz pmgw_secret=boom pmgw_bang=bar"
|
||||
],
|
||||
"PrimaryGatewaysInterval": "0s",
|
||||
"ReadReplica": false,
|
||||
"RPCAdvertiseAddr": "",
|
||||
"RPCBindAddr": "",
|
||||
"RPCHandshakeTimeout": "0s",
|
||||
|
@ -7511,7 +7718,7 @@ func TestSanitize(t *testing.T) {
|
|||
"RPCConfig": {
|
||||
"EnableStreaming": false
|
||||
},
|
||||
"RaftProtocol": 0,
|
||||
"RaftProtocol": 3,
|
||||
"RaftSnapshotInterval": "0s",
|
||||
"RaftSnapshotThreshold": 0,
|
||||
"RaftTrailingLogs": 0,
|
||||
|
@ -7541,7 +7748,7 @@ func TestSanitize(t *testing.T) {
|
|||
"SerfBindAddrWAN": "",
|
||||
"SerfPortLAN": 0,
|
||||
"SerfPortWAN": 0,
|
||||
"CacheUseStreamingBackend": false,
|
||||
"UseStreamingBackend": false,
|
||||
"ServerMode": false,
|
||||
"ServerName": "",
|
||||
"ServerPort": 0,
|
||||
|
@ -7626,9 +7833,15 @@ func TestSanitize(t *testing.T) {
|
|||
"DogstatsdTags": [],
|
||||
"FilterDefault": false,
|
||||
"MetricsPrefix": "",
|
||||
"PrometheusRetentionTime": "0s",
|
||||
"StatsdAddr": "",
|
||||
"StatsiteAddr": ""
|
||||
"StatsiteAddr": "",
|
||||
"PrometheusOpts": {
|
||||
"Expiration": "0s",
|
||||
"Registerer": null,
|
||||
"GaugeDefinitions": [],
|
||||
"CounterDefinitions": [],
|
||||
"SummaryDefinitions": []
|
||||
}
|
||||
},
|
||||
"TranslateWANAddrs": false,
|
||||
"TxnMaxReqLen": 5678000000000000,
|
||||
|
@ -7640,8 +7853,14 @@ func TestSanitize(t *testing.T) {
|
|||
"MetricsProviderFiles": [],
|
||||
"MetricsProviderOptionsJSON": "",
|
||||
"MetricsProxy": {
|
||||
"AddHeaders": [],
|
||||
"BaseURL": ""
|
||||
"AddHeaders": [
|
||||
{
|
||||
"Name": "foo",
|
||||
"Value": "hidden"
|
||||
}
|
||||
],
|
||||
"BaseURL": "",
|
||||
"PathAllowlist": []
|
||||
},
|
||||
"DashboardURLTemplates": {}
|
||||
},
|
||||
|
|
|
@ -6,10 +6,11 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type consulCAMockDelegate struct {
|
||||
|
@ -48,10 +49,7 @@ func (c *consulCAMockDelegate) ApplyCARequest(req *structs.CARequest) (interface
|
|||
}
|
||||
|
||||
func newMockDelegate(t *testing.T, conf *structs.CAConfiguration) *consulCAMockDelegate {
|
||||
s, err := state.NewStateStore(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
s := state.NewStateStore(nil)
|
||||
if s == nil {
|
||||
t.Fatalf("missing state store")
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
|||
v.spiffeID = connect.SpiffeIDSigningForCluster(&structs.CAConfiguration{ClusterID: v.clusterID})
|
||||
|
||||
// Look up the token to see if we can auto-renew its lease.
|
||||
secret, err := client.Auth().Token().Lookup(config.Token)
|
||||
secret, err := client.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
|
@ -15,6 +16,32 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var ACLCounters = []prometheus.CounterDefinition{
|
||||
{
|
||||
Name: []string{"acl", "token", "cache_hit"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "token", "cache_miss"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ACLSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"acl", "resolveTokenLegacy"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "ResolveToken"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "ResolveTokenToIdentity"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
// These must be kept in sync with the constants in command/agent/acl.go.
|
||||
const (
|
||||
// anonymousToken is the token ID we re-write to if there is no token ID
|
||||
|
|
|
@ -11,7 +11,8 @@ import (
|
|||
"regexp"
|
||||
"time"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
|
@ -30,6 +31,73 @@ const (
|
|||
aclBootstrapReset = "acl-bootstrap-reset"
|
||||
)
|
||||
|
||||
var ACLEndpointSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"acl", "token", "clone"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "token", "upsert"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "token", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "policy", "upsert"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "policy", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "policy", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "role", "upsert"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "role", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "bindingrule", "upsert"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "bindingrule", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "authmethod", "upsert"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "authmethod", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "login"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "login"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "logout"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"acl", "logout"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
// Regex for matching
|
||||
var (
|
||||
validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -12,6 +13,13 @@ import (
|
|||
"github.com/hashicorp/go-memdb"
|
||||
)
|
||||
|
||||
var ACLEndpointLegacySummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"acl", "apply"},
|
||||
Help: "Measures the time it takes to complete an update to the ACL store.",
|
||||
},
|
||||
}
|
||||
|
||||
// Bootstrap is used to perform a one-time ACL bootstrap operation on
|
||||
// a cluster to get the first management token.
|
||||
func (a *ACL) Bootstrap(args *structs.DCSpecificRequest, reply *structs.ACL) error {
|
||||
|
|
|
@ -449,9 +449,9 @@ func TestACLEndpoint_ReplicationStatus(t *testing.T) {
|
|||
err := msgpackrpc.CallWithCodec(codec, "ACL.ReplicationStatus", &getR, &status)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, status.Enabled)
|
||||
require.True(t, status.Running)
|
||||
require.Equal(t, "dc2", status.SourceDatacenter)
|
||||
require.True(r, status.Enabled)
|
||||
require.True(r, status.Running)
|
||||
require.Equal(r, "dc2", status.SourceDatacenter)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1264,7 +1264,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
|||
legacy: false,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
tokenReadFn: func(args *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
tokenReadFn: func(_ *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
if !tokenResolved {
|
||||
reply.Token = foundToken
|
||||
tokenResolved = true
|
||||
|
@ -1330,7 +1330,7 @@ func TestACLResolver_DownPolicy(t *testing.T) {
|
|||
legacy: false,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
tokenReadFn: func(args *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
tokenReadFn: func(_ *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
// no limit
|
||||
reply.Token = foundToken
|
||||
return nil
|
||||
|
@ -1440,7 +1440,7 @@ func TestACLResolver_Client(t *testing.T) {
|
|||
legacy: false,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
tokenReadFn: func(args *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
tokenReadFn: func(_ *structs.ACLTokenGetRequest, reply *structs.ACLTokenResponse) error {
|
||||
atomic.AddInt32(&tokenReads, 1)
|
||||
if deleted {
|
||||
return acl.ErrNotFound
|
||||
|
@ -1558,7 +1558,7 @@ func TestACLResolver_Client(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 30
|
||||
reply.ETag = "nothing"
|
||||
|
@ -1900,7 +1900,7 @@ func TestACLResolver_Legacy(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
if !cached {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 30
|
||||
|
@ -1951,7 +1951,7 @@ func TestACLResolver_Legacy(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
if !cached {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 0
|
||||
|
@ -2004,7 +2004,7 @@ func TestACLResolver_Legacy(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
if !cached {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 0
|
||||
|
@ -2058,7 +2058,7 @@ func TestACLResolver_Legacy(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
if !cached {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 0
|
||||
|
@ -2112,7 +2112,7 @@ func TestACLResolver_Legacy(t *testing.T) {
|
|||
legacy: true,
|
||||
localTokens: false,
|
||||
localPolicies: false,
|
||||
getPolicyFn: func(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
getPolicyFn: func(_ *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
|
||||
if !cached {
|
||||
reply.Parent = "deny"
|
||||
reply.TTL = 0
|
||||
|
|
|
@ -280,7 +280,7 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
patchResponse: func(t *testing.T, srv *Server, resp *pbautoconf.AutoConfigResponse) {
|
||||
patchResponse: func(t *testing.T, _ *Server, resp *pbautoconf.AutoConfigResponse) {
|
||||
// we are expecting an ACL token but cannot check anything for equality
|
||||
// so here we check that it was set and overwrite it
|
||||
require.NotNil(t, resp.Config)
|
||||
|
|
|
@ -3,60 +3,49 @@ package consul
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/agent/consul/autopilot"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/agent/metadata"
|
||||
"github.com/hashicorp/consul/types"
|
||||
"github.com/hashicorp/raft"
|
||||
autopilot "github.com/hashicorp/raft-autopilot"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
var AutopilotGauges = []prometheus.GaugeDefinition{
|
||||
{
|
||||
Name: []string{"autopilot", "failure_tolerance"},
|
||||
Help: "Tracks the number of voting servers that the cluster can lose while continuing to function.",
|
||||
},
|
||||
{
|
||||
Name: []string{"autopilot", "healthy"},
|
||||
Help: "Tracks the overall health of the local server cluster. 1 if all servers are healthy, 0 if one or more are unhealthy.",
|
||||
},
|
||||
}
|
||||
|
||||
// AutopilotDelegate is a Consul delegate for autopilot operations.
|
||||
type AutopilotDelegate struct {
|
||||
server *Server
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) AutopilotConfig() *autopilot.Config {
|
||||
return d.server.getOrCreateAutopilotConfig()
|
||||
return d.server.getOrCreateAutopilotConfig().ToAutopilotLibraryConfig()
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) FetchStats(ctx context.Context, servers []serf.Member) map[string]*autopilot.ServerStats {
|
||||
func (d *AutopilotDelegate) KnownServers() map[raft.ServerID]*autopilot.Server {
|
||||
return d.server.autopilotServers()
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) FetchServerStats(ctx context.Context, servers map[raft.ServerID]*autopilot.Server) map[raft.ServerID]*autopilot.ServerStats {
|
||||
return d.server.statsFetcher.Fetch(ctx, servers)
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) IsServer(m serf.Member) (*autopilot.ServerInfo, error) {
|
||||
if m.Tags["role"] != "consul" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
portStr := m.Tags["port"]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buildVersion, err := metadata.Build(&m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := &autopilot.ServerInfo{
|
||||
Name: m.Name,
|
||||
ID: m.Tags["id"],
|
||||
Addr: &net.TCPAddr{IP: m.Addr, Port: port},
|
||||
Build: *buildVersion,
|
||||
Status: m.Status,
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Heartbeat a metric for monitoring if we're the leader
|
||||
func (d *AutopilotDelegate) NotifyHealth(health autopilot.OperatorHealthReply) {
|
||||
func (d *AutopilotDelegate) NotifyState(state *autopilot.State) {
|
||||
// emit metrics if we are the leader regarding overall healthiness and the failure tolerance
|
||||
if d.server.raft.State() == raft.Leader {
|
||||
metrics.SetGauge([]string{"autopilot", "failure_tolerance"}, float32(health.FailureTolerance))
|
||||
if health.Healthy {
|
||||
metrics.SetGauge([]string{"autopilot", "failure_tolerance"}, float32(state.FailureTolerance))
|
||||
if state.Healthy {
|
||||
metrics.SetGauge([]string{"autopilot", "healthy"}, 1)
|
||||
} else {
|
||||
metrics.SetGauge([]string{"autopilot", "healthy"}, 0)
|
||||
|
@ -64,23 +53,88 @@ func (d *AutopilotDelegate) NotifyHealth(health autopilot.OperatorHealthReply) {
|
|||
}
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) PromoteNonVoters(conf *autopilot.Config, health autopilot.OperatorHealthReply) ([]raft.Server, error) {
|
||||
future := d.server.raft.GetConfiguration()
|
||||
if err := future.Error(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get raft configuration: %v", err)
|
||||
func (d *AutopilotDelegate) RemoveFailedServer(srv *autopilot.Server) {
|
||||
go func() {
|
||||
if err := d.server.RemoveFailedNode(srv.Name, false); err != nil {
|
||||
d.server.logger.Error("failedto remove server", "name", srv.Name, "id", srv.ID, "error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) initAutopilot(config *Config) {
|
||||
apDelegate := &AutopilotDelegate{s}
|
||||
|
||||
s.autopilot = autopilot.New(
|
||||
s.raft,
|
||||
apDelegate,
|
||||
autopilot.WithLogger(s.logger),
|
||||
autopilot.WithReconcileInterval(config.AutopilotInterval),
|
||||
autopilot.WithUpdateInterval(config.ServerHealthInterval),
|
||||
autopilot.WithPromoter(s.autopilotPromoter()),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) autopilotServers() map[raft.ServerID]*autopilot.Server {
|
||||
servers := make(map[raft.ServerID]*autopilot.Server)
|
||||
for _, member := range s.serfLAN.Members() {
|
||||
srv, err := s.autopilotServer(member)
|
||||
if err != nil {
|
||||
s.logger.Warn("Error parsing server info", "name", member.Name, "error", err)
|
||||
continue
|
||||
} else if srv == nil {
|
||||
// this member was a client
|
||||
continue
|
||||
}
|
||||
|
||||
servers[srv.ID] = srv
|
||||
}
|
||||
|
||||
return autopilot.PromoteStableServers(conf, health, future.Configuration().Servers), nil
|
||||
return servers
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) Raft() *raft.Raft {
|
||||
return d.server.raft
|
||||
func (s *Server) autopilotServer(m serf.Member) (*autopilot.Server, error) {
|
||||
ok, srv := metadata.IsConsulServer(m)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s.autopilotServerFromMetadata(srv)
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) SerfLAN() *serf.Serf {
|
||||
return d.server.serfLAN
|
||||
}
|
||||
func (s *Server) autopilotServerFromMetadata(srv *metadata.Server) (*autopilot.Server, error) {
|
||||
server := &autopilot.Server{
|
||||
Name: srv.ShortName,
|
||||
ID: raft.ServerID(srv.ID),
|
||||
Address: raft.ServerAddress(srv.Addr.String()),
|
||||
Version: srv.Build.String(),
|
||||
RaftVersion: srv.RaftVersion,
|
||||
Ext: s.autopilotServerExt(srv),
|
||||
}
|
||||
|
||||
func (d *AutopilotDelegate) SerfWAN() *serf.Serf {
|
||||
return d.server.serfWAN
|
||||
switch srv.Status {
|
||||
case serf.StatusLeft:
|
||||
server.NodeStatus = autopilot.NodeLeft
|
||||
case serf.StatusAlive, serf.StatusLeaving:
|
||||
// we want to treat leaving as alive to prevent autopilot from
|
||||
// prematurely removing the node.
|
||||
server.NodeStatus = autopilot.NodeAlive
|
||||
case serf.StatusFailed:
|
||||
server.NodeStatus = autopilot.NodeFailed
|
||||
default:
|
||||
server.NodeStatus = autopilot.NodeUnknown
|
||||
}
|
||||
|
||||
// populate the node meta if there is any. When a node first joins or if
|
||||
// there are ACL issues then this could be empty if the server has not
|
||||
// yet been able to register itself in the catalog
|
||||
_, node, err := s.fsm.State().GetNodeID(types.NodeID(srv.ID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving node from state store: %w", err)
|
||||
}
|
||||
|
||||
if node != nil {
|
||||
server.Meta = node.Meta
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
|
|
@ -1,540 +0,0 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/logging"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/raft"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
// Delegate is the interface for the Autopilot mechanism
|
||||
type Delegate interface {
|
||||
AutopilotConfig() *Config
|
||||
FetchStats(context.Context, []serf.Member) map[string]*ServerStats
|
||||
IsServer(serf.Member) (*ServerInfo, error)
|
||||
NotifyHealth(OperatorHealthReply)
|
||||
PromoteNonVoters(*Config, OperatorHealthReply) ([]raft.Server, error)
|
||||
Raft() *raft.Raft
|
||||
SerfLAN() *serf.Serf
|
||||
SerfWAN() *serf.Serf
|
||||
}
|
||||
|
||||
// Autopilot is a mechanism for automatically managing the Raft
|
||||
// quorum using server health information along with updates from Serf gossip.
|
||||
// For more information, see https://www.consul.io/docs/guides/autopilot.html
|
||||
type Autopilot struct {
|
||||
logger hclog.Logger
|
||||
delegate Delegate
|
||||
|
||||
interval time.Duration
|
||||
healthInterval time.Duration
|
||||
|
||||
clusterHealth OperatorHealthReply
|
||||
clusterHealthLock sync.RWMutex
|
||||
|
||||
enabled bool
|
||||
removeDeadCh chan struct{}
|
||||
shutdownCh chan struct{}
|
||||
shutdownLock sync.Mutex
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
type ServerInfo struct {
|
||||
Name string
|
||||
ID string
|
||||
Addr net.Addr
|
||||
Build version.Version
|
||||
Status serf.MemberStatus
|
||||
}
|
||||
|
||||
func NewAutopilot(logger hclog.Logger, delegate Delegate, interval, healthInterval time.Duration) *Autopilot {
|
||||
return &Autopilot{
|
||||
logger: logger.Named(logging.Autopilot),
|
||||
delegate: delegate,
|
||||
interval: interval,
|
||||
healthInterval: healthInterval,
|
||||
removeDeadCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Autopilot) Start() {
|
||||
a.shutdownLock.Lock()
|
||||
defer a.shutdownLock.Unlock()
|
||||
|
||||
// Nothing to do
|
||||
if a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
a.shutdownCh = make(chan struct{})
|
||||
a.waitGroup = sync.WaitGroup{}
|
||||
a.clusterHealth = OperatorHealthReply{}
|
||||
|
||||
a.waitGroup.Add(2)
|
||||
go a.run()
|
||||
go a.serverHealthLoop()
|
||||
a.enabled = true
|
||||
}
|
||||
|
||||
func (a *Autopilot) Stop() {
|
||||
a.shutdownLock.Lock()
|
||||
defer a.shutdownLock.Unlock()
|
||||
|
||||
// Nothing to do
|
||||
if !a.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
close(a.shutdownCh)
|
||||
a.waitGroup.Wait()
|
||||
a.enabled = false
|
||||
}
|
||||
|
||||
// run periodically looks for nonvoting servers to promote and dead servers to remove.
|
||||
func (a *Autopilot) run() {
|
||||
defer a.waitGroup.Done()
|
||||
|
||||
// Monitor server health until shutdown
|
||||
ticker := time.NewTicker(a.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-a.shutdownCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := a.promoteServers(); err != nil {
|
||||
a.logger.Error("Error promoting servers", "error", err)
|
||||
}
|
||||
|
||||
if err := a.pruneDeadServers(); err != nil {
|
||||
a.logger.Error("Error checking for dead servers to remove", "error", err)
|
||||
}
|
||||
case <-a.removeDeadCh:
|
||||
if err := a.pruneDeadServers(); err != nil {
|
||||
a.logger.Error("Error checking for dead servers to remove", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// promoteServers asks the delegate for any promotions and carries them out.
|
||||
func (a *Autopilot) promoteServers() error {
|
||||
conf := a.delegate.AutopilotConfig()
|
||||
if conf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip the non-voter promotions unless all servers support the new APIs
|
||||
minRaftProtocol, err := a.MinRaftProtocol()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting server raft protocol versions: %s", err)
|
||||
}
|
||||
if minRaftProtocol >= 3 {
|
||||
promotions, err := a.delegate.PromoteNonVoters(conf, a.GetClusterHealth())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking for non-voters to promote: %s", err)
|
||||
}
|
||||
if err := a.handlePromotions(promotions); err != nil {
|
||||
return fmt.Errorf("error handling promotions: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fmtServer prints info about a server in a standard way for logging.
|
||||
func fmtServer(server raft.Server) string {
|
||||
return fmt.Sprintf("Server (ID: %q Address: %q)", server.ID, server.Address)
|
||||
}
|
||||
|
||||
// NumPeers counts the number of voting peers in the given raft config.
|
||||
func NumPeers(raftConfig raft.Configuration) int {
|
||||
var numPeers int
|
||||
for _, server := range raftConfig.Servers {
|
||||
if server.Suffrage == raft.Voter {
|
||||
numPeers++
|
||||
}
|
||||
}
|
||||
return numPeers
|
||||
}
|
||||
|
||||
// RemoveDeadServers triggers a pruning of dead servers in a non-blocking way.
|
||||
func (a *Autopilot) RemoveDeadServers() {
|
||||
select {
|
||||
case a.removeDeadCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func canRemoveServers(peers, minQuorum, deadServers int) (bool, string) {
|
||||
if peers-deadServers < minQuorum {
|
||||
return false, fmt.Sprintf("denied, because removing %d/%d servers would leave less then minimal allowed quorum of %d servers", deadServers, peers, minQuorum)
|
||||
}
|
||||
|
||||
// Only do removals if a minority of servers will be affected.
|
||||
// For failure tolerance of F we need n = 2F+1 servers.
|
||||
// This means we can safely remove up to (n-1)/2 servers.
|
||||
if deadServers > (peers-1)/2 {
|
||||
return false, fmt.Sprintf("denied, because removing the majority of servers %d/%d is not safe", deadServers, peers)
|
||||
}
|
||||
return true, fmt.Sprintf("allowed, because removing %d/%d servers leaves a majority of servers above the minimal allowed quorum %d", deadServers, peers, minQuorum)
|
||||
}
|
||||
|
||||
// pruneDeadServers removes up to numPeers/2 failed servers
|
||||
func (a *Autopilot) pruneDeadServers() error {
|
||||
conf := a.delegate.AutopilotConfig()
|
||||
if conf == nil || !conf.CleanupDeadServers {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Failed servers are known to Serf and marked failed, and stale servers
|
||||
// are known to Raft but not Serf.
|
||||
var failed []serf.Member
|
||||
staleRaftServers := make(map[string]raft.Server)
|
||||
raftNode := a.delegate.Raft()
|
||||
future := raftNode.GetConfiguration()
|
||||
if err := future.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raftConfig := future.Configuration()
|
||||
for _, server := range raftConfig.Servers {
|
||||
staleRaftServers[string(server.Address)] = server
|
||||
}
|
||||
serfWAN := a.delegate.SerfWAN()
|
||||
serfLAN := a.delegate.SerfLAN()
|
||||
for _, member := range serfLAN.Members() {
|
||||
server, err := a.delegate.IsServer(member)
|
||||
if err != nil {
|
||||
a.logger.Warn("Error parsing server info", "name", member.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
if server != nil {
|
||||
// todo(kyhavlov): change this to index by UUID
|
||||
s, found := staleRaftServers[server.Addr.String()]
|
||||
if found {
|
||||
delete(staleRaftServers, server.Addr.String())
|
||||
}
|
||||
|
||||
if member.Status == serf.StatusFailed {
|
||||
// If the node is a nonvoter, we can remove it immediately.
|
||||
if found && s.Suffrage == raft.Nonvoter {
|
||||
a.logger.Info("Attempting removal of failed server node", "name", member.Name)
|
||||
go serfLAN.RemoveFailedNode(member.Name)
|
||||
if serfWAN != nil {
|
||||
go serfWAN.RemoveFailedNode(member.Name)
|
||||
}
|
||||
} else {
|
||||
failed = append(failed, member)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deadServers := len(failed) + len(staleRaftServers)
|
||||
|
||||
// nothing to do
|
||||
if deadServers == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, msg := canRemoveServers(NumPeers(raftConfig), int(conf.MinQuorum), deadServers); !ok {
|
||||
a.logger.Debug("Failed to remove dead servers", "error", msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, node := range failed {
|
||||
a.logger.Info("Attempting removal of failed server node", "name", node.Name)
|
||||
go serfLAN.RemoveFailedNode(node.Name)
|
||||
if serfWAN != nil {
|
||||
go serfWAN.RemoveFailedNode(fmt.Sprintf("%s.%s", node.Name, node.Tags["dc"]))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
minRaftProtocol, err := a.MinRaftProtocol()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, raftServer := range staleRaftServers {
|
||||
a.logger.Info("Attempting removal of stale server", "server", fmtServer(raftServer))
|
||||
var future raft.Future
|
||||
if minRaftProtocol >= 2 {
|
||||
future = raftNode.RemoveServer(raftServer.ID, 0, 0)
|
||||
} else {
|
||||
future = raftNode.RemovePeer(raftServer.Address)
|
||||
}
|
||||
if err := future.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MinRaftProtocol returns the lowest supported Raft protocol among alive servers
|
||||
func (a *Autopilot) MinRaftProtocol() (int, error) {
|
||||
return minRaftProtocol(a.delegate.SerfLAN().Members(), a.delegate.IsServer)
|
||||
}
|
||||
|
||||
func minRaftProtocol(members []serf.Member, serverFunc func(serf.Member) (*ServerInfo, error)) (int, error) {
|
||||
minVersion := -1
|
||||
for _, m := range members {
|
||||
if m.Status != serf.StatusAlive {
|
||||
continue
|
||||
}
|
||||
|
||||
server, err := serverFunc(m)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if server == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
vsn, ok := m.Tags["raft_vsn"]
|
||||
if !ok {
|
||||
vsn = "1"
|
||||
}
|
||||
raftVsn, err := strconv.Atoi(vsn)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if minVersion == -1 || raftVsn < minVersion {
|
||||
minVersion = raftVsn
|
||||
}
|
||||
}
|
||||
|
||||
if minVersion == -1 {
|
||||
return minVersion, fmt.Errorf("No servers found")
|
||||
}
|
||||
|
||||
return minVersion, nil
|
||||
}
|
||||
|
||||
// handlePromotions is a helper shared with Consul Enterprise that attempts to
|
||||
// apply desired server promotions to the Raft configuration.
|
||||
func (a *Autopilot) handlePromotions(promotions []raft.Server) error {
|
||||
// This used to wait to only promote to maintain an odd quorum of
|
||||
// servers, but this was at odds with the dead server cleanup when doing
|
||||
// rolling updates (add one new server, wait, and then kill an old
|
||||
// server). The dead server cleanup would still count the old server as
|
||||
// a peer, which is conservative and the right thing to do, and this
|
||||
// would wait to promote, so you could get into a stalemate. It is safer
|
||||
// to promote early than remove early, so by promoting as soon as
|
||||
// possible we have chosen that as the solution here.
|
||||
for _, server := range promotions {
|
||||
a.logger.Info("Promoting server to voter", "server", fmtServer(server))
|
||||
addFuture := a.delegate.Raft().AddVoter(server.ID, server.Address, 0, 0)
|
||||
if err := addFuture.Error(); err != nil {
|
||||
return fmt.Errorf("failed to add raft peer: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If we promoted a server, trigger a check to remove dead servers.
|
||||
if len(promotions) > 0 {
|
||||
select {
|
||||
case a.removeDeadCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverHealthLoop monitors the health of the servers in the cluster
|
||||
func (a *Autopilot) serverHealthLoop() {
|
||||
defer a.waitGroup.Done()
|
||||
|
||||
// Monitor server health until shutdown
|
||||
ticker := time.NewTicker(a.healthInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-a.shutdownCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := a.updateClusterHealth(); err != nil {
|
||||
a.logger.Error("Error updating cluster health", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateClusterHealth fetches the Raft stats of the other servers and updates
|
||||
// s.clusterHealth based on the configured Autopilot thresholds
|
||||
func (a *Autopilot) updateClusterHealth() error {
|
||||
// Don't do anything if the min Raft version is too low
|
||||
minRaftProtocol, err := a.MinRaftProtocol()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting server raft protocol versions: %s", err)
|
||||
}
|
||||
if minRaftProtocol < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
autopilotConf := a.delegate.AutopilotConfig()
|
||||
// Bail early if autopilot config hasn't been initialized yet
|
||||
if autopilotConf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the the serf members which are Consul servers
|
||||
var serverMembers []serf.Member
|
||||
serverMap := make(map[string]*ServerInfo)
|
||||
for _, member := range a.delegate.SerfLAN().Members() {
|
||||
if member.Status == serf.StatusLeft {
|
||||
continue
|
||||
}
|
||||
|
||||
server, err := a.delegate.IsServer(member)
|
||||
if err != nil {
|
||||
a.logger.Warn("Error parsing server info", "name", member.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
if server != nil {
|
||||
serverMap[server.ID] = server
|
||||
serverMembers = append(serverMembers, member)
|
||||
}
|
||||
}
|
||||
|
||||
raftNode := a.delegate.Raft()
|
||||
future := raftNode.GetConfiguration()
|
||||
if err := future.Error(); err != nil {
|
||||
return fmt.Errorf("error getting Raft configuration %s", err)
|
||||
}
|
||||
servers := future.Configuration().Servers
|
||||
|
||||
// Fetch the health for each of the servers in parallel so we get as
|
||||
// consistent of a sample as possible. We capture the leader's index
|
||||
// here as well so it roughly lines up with the same point in time.
|
||||
targetLastIndex := raftNode.LastIndex()
|
||||
d := time.Now().Add(a.healthInterval / 2)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), d)
|
||||
defer cancel()
|
||||
fetchedStats := a.delegate.FetchStats(ctx, serverMembers)
|
||||
|
||||
// Build a current list of server healths
|
||||
leader := raftNode.Leader()
|
||||
var clusterHealth OperatorHealthReply
|
||||
voterCount := 0
|
||||
healthyCount := 0
|
||||
healthyVoterCount := 0
|
||||
for _, server := range servers {
|
||||
health := ServerHealth{
|
||||
ID: string(server.ID),
|
||||
Address: string(server.Address),
|
||||
Leader: server.Address == leader,
|
||||
LastContact: -1,
|
||||
Voter: server.Suffrage == raft.Voter,
|
||||
}
|
||||
|
||||
parts, ok := serverMap[string(server.ID)]
|
||||
if ok {
|
||||
health.Name = parts.Name
|
||||
health.SerfStatus = parts.Status
|
||||
health.Version = parts.Build.String()
|
||||
if stats, ok := fetchedStats[string(server.ID)]; ok {
|
||||
if err := a.updateServerHealth(&health, parts, stats, autopilotConf, targetLastIndex); err != nil {
|
||||
a.logger.Warn("Error updating server health", "server", fmtServer(server), "error", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
health.SerfStatus = serf.StatusNone
|
||||
}
|
||||
|
||||
if health.Voter {
|
||||
voterCount++
|
||||
}
|
||||
if health.Healthy {
|
||||
healthyCount++
|
||||
if health.Voter {
|
||||
healthyVoterCount++
|
||||
}
|
||||
}
|
||||
|
||||
clusterHealth.Servers = append(clusterHealth.Servers, health)
|
||||
}
|
||||
clusterHealth.Healthy = healthyCount == len(servers)
|
||||
|
||||
// If we have extra healthy voters, update FailureTolerance
|
||||
requiredQuorum := voterCount/2 + 1
|
||||
if healthyVoterCount > requiredQuorum {
|
||||
clusterHealth.FailureTolerance = healthyVoterCount - requiredQuorum
|
||||
}
|
||||
|
||||
a.delegate.NotifyHealth(clusterHealth)
|
||||
|
||||
a.clusterHealthLock.Lock()
|
||||
a.clusterHealth = clusterHealth
|
||||
a.clusterHealthLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateServerHealth computes the resulting health of the server based on its
|
||||
// fetched stats and the state of the leader.
|
||||
func (a *Autopilot) updateServerHealth(health *ServerHealth,
|
||||
server *ServerInfo, stats *ServerStats,
|
||||
autopilotConf *Config, targetLastIndex uint64) error {
|
||||
|
||||
health.LastTerm = stats.LastTerm
|
||||
health.LastIndex = stats.LastIndex
|
||||
|
||||
if stats.LastContact != "never" {
|
||||
var err error
|
||||
health.LastContact, err = time.ParseDuration(stats.LastContact)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing last_contact duration: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
raftNode := a.delegate.Raft()
|
||||
lastTerm, err := strconv.ParseUint(raftNode.Stats()["last_log_term"], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing last_log_term: %s", err)
|
||||
}
|
||||
health.Healthy = health.IsHealthy(lastTerm, targetLastIndex, autopilotConf)
|
||||
|
||||
// If this is a new server or the health changed, reset StableSince
|
||||
lastHealth := a.GetServerHealth(server.ID)
|
||||
if lastHealth == nil || lastHealth.Healthy != health.Healthy {
|
||||
health.StableSince = time.Now()
|
||||
} else {
|
||||
health.StableSince = lastHealth.StableSince
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Autopilot) GetClusterHealth() OperatorHealthReply {
|
||||
a.clusterHealthLock.RLock()
|
||||
defer a.clusterHealthLock.RUnlock()
|
||||
return a.clusterHealth
|
||||
}
|
||||
|
||||
func (a *Autopilot) GetServerHealth(id string) *ServerHealth {
|
||||
a.clusterHealthLock.RLock()
|
||||
defer a.clusterHealthLock.RUnlock()
|
||||
return a.clusterHealth.ServerHealth(id)
|
||||
}
|
||||
|
||||
func IsPotentialVoter(suffrage raft.ServerSuffrage) bool {
|
||||
switch suffrage {
|
||||
case raft.Voter, raft.Staging:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/serf/serf"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMinRaftProtocol(t *testing.T) {
|
||||
t.Parallel()
|
||||
makeMember := func(version string) serf.Member {
|
||||
return serf.Member{
|
||||
Name: "foo",
|
||||
Addr: net.IP([]byte{127, 0, 0, 1}),
|
||||
Tags: map[string]string{
|
||||
"role": "consul",
|
||||
"dc": "dc1",
|
||||
"port": "10000",
|
||||
"vsn": "1",
|
||||
"raft_vsn": version,
|
||||
},
|
||||
Status: serf.StatusAlive,
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
members []serf.Member
|
||||
expected int
|
||||
err error
|
||||
}{
|
||||
// No servers, error
|
||||
{
|
||||
members: []serf.Member{},
|
||||
expected: -1,
|
||||
err: errors.New("No servers found"),
|
||||
},
|
||||
// One server
|
||||
{
|
||||
members: []serf.Member{
|
||||
makeMember("1"),
|
||||
},
|
||||
expected: 1,
|
||||
},
|
||||
// One server, bad version formatting
|
||||
{
|
||||
members: []serf.Member{
|
||||
makeMember("asdf"),
|
||||
},
|
||||
expected: -1,
|
||||
err: errors.New(`strconv.Atoi: parsing "asdf": invalid syntax`),
|
||||
},
|
||||
// Multiple servers, different versions
|
||||
{
|
||||
members: []serf.Member{
|
||||
makeMember("1"),
|
||||
makeMember("2"),
|
||||
},
|
||||
expected: 1,
|
||||
},
|
||||
// Multiple servers, same version
|
||||
{
|
||||
members: []serf.Member{
|
||||
makeMember("2"),
|
||||
makeMember("2"),
|
||||
},
|
||||
expected: 2,
|
||||
},
|
||||
}
|
||||
|
||||
serverFunc := func(m serf.Member) (*ServerInfo, error) {
|
||||
return &ServerInfo{}, nil
|
||||
}
|
||||
for _, tc := range cases {
|
||||
result, err := minRaftProtocol(tc.members, serverFunc)
|
||||
if result != tc.expected {
|
||||
t.Fatalf("bad: %v, %v, %v", result, tc.expected, tc)
|
||||
}
|
||||
if tc.err != nil {
|
||||
if err == nil || tc.err.Error() != err.Error() {
|
||||
t.Fatalf("bad: %v, %v, %v", err, tc.err, tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutopilot_canRemoveServers(t *testing.T) {
|
||||
type test struct {
|
||||
peers int
|
||||
minQuorum int
|
||||
deadServers int
|
||||
ok bool
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{1, 1, 1, false},
|
||||
{3, 3, 1, false},
|
||||
{4, 3, 3, false},
|
||||
{5, 3, 3, false},
|
||||
{5, 3, 2, true},
|
||||
{5, 3, 1, true},
|
||||
{9, 3, 5, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
ok, msg := canRemoveServers(test.peers, test.minQuorum, test.deadServers)
|
||||
require.Equal(t, test.ok, ok)
|
||||
t.Logf("%+v: %s", test, msg)
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
)
|
||||
|
||||
// PromoteStableServers is a basic autopilot promotion policy that promotes any
|
||||
// server which has been healthy and stable for the duration specified in the
|
||||
// given Autopilot config.
|
||||
func PromoteStableServers(autopilotConfig *Config, health OperatorHealthReply, servers []raft.Server) []raft.Server {
|
||||
// Find any non-voters eligible for promotion.
|
||||
now := time.Now()
|
||||
var promotions []raft.Server
|
||||
for _, server := range servers {
|
||||
if !IsPotentialVoter(server.Suffrage) {
|
||||
health := health.ServerHealth(string(server.ID))
|
||||
if health.IsStable(now, autopilotConfig) {
|
||||
promotions = append(promotions, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return promotions
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPromotion(t *testing.T) {
|
||||
config := &Config{
|
||||
LastContactThreshold: 5 * time.Second,
|
||||
MaxTrailingLogs: 100,
|
||||
ServerStabilizationTime: 3 * time.Second,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
conf *Config
|
||||
health OperatorHealthReply
|
||||
servers []raft.Server
|
||||
promotions []raft.Server
|
||||
}{
|
||||
{
|
||||
name: "one stable voter, no promotions",
|
||||
conf: config,
|
||||
health: OperatorHealthReply{
|
||||
Servers: []ServerHealth{
|
||||
{
|
||||
ID: "a",
|
||||
Healthy: true,
|
||||
StableSince: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
servers: []raft.Server{
|
||||
{ID: "a", Suffrage: raft.Voter},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one stable nonvoter, should be promoted",
|
||||
conf: config,
|
||||
health: OperatorHealthReply{
|
||||
Servers: []ServerHealth{
|
||||
{
|
||||
ID: "a",
|
||||
Healthy: true,
|
||||
StableSince: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
{
|
||||
ID: "b",
|
||||
Healthy: true,
|
||||
StableSince: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
servers: []raft.Server{
|
||||
{ID: "a", Suffrage: raft.Voter},
|
||||
{ID: "b", Suffrage: raft.Nonvoter},
|
||||
},
|
||||
promotions: []raft.Server{
|
||||
{ID: "b", Suffrage: raft.Nonvoter},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unstable servers, neither should be promoted",
|
||||
conf: config,
|
||||
health: OperatorHealthReply{
|
||||
Servers: []ServerHealth{
|
||||
{
|
||||
ID: "a",
|
||||
Healthy: true,
|
||||
StableSince: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
{
|
||||
ID: "b",
|
||||
Healthy: false,
|
||||
StableSince: time.Now().Add(-10 * time.Second),
|
||||
},
|
||||
{
|
||||
ID: "c",
|
||||
Healthy: true,
|
||||
StableSince: time.Now().Add(-1 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
servers: []raft.Server{
|
||||
{ID: "a", Suffrage: raft.Voter},
|
||||
{ID: "b", Suffrage: raft.Nonvoter},
|
||||
{ID: "c", Suffrage: raft.Nonvoter},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
promotions := PromoteStableServers(tc.conf, tc.health, tc.servers)
|
||||
require.Equal(t, tc.promotions, promotions)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package autopilot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
func TestServerHealth_IsHealthy(t *testing.T) {
|
||||
cases := []struct {
|
||||
health ServerHealth
|
||||
lastTerm uint64
|
||||
lastIndex uint64
|
||||
conf Config
|
||||
expected bool
|
||||
}{
|
||||
// Healthy server, all values within allowed limits
|
||||
{
|
||||
health: ServerHealth{SerfStatus: serf.StatusAlive, LastTerm: 1, LastIndex: 0},
|
||||
lastTerm: 1,
|
||||
lastIndex: 10,
|
||||
conf: Config{MaxTrailingLogs: 20},
|
||||
expected: true,
|
||||
},
|
||||
// Serf status failed
|
||||
{
|
||||
health: ServerHealth{SerfStatus: serf.StatusFailed},
|
||||
expected: false,
|
||||
},
|
||||
// Old value for lastTerm
|
||||
{
|
||||
health: ServerHealth{SerfStatus: serf.StatusAlive, LastTerm: 0},
|
||||
lastTerm: 1,
|
||||
expected: false,
|
||||
},
|
||||
// Too far behind on logs
|
||||
{
|
||||
health: ServerHealth{SerfStatus: serf.StatusAlive, LastIndex: 0},
|
||||
lastIndex: 10,
|
||||
conf: Config{MaxTrailingLogs: 5},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for index, tc := range cases {
|
||||
actual := tc.health.IsHealthy(tc.lastTerm, tc.lastIndex, &tc.conf)
|
||||
if actual != tc.expected {
|
||||
t.Fatalf("bad value for case %d: %v", index, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerHealth_IsStable(t *testing.T) {
|
||||
start := time.Now()
|
||||
cases := []struct {
|
||||
health *ServerHealth
|
||||
now time.Time
|
||||
conf Config
|
||||
expected bool
|
||||
}{
|
||||
// Healthy server, all values within allowed limits
|
||||
{
|
||||
health: &ServerHealth{Healthy: true, StableSince: start},
|
||||
now: start.Add(15 * time.Second),
|
||||
conf: Config{ServerStabilizationTime: 10 * time.Second},
|
||||
expected: true,
|
||||
},
|
||||
// Unhealthy server
|
||||
{
|
||||
health: &ServerHealth{Healthy: false},
|
||||
expected: false,
|
||||
},
|
||||
// Healthy server, hasn't reached stabilization time
|
||||
{
|
||||
health: &ServerHealth{Healthy: true, StableSince: start},
|
||||
now: start.Add(5 * time.Second),
|
||||
conf: Config{ServerStabilizationTime: 10 * time.Second},
|
||||
expected: false,
|
||||
},
|
||||
// Nil struct
|
||||
{
|
||||
health: nil,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for index, tc := range cases {
|
||||
actual := tc.health.IsStable(tc.now, &tc.conf)
|
||||
if actual != tc.expected {
|
||||
t.Fatalf("bad value for case %d: %v", index, actual)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,15 @@
|
|||
|
||||
package consul
|
||||
|
||||
import "github.com/hashicorp/consul/agent/consul/autopilot"
|
||||
import (
|
||||
"github.com/hashicorp/consul/agent/metadata"
|
||||
autopilot "github.com/hashicorp/raft-autopilot"
|
||||
)
|
||||
|
||||
func (s *Server) initAutopilot(config *Config) {
|
||||
apDelegate := &AutopilotDelegate{s}
|
||||
s.autopilot = autopilot.NewAutopilot(s.logger, apDelegate, config.AutopilotInterval, config.ServerHealthInterval)
|
||||
func (s *Server) autopilotPromoter() autopilot.Promoter {
|
||||
return autopilot.DefaultPromoter()
|
||||
}
|
||||
|
||||
func (_ *Server) autopilotServerExt(_ *metadata.Server) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
"github.com/hashicorp/raft"
|
||||
|
@ -18,28 +20,20 @@ func TestAutopilot_IdempotentShutdown(t *testing.T) {
|
|||
defer s1.Shutdown()
|
||||
retry.Run(t, func(r *retry.R) { r.Check(waitForLeader(s1)) })
|
||||
|
||||
s1.autopilot.Start()
|
||||
s1.autopilot.Start()
|
||||
s1.autopilot.Start()
|
||||
s1.autopilot.Stop()
|
||||
s1.autopilot.Stop()
|
||||
s1.autopilot.Stop()
|
||||
s1.autopilot.Start(context.Background())
|
||||
s1.autopilot.Start(context.Background())
|
||||
s1.autopilot.Start(context.Background())
|
||||
<-s1.autopilot.Stop()
|
||||
<-s1.autopilot.Stop()
|
||||
<-s1.autopilot.Stop()
|
||||
}
|
||||
|
||||
func TestAutopilot_CleanupDeadServer(t *testing.T) {
|
||||
t.Parallel()
|
||||
for i := 1; i <= 3; i++ {
|
||||
testCleanupDeadServer(t, i)
|
||||
}
|
||||
}
|
||||
|
||||
func testCleanupDeadServer(t *testing.T, raftVersion int) {
|
||||
dc := "dc1"
|
||||
conf := func(c *Config) {
|
||||
c.Datacenter = dc
|
||||
c.Bootstrap = false
|
||||
c.BootstrapExpect = 5
|
||||
c.RaftConfig.ProtocolVersion = raft.ProtocolVersion(raftVersion)
|
||||
}
|
||||
dir1, s1 := testServerWithConfig(t, conf)
|
||||
defer os.RemoveAll(dir1)
|
||||
|
@ -119,10 +113,19 @@ func testCleanupDeadServer(t *testing.T, raftVersion int) {
|
|||
}
|
||||
|
||||
func TestAutopilot_CleanupDeadNonvoter(t *testing.T) {
|
||||
dir1, s1 := testServer(t)
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.AutopilotConfig = &structs.AutopilotConfig{
|
||||
CleanupDeadServers: true,
|
||||
ServerStabilizationTime: 100 * time.Millisecond,
|
||||
}
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
// we have to wait for autopilot to be running long enough for the server stabilization time
|
||||
// to kick in for this test to work.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
dir2, s2 := testServerDCBootstrap(t, "dc1", false)
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
@ -316,7 +319,7 @@ func TestAutopilot_CleanupStaleRaftServer(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify we have 4 peers
|
||||
peers, err := s1.numPeers()
|
||||
peers, err := s1.autopilot.NumVoters()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -335,7 +338,6 @@ func TestAutopilot_PromoteNonVoter(t *testing.T) {
|
|||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc1"
|
||||
c.Bootstrap = true
|
||||
c.RaftConfig.ProtocolVersion = 3
|
||||
c.AutopilotConfig.ServerStabilizationTime = 200 * time.Millisecond
|
||||
c.ServerHealthInterval = 100 * time.Millisecond
|
||||
c.AutopilotInterval = 100 * time.Millisecond
|
||||
|
@ -346,6 +348,10 @@ func TestAutopilot_PromoteNonVoter(t *testing.T) {
|
|||
defer codec.Close()
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
// this may seem arbitrary but we need to get past the server stabilization time
|
||||
// so that we start factoring in that time for newly connected nodes.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc1"
|
||||
c.Bootstrap = false
|
||||
|
@ -370,7 +376,7 @@ func TestAutopilot_PromoteNonVoter(t *testing.T) {
|
|||
if servers[1].Suffrage != raft.Nonvoter {
|
||||
r.Fatalf("bad: %v", servers)
|
||||
}
|
||||
health := s1.autopilot.GetServerHealth(string(servers[1].ID))
|
||||
health := s1.autopilot.GetServerHealth(servers[1].ID)
|
||||
if health == nil {
|
||||
r.Fatal("nil health")
|
||||
}
|
||||
|
@ -406,7 +412,6 @@ func TestAutopilot_MinQuorum(t *testing.T) {
|
|||
c.Bootstrap = false
|
||||
c.BootstrapExpect = 4
|
||||
c.AutopilotConfig.MinQuorum = 3
|
||||
c.RaftConfig.ProtocolVersion = raft.ProtocolVersion(2)
|
||||
c.AutopilotInterval = 100 * time.Millisecond
|
||||
}
|
||||
dir1, s1 := testServerWithConfig(t, conf)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -17,6 +18,52 @@ import (
|
|||
"github.com/hashicorp/go-uuid"
|
||||
)
|
||||
|
||||
var CatalogCounters = []prometheus.CounterDefinition{
|
||||
{
|
||||
Name: []string{"catalog", "service", "query"},
|
||||
Help: "Increments for each catalog query for the given service.",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "connect", "query"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "service", "query-tag"},
|
||||
Help: "Increments for each catalog query for the given service with the given tag.",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "connect", "query-tag"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "service", "query-tags"},
|
||||
Help: "Increments for each catalog query for the given service with the given tags.",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "connect", "query-tags"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "service", "not-found"},
|
||||
Help: "Increments for each catalog query where the given service could not be found.",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "connect", "not-found"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
var CatalogSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"catalog", "deregister"},
|
||||
Help: "Measures the time it takes to complete a catalog deregister operation.",
|
||||
},
|
||||
{
|
||||
Name: []string{"catalog", "register"},
|
||||
Help: "Measures the time it takes to complete a catalog register operation.",
|
||||
},
|
||||
}
|
||||
|
||||
// Catalog endpoint is used to manipulate the service catalog
|
||||
type Catalog struct {
|
||||
srv *Server
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/agent/pool"
|
||||
"github.com/hashicorp/consul/agent/router"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -21,6 +22,21 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var ClientCounters = []prometheus.CounterDefinition{
|
||||
{
|
||||
Name: []string{"client", "rpc"},
|
||||
Help: "Increments whenever a Consul agent in client mode makes an RPC request to a Consul server.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "exceeded"},
|
||||
Help: "Increments whenever a Consul agent in client mode makes an RPC request to a Consul server gets rate limited by that agent's limits configuration.",
|
||||
},
|
||||
{
|
||||
Name: []string{"client", "rpc", "failed"},
|
||||
Help: "Increments whenever a Consul agent in client mode makes an RPC request to a Consul server and fails.",
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
// serfEventBacklog is the maximum number of unprocessed Serf Events
|
||||
// that will be held in queue before new serf events block. A
|
||||
|
|
|
@ -145,8 +145,8 @@ func TestClient_LANReap(t *testing.T) {
|
|||
// Check the router has both
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
server := c1.router.FindLANServer()
|
||||
require.NotNil(t, server)
|
||||
require.Equal(t, s1.config.NodeName, server.Name)
|
||||
require.NotNil(r, server)
|
||||
require.Equal(r, s1.config.NodeName, server.Name)
|
||||
})
|
||||
|
||||
// shutdown the second dc
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/agent/checks"
|
||||
"github.com/hashicorp/consul/agent/consul/autopilot"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
libserf "github.com/hashicorp/consul/lib/serf"
|
||||
"github.com/hashicorp/consul/tlsutil"
|
||||
|
@ -111,9 +110,9 @@ type Config struct {
|
|||
// RaftConfig is the configuration used for Raft in the local DC
|
||||
RaftConfig *raft.Config
|
||||
|
||||
// (Enterprise-only) NonVoter is used to prevent this server from being added
|
||||
// (Enterprise-only) ReadReplica is used to prevent this server from being added
|
||||
// as a voting member of the Raft cluster.
|
||||
NonVoter bool
|
||||
ReadReplica bool
|
||||
|
||||
// NotifyListen is called after the RPC listener has been configured.
|
||||
// RPCAdvertise will be set to the listener address if it hasn't been
|
||||
|
@ -269,8 +268,8 @@ type Config struct {
|
|||
|
||||
// ACLDefaultPolicy is used to control the ACL interaction when
|
||||
// there is no defined policy. This can be "allow" which means
|
||||
// ACLs are used to black-list, or "deny" which means ACLs are
|
||||
// white-lists.
|
||||
// ACLs are used to deny-list, or "deny" which means ACLs are
|
||||
// allow-lists.
|
||||
ACLDefaultPolicy string
|
||||
|
||||
// ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter
|
||||
|
@ -438,7 +437,7 @@ type Config struct {
|
|||
|
||||
// AutopilotConfig is used to apply the initial autopilot config when
|
||||
// bootstrapping.
|
||||
AutopilotConfig *autopilot.Config
|
||||
AutopilotConfig *structs.AutopilotConfig
|
||||
|
||||
// ServerHealthInterval is the frequency with which the health of the
|
||||
// servers in the cluster will be updated.
|
||||
|
@ -590,7 +589,7 @@ func DefaultConfig() *Config {
|
|||
|
||||
// TODO (slackpad) - Until #3744 is done, we need to keep these
|
||||
// in sync with agent/config/default.go.
|
||||
AutopilotConfig: &autopilot.Config{
|
||||
AutopilotConfig: &structs.AutopilotConfig{
|
||||
CleanupDeadServers: true,
|
||||
LastContactThreshold: 200 * time.Millisecond,
|
||||
MaxTrailingLogs: 250,
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
|
@ -12,6 +14,33 @@ import (
|
|||
"github.com/mitchellh/copystructure"
|
||||
)
|
||||
|
||||
var ConfigSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"config_entry", "apply"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"config_entry", "get"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"config_entry", "list"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"config_entry", "listAll"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"config_entry", "delete"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"config_entry", "resolve_service_config"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
// The ConfigEntry endpoint is used to query centralized config information
|
||||
type ConfigEntry struct {
|
||||
srv *Server
|
||||
|
@ -19,10 +48,6 @@ type ConfigEntry struct {
|
|||
|
||||
// Apply does an upsert of the given config entry.
|
||||
func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error {
|
||||
return c.applyInternal(args, reply, nil)
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) applyInternal(args *structs.ConfigEntryRequest, reply *bool, normalizeAndValidateFn func(structs.ConfigEntry) error) error {
|
||||
if err := c.srv.validateEnterpriseRequest(args.Entry.GetEnterpriseMeta(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -47,17 +72,11 @@ func (c *ConfigEntry) applyInternal(args *structs.ConfigEntryRequest, reply *boo
|
|||
}
|
||||
|
||||
// Normalize and validate the incoming config entry as if it came from a user.
|
||||
if normalizeAndValidateFn == nil {
|
||||
if err := args.Entry.Normalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := args.Entry.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := normalizeAndValidateFn(args.Entry); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := args.Entry.Normalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := args.Entry.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if authz != nil && !args.Entry.CanWrite(authz) {
|
||||
|
@ -454,6 +473,11 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r
|
|||
func (c *ConfigEntry) preflightCheck(kind string) error {
|
||||
switch kind {
|
||||
case structs.ServiceIntentions:
|
||||
// Exit early if Connect hasn't been enabled.
|
||||
if !c.srv.config.ConnectEnabled {
|
||||
return ErrConnectNotEnabled
|
||||
}
|
||||
|
||||
usingConfigEntries, err := c.srv.fsm.State().AreIntentionsInConfigEntries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("system metadata lookup failed: %v", err)
|
||||
|
|
|
@ -11,12 +11,8 @@ import (
|
|||
"github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
func cmpConfigLess(first structs.ConfigEntry, second structs.ConfigEntry) bool {
|
||||
return first.GetKind() < second.GetKind() || (first.GetKind() == second.GetKind() && first.GetName() < second.GetName())
|
||||
}
|
||||
|
||||
func configSort(configs []structs.ConfigEntry) {
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
sort.SliceStable(configs, func(i, j int) bool {
|
||||
return cmpConfigLess(configs[i], configs[j])
|
||||
})
|
||||
}
|
||||
|
@ -25,12 +21,14 @@ func diffConfigEntries(local []structs.ConfigEntry, remote []structs.ConfigEntry
|
|||
configSort(local)
|
||||
configSort(remote)
|
||||
|
||||
var deletions []structs.ConfigEntry
|
||||
var updates []structs.ConfigEntry
|
||||
var localIdx int
|
||||
var remoteIdx int
|
||||
var (
|
||||
deletions []structs.ConfigEntry
|
||||
updates []structs.ConfigEntry
|
||||
localIdx int
|
||||
remoteIdx int
|
||||
)
|
||||
for localIdx, remoteIdx = 0, 0; localIdx < len(local) && remoteIdx < len(remote); {
|
||||
if local[localIdx].GetKind() == remote[remoteIdx].GetKind() && local[localIdx].GetName() == remote[remoteIdx].GetName() {
|
||||
if configSameID(local[localIdx], remote[remoteIdx]) {
|
||||
// config is in both the local and remote state - need to check raft indices
|
||||
if remote[remoteIdx].GetRaftIndex().ModifyIndex > lastRemoteIndex {
|
||||
updates = append(updates, remote[remoteIdx])
|
||||
|
@ -64,6 +62,30 @@ func diffConfigEntries(local []structs.ConfigEntry, remote []structs.ConfigEntry
|
|||
return deletions, updates
|
||||
}
|
||||
|
||||
func cmpConfigLess(first structs.ConfigEntry, second structs.ConfigEntry) bool {
|
||||
if first.GetKind() < second.GetKind() {
|
||||
return true
|
||||
}
|
||||
if first.GetKind() > second.GetKind() {
|
||||
return false
|
||||
}
|
||||
|
||||
if first.GetEnterpriseMeta().LessThan(second.GetEnterpriseMeta()) {
|
||||
return true
|
||||
}
|
||||
if second.GetEnterpriseMeta().LessThan(first.GetEnterpriseMeta()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return first.GetName() < second.GetName()
|
||||
}
|
||||
|
||||
func configSameID(e1, e2 structs.ConfigEntry) bool {
|
||||
return e1.GetKind() == e2.GetKind() &&
|
||||
e1.GetEnterpriseMeta().IsSame(e2.GetEnterpriseMeta()) &&
|
||||
e1.GetName() == e2.GetName()
|
||||
}
|
||||
|
||||
func (s *Server) reconcileLocalConfig(ctx context.Context, configs []structs.ConfigEntry, op structs.ConfigEntryOp) (bool, error) {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(s.config.ConfigReplicationApplyLimit))
|
||||
defer ticker.Stop()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
|
@ -11,6 +12,85 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReplication_ConfigSort(t *testing.T) {
|
||||
newDefaults := func(name, protocol string) *structs.ServiceConfigEntry {
|
||||
return &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: name,
|
||||
Protocol: protocol,
|
||||
}
|
||||
}
|
||||
newResolver := func(name string, timeout time.Duration) *structs.ServiceResolverConfigEntry {
|
||||
return &structs.ServiceResolverConfigEntry{
|
||||
Kind: structs.ServiceResolver,
|
||||
Name: name,
|
||||
ConnectTimeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
configs []structs.ConfigEntry
|
||||
expect []structs.ConfigEntry
|
||||
}
|
||||
|
||||
cases := map[string]testcase{
|
||||
"none": {},
|
||||
"one": {
|
||||
configs: []structs.ConfigEntry{
|
||||
newDefaults("web", "grpc"),
|
||||
},
|
||||
expect: []structs.ConfigEntry{
|
||||
newDefaults("web", "grpc"),
|
||||
},
|
||||
},
|
||||
"just kinds": {
|
||||
configs: []structs.ConfigEntry{
|
||||
newResolver("web", 33*time.Second),
|
||||
newDefaults("web", "grpc"),
|
||||
},
|
||||
expect: []structs.ConfigEntry{
|
||||
newDefaults("web", "grpc"),
|
||||
newResolver("web", 33*time.Second),
|
||||
},
|
||||
},
|
||||
"just names": {
|
||||
configs: []structs.ConfigEntry{
|
||||
newDefaults("db", "grpc"),
|
||||
newDefaults("api", "http2"),
|
||||
},
|
||||
expect: []structs.ConfigEntry{
|
||||
newDefaults("api", "http2"),
|
||||
newDefaults("db", "grpc"),
|
||||
},
|
||||
},
|
||||
"all": {
|
||||
configs: []structs.ConfigEntry{
|
||||
newResolver("web", 33*time.Second),
|
||||
newDefaults("web", "grpc"),
|
||||
newDefaults("db", "grpc"),
|
||||
newDefaults("api", "http2"),
|
||||
},
|
||||
expect: []structs.ConfigEntry{
|
||||
newDefaults("api", "http2"),
|
||||
newDefaults("db", "grpc"),
|
||||
newDefaults("web", "grpc"),
|
||||
newResolver("web", 33*time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
configSort(tc.configs)
|
||||
require.Equal(t, tc.expect, tc.configs)
|
||||
// and it should be stable
|
||||
configSort(tc.configs)
|
||||
require.Equal(t, tc.expect, tc.configs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplication_ConfigEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
|
|
|
@ -67,7 +67,7 @@ func (s *ConnectCA) ConfigurationGet(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rule != nil && rule.OperatorRead(nil) != acl.Allow {
|
||||
if rule != nil && rule.OperatorWrite(nil) != acl.Allow {
|
||||
return acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
ca "github.com/hashicorp/consul/agent/connect/ca"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -18,6 +17,7 @@ import (
|
|||
"github.com/hashicorp/consul/testrpc"
|
||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testParseCert(t *testing.T, pemValue string) *x509.Certificate {
|
||||
|
@ -144,6 +144,93 @@ func TestConnectCAConfig_GetSet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConnectCAConfig_GetSet_ACLDeny(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLsEnabled = true
|
||||
c.ACLMasterToken = TestDefaultMasterToken
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
opReadToken, err := upsertTestTokenWithPolicyRules(
|
||||
codec, TestDefaultMasterToken, "dc1", `operator = "read"`)
|
||||
require.NoError(t, err)
|
||||
|
||||
opWriteToken, err := upsertTestTokenWithPolicyRules(
|
||||
codec, TestDefaultMasterToken, "dc1", `operator = "write"`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update a config value
|
||||
newConfig := &structs.CAConfiguration{
|
||||
Provider: "consul",
|
||||
Config: map[string]interface{}{
|
||||
"PrivateKey": `
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49
|
||||
AwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav
|
||||
q5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==
|
||||
-----END EC PRIVATE KEY-----`,
|
||||
"RootCert": `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ
|
||||
VGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG
|
||||
A1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR
|
||||
AB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7
|
||||
SkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD
|
||||
AgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6
|
||||
NDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6
|
||||
NWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf
|
||||
ZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6
|
||||
ZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw
|
||||
WQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1
|
||||
NTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG
|
||||
SM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA
|
||||
pY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=
|
||||
-----END CERTIFICATE-----`,
|
||||
},
|
||||
}
|
||||
|
||||
args := &structs.CARequest{
|
||||
Datacenter: "dc1",
|
||||
Config: newConfig,
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
var reply interface{}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
|
||||
|
||||
t.Run("deny get with operator:read", func(t *testing.T) {
|
||||
args := &structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Token: opReadToken.SecretID},
|
||||
}
|
||||
|
||||
var reply structs.CAConfiguration
|
||||
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)
|
||||
assert.True(t, acl.IsErrPermissionDenied(err))
|
||||
})
|
||||
|
||||
t.Run("allow get with operator:write", func(t *testing.T) {
|
||||
args := &structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Token: opWriteToken.SecretID},
|
||||
}
|
||||
|
||||
var reply structs.CAConfiguration
|
||||
err = msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationGet", args, &reply)
|
||||
assert.False(t, acl.IsErrPermissionDenied(err))
|
||||
assert.Equal(t, newConfig.Config, reply.Config)
|
||||
})
|
||||
}
|
||||
|
||||
// This test case tests that the logic around forcing a rotation without cross
|
||||
// signing works when requested (and is denied when not requested). This occurs
|
||||
// if the current CA is not able to cross sign external CA certificates.
|
||||
|
|
|
@ -5,13 +5,33 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
)
|
||||
|
||||
var FederationStateSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"federation_state", "apply"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"federation_state", "get"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"federation_state", "list"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"federation_state", "list_mesh_gateways"},
|
||||
Help: "",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
errFederationStatesNotEnabled = errors.New("Federation states are currently disabled until all servers in the datacenter support the feature")
|
||||
)
|
||||
|
|
|
@ -4,11 +4,102 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
var CommandsSummaries = []prometheus.SummaryDefinition{
|
||||
{
|
||||
Name: []string{"fsm", "register"},
|
||||
Help: "Measures the time it takes to apply a catalog register operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "deregister"},
|
||||
Help: "Measures the time it takes to apply a catalog deregister operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "kvs"},
|
||||
Help: "Measures the time it takes to apply the given KV operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "session"},
|
||||
Help: "Measures the time it takes to apply the given session operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "acl"},
|
||||
Help: "Measures the time it takes to apply the given ACL operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "tombstone"},
|
||||
Help: "Measures the time it takes to apply the given tombstone operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "coordinate", "batch-update"},
|
||||
Help: "Measures the time it takes to apply the given batch coordinate update to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "prepared-query"},
|
||||
Help: "Measures the time it takes to apply the given prepared query update operation to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "txn"},
|
||||
Help: "Measures the time it takes to apply the given transaction update to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "autopilot"},
|
||||
Help: "Measures the time it takes to apply the given autopilot update to the FSM.",
|
||||
},
|
||||
{
|
||||
Name: []string{"consul", "fsm", "intention"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "intention"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"consul", "fsm", "ca"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "ca", "leaf"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "acl", "token"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "ca", "leaf"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "acl", "policy"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "acl", "bindingrule"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "acl", "authmethod"},
|
||||
Help: "",
|
||||
},
|
||||
{
|
||||
Name: []string{"fsm", "system_metadata"},
|
||||
Help: "",
|
||||
},
|
||||
// TODO(kit): We generate the config-entry fsm summaries by reading off of the request. It is
|
||||
// possible to statically declare these when we know all of the names, but I didn't get to it
|
||||
// in this patch. Config-entries are known though and we should add these in the future.
|
||||
// {
|
||||
// Name: []string{"fsm", "config_entry", req.Entry.GetKind()},
|
||||
// Help: "",
|
||||
// },
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerCommand(structs.RegisterRequestType, (*FSM).applyRegister)
|
||||
registerCommand(structs.DeregisterRequestType, (*FSM).applyDeregister)
|
||||
|
@ -291,6 +382,11 @@ func (c *FSM) applyIntentionOperation(buf []byte, index uint64) interface{} {
|
|||
[]metrics.Label{{Name: "op", Value: string(req.Op)}})
|
||||
defer metrics.MeasureSinceWithLabels([]string{"fsm", "intention"}, time.Now(),
|
||||
[]metrics.Label{{Name: "op", Value: string(req.Op)}})
|
||||
|
||||
if req.Mutation != nil {
|
||||
return c.state.IntentionMutation(index, req.Op, req.Mutation)
|
||||
}
|
||||
|
||||
switch req.Op {
|
||||
case structs.IntentionOpCreate, structs.IntentionOpUpdate:
|
||||
//nolint:staticcheck
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/consul/autopilot"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
|
@ -1163,7 +1162,7 @@ func TestFSM_Autopilot(t *testing.T) {
|
|||
// Set the autopilot config using a request.
|
||||
req := structs.AutopilotSetConfigRequest{
|
||||
Datacenter: "dc1",
|
||||
Config: autopilot.Config{
|
||||
Config: structs.AutopilotConfig{
|
||||
CleanupDeadServers: true,
|
||||
LastContactThreshold: 10 * time.Second,
|
||||
MaxTrailingLogs: 300,
|
||||
|
|
|
@ -6,13 +6,14 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-msgpack/codec"
|
||||
"github.com/hashicorp/go-raftchunking"
|
||||
"github.com/hashicorp/raft"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
)
|
||||
|
||||
// command is a command method on the FSM.
|
||||
|
@ -41,7 +42,9 @@ func registerCommand(msg structs.MessageType, fn unboundCommand) {
|
|||
// along with Raft to provide strong consistency. We implement
|
||||
// this outside the Server to avoid exposing this outside the package.
|
||||
type FSM struct {
|
||||
logger hclog.Logger
|
||||
deps Deps
|
||||
logger hclog.Logger
|
||||
chunker *raftchunking.ChunkingFSM
|
||||
|
||||
// apply is built off the commands global and is used to route apply
|
||||
// operations to their appropriate handlers.
|
||||
|
@ -53,28 +56,40 @@ type FSM struct {
|
|||
// Raft side, so doesn't need to lock this.
|
||||
stateLock sync.RWMutex
|
||||
state *state.Store
|
||||
|
||||
gc *state.TombstoneGC
|
||||
|
||||
chunker *raftchunking.ChunkingFSM
|
||||
}
|
||||
|
||||
// New is used to construct a new FSM with a blank state.
|
||||
//
|
||||
// Deprecated: use NewFromDeps.
|
||||
func New(gc *state.TombstoneGC, logger hclog.Logger) (*FSM, error) {
|
||||
if logger == nil {
|
||||
logger = hclog.New(&hclog.LoggerOptions{})
|
||||
newStateStore := func() *state.Store {
|
||||
return state.NewStateStore(gc)
|
||||
}
|
||||
return NewFromDeps(Deps{Logger: logger, NewStateStore: newStateStore}), nil
|
||||
}
|
||||
|
||||
stateNew, err := state.NewStateStore(gc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Deps are dependencies used to construct the FSM.
|
||||
type Deps struct {
|
||||
// Logger used to emit log messages
|
||||
Logger hclog.Logger
|
||||
// NewStateStore returns a state.Store which the FSM will use to make changes
|
||||
// to the state.
|
||||
// NewStateStore will be called once when the FSM is created and again any
|
||||
// time Restore() is called.
|
||||
NewStateStore func() *state.Store
|
||||
}
|
||||
|
||||
// NewFromDeps creates a new FSM from its dependencies.
|
||||
func NewFromDeps(deps Deps) *FSM {
|
||||
if deps.Logger == nil {
|
||||
deps.Logger = hclog.New(&hclog.LoggerOptions{})
|
||||
}
|
||||
|
||||
fsm := &FSM{
|
||||
logger: logger.Named(logging.FSM),
|
||||
deps: deps,
|
||||
logger: deps.Logger.Named(logging.FSM),
|
||||
apply: make(map[structs.MessageType]command),
|
||||
state: stateNew,
|
||||
gc: gc,
|
||||
state: deps.NewStateStore(),
|
||||
}
|
||||
|
||||
// Build out the apply dispatch table based on the registered commands.
|
||||
|
@ -86,8 +101,7 @@ func New(gc *state.TombstoneGC, logger hclog.Logger) (*FSM, error) {
|
|||
}
|
||||
|
||||
fsm.chunker = raftchunking.NewChunkingFSM(fsm, nil)
|
||||
|
||||
return fsm, nil
|
||||
return fsm
|
||||
}
|
||||
|
||||
func (c *FSM) ChunkingFSM() *raftchunking.ChunkingFSM {
|
||||
|
@ -149,11 +163,7 @@ func (c *FSM) Snapshot() (raft.FSMSnapshot, error) {
|
|||
func (c *FSM) Restore(old io.ReadCloser) error {
|
||||
defer old.Close()
|
||||
|
||||
// Create a new state store.
|
||||
stateNew, err := state.NewStateStore(c.gc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stateNew := c.deps.NewStateStore()
|
||||
|
||||
// Set up a new restore transaction
|
||||
restore := stateNew.Restore()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue