diff --git a/.changelog/11742.txt b/.changelog/11742.txt new file mode 100644 index 000000000..6c6d4c249 --- /dev/null +++ b/.changelog/11742.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Add filtering support to Catalog's List Services (v1/catalog/services) +``` diff --git a/.changelog/12905.txt b/.changelog/12905.txt new file mode 100644 index 000000000..7b2889f24 --- /dev/null +++ b/.changelog/12905.txt @@ -0,0 +1,3 @@ +```release-note:improvement +metrics: Service RPC calls less than 1ms are now emitted as a decimal number. +``` diff --git a/.changelog/13493.txt b/.changelog/13493.txt new file mode 100644 index 000000000..9c3eec605 --- /dev/null +++ b/.changelog/13493.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: Fix Consul kv CLI 'GET' flags 'keys' and 'recurse' to be set together +``` diff --git a/.changelog/13998.txt b/.changelog/13998.txt new file mode 100644 index 000000000..0fba03f22 --- /dev/null +++ b/.changelog/13998.txt @@ -0,0 +1,3 @@ +```release-note:improvement +connect: expose new tracing configuration on envoy +``` diff --git a/.changelog/14034.txt b/.changelog/14034.txt new file mode 100644 index 000000000..216c5406a --- /dev/null +++ b/.changelog/14034.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: When launching a sidecar proxy with `consul connect envoy` or `consul connect proxy`, the `-sidecar-for` service ID argument is now treated as case-insensitive. +``` diff --git a/.changelog/14119.txt b/.changelog/14119.txt new file mode 100644 index 000000000..f0958361b --- /dev/null +++ b/.changelog/14119.txt @@ -0,0 +1,3 @@ +```release-note:bug +connect: Fixed some spurious issues during peering establishment when a follower is dialed +``` diff --git a/.changelog/14161.txt b/.changelog/14161.txt new file mode 100644 index 000000000..2926ffbe9 --- /dev/null +++ b/.changelog/14161.txt @@ -0,0 +1,3 @@ +```release-note:improvement +metrics: add labels of segment, partition, network area, network (lan or wan) to serf and memberlist metrics +``` diff --git a/.changelog/14162.txt b/.changelog/14162.txt new file mode 100644 index 000000000..d1ab0e08d --- /dev/null +++ b/.changelog/14162.txt @@ -0,0 +1,5 @@ +```release-note:improvement +config-entry: Validate that service-resolver `Failover`s and `Redirect`s only +specify `Partition` and `Namespace` on Consul Enterprise. This prevents scenarios +where OSS Consul would save service-resolvers that require Consul Enterprise. +``` diff --git a/.changelog/14178.txt b/.changelog/14178.txt new file mode 100644 index 000000000..99f87bbfc --- /dev/null +++ b/.changelog/14178.txt @@ -0,0 +1,4 @@ +```release-note:breaking-change +xds: Convert service mesh failover to use Envoy's aggregate clusters. This +changes the names of some [Envoy dynamic HTTP metrics](https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats#dynamic-http-statistics). +``` diff --git a/.changelog/14233.txt b/.changelog/14233.txt new file mode 100644 index 000000000..5a2c6dee1 --- /dev/null +++ b/.changelog/14233.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +rpc: Adds max jitter to client deadlines to prevent i/o deadline errors on blocking queries +``` diff --git a/.changelog/14238.txt b/.changelog/14238.txt new file mode 100644 index 000000000..43c570915 --- /dev/null +++ b/.changelog/14238.txt @@ -0,0 +1,3 @@ +```release-note:improvement +envoy: adds additional Envoy outlier ejection parameters to passive health check configurations. +``` \ No newline at end of file diff --git a/.changelog/14269.txt b/.changelog/14269.txt new file mode 100644 index 000000000..29eec6d5d --- /dev/null +++ b/.changelog/14269.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +connect: Fix issue where `auto_config` and `auto_encrypt` could unintentionally enable TLS for gRPC xDS connections. +``` \ No newline at end of file diff --git a/.changelog/14285.txt b/.changelog/14285.txt new file mode 100644 index 000000000..6e82e5458 --- /dev/null +++ b/.changelog/14285.txt @@ -0,0 +1,3 @@ +```release-note:feature +connect: Server address changes are streamed to peers +``` \ No newline at end of file diff --git a/.changelog/14290.txt b/.changelog/14290.txt new file mode 100644 index 000000000..719bd67b3 --- /dev/null +++ b/.changelog/14290.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +envoy: validate name before deleting proxy default configurations. +``` \ No newline at end of file diff --git a/.changelog/14294.txt b/.changelog/14294.txt new file mode 100644 index 000000000..7fcb497b1 --- /dev/null +++ b/.changelog/14294.txt @@ -0,0 +1,6 @@ +```release-note:breaking-change +config: Add new `ports.grpc_tls` configuration option. +Introduce a new port to better separate TLS config from the existing `ports.grpc` config. +The new `ports.grpc_tls` only supports TLS encrypted communication. +The existing `ports.grpc` currently supports both plain-text and tls communication, but tls support will be removed in a future release. +``` diff --git a/.changelog/14343.txt b/.changelog/14343.txt new file mode 100644 index 000000000..94e7432b4 --- /dev/null +++ b/.changelog/14343.txt @@ -0,0 +1,4 @@ +```release-note:feature +ui: Use withCredentials for all HTTP API requests +``` + diff --git a/.changelog/14364.txt b/.changelog/14364.txt new file mode 100644 index 000000000..d2f777af4 --- /dev/null +++ b/.changelog/14364.txt @@ -0,0 +1,3 @@ +```release-note:bugfix +peering: Fix issue preventing deletion and recreation of peerings in TERMINATED state. +``` \ No newline at end of file diff --git a/.changelog/14373.txt b/.changelog/14373.txt new file mode 100644 index 000000000..d9531b09e --- /dev/null +++ b/.changelog/14373.txt @@ -0,0 +1,3 @@ +```release-note:improvement +xds: Set `max_ejection_percent` on Envoy's outlier detection to 100% for peered services. +``` diff --git a/.changelog/14378.txt b/.changelog/14378.txt new file mode 100644 index 000000000..2ab1b8f13 --- /dev/null +++ b/.changelog/14378.txt @@ -0,0 +1,5 @@ +```release-note:bug +api: Fix a breaking change caused by renaming `QueryDatacenterOptions` to +`QueryFailoverOptions`. This adds `QueryDatacenterOptions` back as an alias to +`QueryFailoverOptions` and marks it as deprecated. +``` diff --git a/.changelog/14395.txt b/.changelog/14395.txt new file mode 100644 index 000000000..3caa76401 --- /dev/null +++ b/.changelog/14395.txt @@ -0,0 +1,4 @@ +```release-note:feature +service-defaults: Added support for `local_request_timeout_ms` and +`local_connect_timeout_ms` in servicedefaults config entry +``` diff --git a/.changelog/14396.txt b/.changelog/14396.txt new file mode 100644 index 000000000..3905df946 --- /dev/null +++ b/.changelog/14396.txt @@ -0,0 +1,3 @@ +```release-note:feature +peering: Add support to failover to services running on cluster peers. +``` diff --git a/.changelog/14397.txt b/.changelog/14397.txt new file mode 100644 index 000000000..96dbf81be --- /dev/null +++ b/.changelog/14397.txt @@ -0,0 +1,3 @@ +```release-note:feature +xds: servers will limit the number of concurrent xDS streams they can handle to balance the load across all servers +``` diff --git a/.changelog/14423.txt b/.changelog/14423.txt new file mode 100644 index 000000000..fd4033945 --- /dev/null +++ b/.changelog/14423.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: Adds new subcommands for `peering` workflows. Refer to the [CLI docs](https://www.consul.io/commands/peering) for more information. +``` diff --git a/.changelog/14429.txt b/.changelog/14429.txt new file mode 100644 index 000000000..4387d1ed4 --- /dev/null +++ b/.changelog/14429.txt @@ -0,0 +1,3 @@ +```release-note:bug +connect: Fixed an issue where intermediate certificates could build up in the root CA because they were never being pruned after expiring. +`` \ No newline at end of file diff --git a/.changelog/14433.txt b/.changelog/14433.txt new file mode 100644 index 000000000..25167320c --- /dev/null +++ b/.changelog/14433.txt @@ -0,0 +1,3 @@ +```release-note:bug +checks: If set, use proxy address for automatically added sidecar check instead of service address. +``` diff --git a/.changelog/14445.txt b/.changelog/14445.txt new file mode 100644 index 000000000..ec2d1e764 --- /dev/null +++ b/.changelog/14445.txt @@ -0,0 +1,3 @@ +```release-note:feature +peering: Add support to redirect to services running on cluster peers with service resolvers. +``` diff --git a/.changelog/14474.txt b/.changelog/14474.txt new file mode 100644 index 000000000..fcc326547 --- /dev/null +++ b/.changelog/14474.txt @@ -0,0 +1,3 @@ +```release-note:feature +http: Add new `get-or-empty` operation to the txn api. Refer to the [API docs](https://www.consul.io/api-docs/txn#kv-operations) for more information. +``` \ No newline at end of file diff --git a/.changelog/14475.txt b/.changelog/14475.txt new file mode 100644 index 000000000..26124ca6f --- /dev/null +++ b/.changelog/14475.txt @@ -0,0 +1,3 @@ +```release-note:bug +metrics: Add duplicate metrics that have only a single "consul_" prefix for all existing metrics with double ("consul_consul_") prefix, with the intent to standardize on single prefixes. +``` \ No newline at end of file diff --git a/.changelog/14495.txt b/.changelog/14495.txt new file mode 100644 index 000000000..e002bbbdc --- /dev/null +++ b/.changelog/14495.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Detect a TokenSecretID cookie and passthrough to localStorage +``` diff --git a/.changelog/14516.txt b/.changelog/14516.txt new file mode 100644 index 000000000..9980ee20f --- /dev/null +++ b/.changelog/14516.txt @@ -0,0 +1,3 @@ +```release-note:bug +ca: Fixed a bug with the Vault CA provider where the intermediate PKI mount and leaf cert role were not being updated when the CA configuration was changed. +``` \ No newline at end of file diff --git a/.changelog/14521.txt b/.changelog/14521.txt new file mode 100644 index 000000000..8dc0de40f --- /dev/null +++ b/.changelog/14521.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Reuse connections for requests to /v1/internal/ui/metrics-proxy/ +``` diff --git a/.changelog/14563.txt b/.changelog/14563.txt new file mode 100644 index 000000000..110a5230e --- /dev/null +++ b/.changelog/14563.txt @@ -0,0 +1,3 @@ +```release-note:improvement +peering: Validate peering tokens for server name conflicts +``` diff --git a/.changelog/14573.txt b/.changelog/14573.txt new file mode 100644 index 000000000..3dac5b565 --- /dev/null +++ b/.changelog/14573.txt @@ -0,0 +1,3 @@ +```release-note: improvement +connect: Bump latest Envoy to 1.23.1 in test matrix +``` diff --git a/.changelog/14577.txt b/.changelog/14577.txt new file mode 100644 index 000000000..022cdf011 --- /dev/null +++ b/.changelog/14577.txt @@ -0,0 +1,3 @@ +```release-note:security +auto-config: Added input validation for auto-config JWT authorization checks. Prior to this change, it was possible for malicious actors to construct requests which incorrectly pass custom JWT claim validation for the `AutoConfig.InitialConfiguration` endpoint. Now, only a subset of characters are allowed for the input before evaluating the bexpr. +``` \ No newline at end of file diff --git a/.changelog/14579.txt b/.changelog/14579.txt new file mode 100644 index 000000000..e03c0dc37 --- /dev/null +++ b/.changelog/14579.txt @@ -0,0 +1,3 @@ +```release-note:security +connect: Added URI length checks to ConnectCA CSR requests. Prior to this change, it was possible for a malicious actor to designate multiple SAN URI values in a call to the `ConnectCA.Sign` endpoint. The endpoint now only allows for exactly one SAN URI to be specified. +``` \ No newline at end of file diff --git a/.changelog/14598.txt b/.changelog/14598.txt new file mode 100644 index 000000000..f76b4958d --- /dev/null +++ b/.changelog/14598.txt @@ -0,0 +1,3 @@ +```release-note:bug +connect: Fixed a bug where old root CAs would be removed from the primary datacenter after switching providers and restarting the cluster. +``` diff --git a/.changelog/14606.txt b/.changelog/14606.txt new file mode 100644 index 000000000..f4ad4612a --- /dev/null +++ b/.changelog/14606.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Removed Overview page from HCP instalations +``` diff --git a/.changelog/14619.txt b/.changelog/14619.txt new file mode 100644 index 000000000..944493124 --- /dev/null +++ b/.changelog/14619.txt @@ -0,0 +1,3 @@ +```release-note:bug +checks: Do not set interval as timeout value +``` diff --git a/.changelog/_2271.txt b/.changelog/_2271.txt new file mode 100644 index 000000000..58dc78dfa --- /dev/null +++ b/.changelog/_2271.txt @@ -0,0 +1,3 @@ +```release-note:improvement +snapshot agent: **(Enterprise only)** Add support for path-based addressing when using s3 backend. +``` \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index af1a2f5c6..f246c3c29 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,7 +27,11 @@ references: - &default_envoy_version "1.20.6" - "1.21.4" - "1.22.2" - - "1.23.0" + - "1.23.1" + nomad-versions: &supported_nomad_versions + - &default_nomad_version "1.3.3" + - "1.2.10" + - "1.1.16" images: # When updating the Go version, remember to also update the versions in the # workflows section for go-test-lib jobs. @@ -105,15 +109,18 @@ commands: type: env_var_name default: ROLE_ARN steps: + # Only run the assume-role command for the main repo. The AWS credentials aren't available for forks. - run: | - export AWS_ACCESS_KEY_ID="${<< parameters.access-key >>}" - export AWS_SECRET_ACCESS_KEY="${<< parameters.secret-key >>}" - export ROLE_ARN="${<< parameters.role-arn >>}" - # assume role has duration of 15 min (the minimum allowed) - CREDENTIALS="$(aws sts assume-role --duration-seconds 900 --role-arn ${ROLE_ARN} --role-session-name build-${CIRCLE_SHA1} | jq '.Credentials')" - echo "export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV - echo "export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV - echo "export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV + if [[ "${CIRCLE_BRANCH%%/*}/" != "pull/" ]]; then + export AWS_ACCESS_KEY_ID="${<< parameters.access-key >>}" + export AWS_SECRET_ACCESS_KEY="${<< parameters.secret-key >>}" + export ROLE_ARN="${<< parameters.role-arn >>}" + # assume role has duration of 15 min (the minimum allowed) + CREDENTIALS="$(aws sts assume-role --duration-seconds 900 --role-arn ${ROLE_ARN} --role-session-name build-${CIRCLE_SHA1} | jq '.Credentials')" + echo "export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV + echo "export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV + echo "export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV + fi run-go-test-full: parameters: @@ -560,17 +567,20 @@ jobs: - run: make ci.dev-docker - run: *notify-slack-failure - # Nomad 0.8 builds on go1.10 - # Run integration tests on nomad/v0.8.7 - nomad-integration-0_8: + nomad-integration-test: &NOMAD_TESTS docker: - - image: docker.mirror.hashicorp.services/cimg/go:1.10 + - image: docker.mirror.hashicorp.services/cimg/go:1.19 + parameters: + nomad-version: + type: enum + enum: *supported_nomad_versions + default: *default_nomad_version environment: <<: *ENVIRONMENT NOMAD_WORKING_DIR: &NOMAD_WORKING_DIR /home/circleci/go/src/github.com/hashicorp/nomad - NOMAD_VERSION: v0.8.7 + NOMAD_VERSION: << parameters.nomad-version >> steps: &NOMAD_INTEGRATION_TEST_STEPS - - run: git clone https://github.com/hashicorp/nomad.git --branch ${NOMAD_VERSION} ${NOMAD_WORKING_DIR} + - run: git clone https://github.com/hashicorp/nomad.git --branch v${NOMAD_VERSION} ${NOMAD_WORKING_DIR} # get consul binary - attach_workspace: @@ -601,16 +611,6 @@ jobs: path: *TEST_RESULTS_DIR - run: *notify-slack-failure - # run integration tests on nomad/main - nomad-integration-main: - docker: - - image: docker.mirror.hashicorp.services/cimg/go:1.18 - environment: - <<: *ENVIRONMENT - NOMAD_WORKING_DIR: /home/circleci/go/src/github.com/hashicorp/nomad - NOMAD_VERSION: main - steps: *NOMAD_INTEGRATION_TEST_STEPS - # build frontend yarn cache frontend-cache: docker: @@ -816,7 +816,7 @@ jobs: # Get go binary from workspace - attach_workspace: at: . - # Build the consul-dev image from the already built binary + # Build the consul:local image from the already built binary - run: command: | sudo rm -rf /usr/local/go @@ -887,8 +887,8 @@ jobs: - attach_workspace: at: . - run: *install-gotestsum - # Build the consul-dev image from the already built binary - - run: docker build -t consul-dev -f ./build-support/docker/Consul-Dev.dockerfile . + # Build the consul:local image from the already built binary + - run: docker build -t consul:local -f ./build-support/docker/Consul-Dev.dockerfile . - run: name: Envoy Integration Tests command: | @@ -902,6 +902,7 @@ jobs: GOTESTSUM_JUNITFILE: /tmp/test-results/results.xml GOTESTSUM_FORMAT: standard-verbose COMPOSE_INTERACTIVE_NO_CLI: 1 + LAMBDA_TESTS_ENABLED: "true" # tput complains if this isn't set to something. TERM: ansi - store_artifacts: @@ -1117,12 +1118,12 @@ workflows: - dev-upload-docker: <<: *dev-upload context: consul-ci - - nomad-integration-main: - requires: - - dev-build - - nomad-integration-0_8: + - nomad-integration-test: requires: - dev-build + matrix: + parameters: + nomad-version: *supported_nomad_versions - envoy-integration-test: requires: - dev-build diff --git a/.github/workflows/backport-assistant.yml b/.github/workflows/backport-assistant.yml index f6738815b..b68e41e61 100644 --- a/.github/workflows/backport-assistant.yml +++ b/.github/workflows/backport-assistant.yml @@ -16,7 +16,7 @@ jobs: backport: if: github.event.pull_request.merged runs-on: ubuntu-latest - container: hashicorpdev/backport-assistant:0.2.3 + container: hashicorpdev/backport-assistant:0.2.5 steps: - name: Run Backport Assistant for stable-website run: | @@ -24,6 +24,7 @@ jobs: env: BACKPORT_LABEL_REGEXP: "type/docs-(?Pcherrypick)" BACKPORT_TARGET_TEMPLATE: "stable-website" + BACKPORT_MERGE_COMMIT: true GITHUB_TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }} - name: Backport changes to latest release branch run: | diff --git a/.golangci.yml b/.golangci.yml index 5dd923583..d71c93d16 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,6 +8,8 @@ linters: - ineffassign - unparam - forbidigo + - gomodguard + - depguard issues: # Disable the default exclude list so that all excludes are explicitly @@ -75,6 +77,30 @@ linters-settings: # Exclude godoc examples from forbidigo checks. # Default: true exclude_godoc_examples: false + gomodguard: + blocked: + # List of blocked modules. + modules: + # Blocked module. + - github.com/hashicorp/net-rpc-msgpackrpc: + recommendations: + - github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc + - github.com/hashicorp/go-msgpack: + recommendations: + - github.com/hashicorp/consul-net-rpc/go-msgpack + + depguard: + list-type: denylist + include-go-root: true + # A list of packages for the list type specified. + # Default: [] + packages: + - net/rpc + # A list of packages for the list type specified. + # Specify an error message to output when a denied package is used. + # Default: [] + packages-with-error-message: + - net/rpc: 'only use forked copy in github.com/hashicorp/consul-net-rpc/net/rpc' run: timeout: 10m diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b8a37a7..94217c74a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.13.1 (August 11, 2022) + +BUG FIXES: + +* agent: Fixed a compatibility issue when restoring snapshots from pre-1.13.0 versions of Consul [[GH-14107](https://github.com/hashicorp/consul/issues/14107)] [[GH-14149](https://github.com/hashicorp/consul/issues/14149)] +* connect: Fixed some spurious issues during peering establishment when a follower is dialed [[GH-14119](https://github.com/hashicorp/consul/issues/14119)] + ## 1.12.4 (August 11, 2022) BUG FIXES: @@ -21,6 +28,9 @@ connect: Terminating gateways with a wildcard service entry should no longer pic BREAKING CHANGES: * config-entry: Exporting a specific service name across all namespace is invalid. +* connect: contains an upgrade compatibility issue when restoring snapshots containing service mesh proxy registrations from pre-1.13 versions of Consul [[GH-14107](https://github.com/hashicorp/consul/issues/14107)]. Fixed in 1.13.1 [[GH-14149](https://github.com/hashicorp/consul/issues/14149)]. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#all-service-mesh-deployments) for more information. +* connect: if using auto-encrypt or auto-config, TLS is required for gRPC communication between Envoy and Consul as of 1.13.0; this TLS for gRPC requirement will be removed in a future 1.13 patch release. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#service-mesh-deployments-using-auto-encrypt-or-auto-config) for more information. +* connect: if a pre-1.13 Consul agent's HTTPS port was not enabled, upgrading to 1.13 may turn on TLS for gRPC communication for Envoy and Consul depending on the agent's TLS configuration. Refer to [1.13 upgrade guidance](https://www.consul.io/docs/upgrading/upgrade-specific#grpc-tls) for more information. * connect: Removes support for Envoy 1.19 [[GH-13807](https://github.com/hashicorp/consul/issues/13807)] * telemetry: config flag `telemetry { disable_compat_1.9 = (true|false) }` has been removed. Before upgrading you should remove this flag from your config if the flag is being used. [[GH-13532](https://github.com/hashicorp/consul/issues/13532)] @@ -937,6 +947,24 @@ NOTES: * legal: **(Enterprise only)** Enterprise binary downloads will now include a copy of the EULA and Terms of Evaluation in the zip archive +## 1.9.17 (April 13, 2022) + +SECURITY: + +* agent: Added a new check field, `disable_redirects`, that allows for disabling the following of redirects for HTTP checks. The intention is to default this to true in a future release so that redirects must explicitly be enabled. [[GH-12685](https://github.com/hashicorp/consul/issues/12685)] +* connect: Properly set SNI when configured for services behind a terminating gateway. [[GH-12672](https://github.com/hashicorp/consul/issues/12672)] + +DEPRECATIONS: + +* tls: With the upgrade to Go 1.17, the ordering of `tls_cipher_suites` will no longer be honored, and `tls_prefer_server_cipher_suites` is now ignored. [[GH-12767](https://github.com/hashicorp/consul/issues/12767)] + +BUG FIXES: + +* connect/ca: cancel old Vault renewal on CA configuration. Provide a 1 - 6 second backoff on repeated token renewal requests to prevent overwhelming Vault. [[GH-12607](https://github.com/hashicorp/consul/issues/12607)] +* memberlist: fixes a bug which prevented members from joining a cluster with +large amounts of churn [[GH-253](https://github.com/hashicorp/memberlist/issues/253)] [[GH-12046](https://github.com/hashicorp/consul/issues/12046)] +* replication: Fixed a bug which could prevent ACL replication from continuing successfully after a leader election. [[GH-12565](https://github.com/hashicorp/consul/issues/12565)] + ## 1.9.16 (February 28, 2022) FEATURES: diff --git a/Dockerfile b/Dockerfile index 762471eb5..8e127254f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,11 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${VERSION} # This is the location of the releases. ENV HASHICORP_RELEASES=https://releases.hashicorp.com @@ -110,13 +111,13 @@ CMD ["agent", "-dev", "-client", "0.0.0.0"] # Remember, this image cannot be built locally. FROM docker.mirror.hashicorp.services/alpine:3.15 as default -ARG VERSION +ARG PRODUCT_VERSION ARG BIN_NAME # PRODUCT_NAME and PRODUCT_VERSION are the name of the software on releases.hashicorp.com # and the version to download. Example: PRODUCT_NAME=consul PRODUCT_VERSION=1.2.3. ENV BIN_NAME=$BIN_NAME -ENV VERSION=$VERSION +ENV PRODUCT_VERSION=$PRODUCT_VERSION ARG PRODUCT_REVISION ARG PRODUCT_NAME=$BIN_NAME @@ -128,10 +129,11 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${PRODUCT_VERSION} # Set up certificates and base tools. # libc6-compat is needed to symlink the shared libraries for ARM builds @@ -217,10 +219,11 @@ LABEL org.opencontainers.image.authors="Consul Team " \ org.opencontainers.image.url="https://www.consul.io/" \ org.opencontainers.image.documentation="https://www.consul.io/docs" \ org.opencontainers.image.source="https://github.com/hashicorp/consul" \ - org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.version=${PRODUCT_VERSION} \ org.opencontainers.image.vendor="HashiCorp" \ org.opencontainers.image.title="consul" \ - org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." + org.opencontainers.image.description="Consul is a datacenter runtime that provides service discovery, configuration, and orchestration." \ + version=${PRODUCT_VERSION} # Copy license for Red Hat certification. COPY LICENSE /licenses/mozilla.txt @@ -284,4 +287,4 @@ USER 100 # By default you'll get an insecure single-node development server that stores # everything in RAM, exposes a web UI and HTTP endpoints, and bootstraps itself. # Don't use this configuration for production. -CMD ["agent", "-dev", "-client", "0.0.0.0"] \ No newline at end of file +CMD ["agent", "-dev", "-client", "0.0.0.0"] diff --git a/GNUmakefile b/GNUmakefile index 6327ea579..f9dd16081 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -16,6 +16,7 @@ PROTOC_GO_INJECT_TAG_VERSION='v1.3.0' GOTAGS ?= GOPATH=$(shell go env GOPATH) +GOARCH?=$(shell go env GOARCH) MAIN_GOPATH=$(shell go env GOPATH | cut -d: -f1) export PATH := $(PWD)/bin:$(GOPATH)/bin:$(PATH) @@ -129,7 +130,7 @@ export GOLDFLAGS # Allow skipping docker build during integration tests in CI since we already # have a built binary -ENVOY_INTEG_DEPS?=dev-docker +ENVOY_INTEG_DEPS?=docker-envoy-integ ifdef SKIP_DOCKER_BUILD ENVOY_INTEG_DEPS=noop endif @@ -152,7 +153,28 @@ dev-docker: linux @docker pull consul:$(CONSUL_IMAGE_VERSION) >/dev/null @echo "Building Consul Development container - $(CONSUL_DEV_IMAGE)" # 'consul:local' tag is needed to run the integration tests - @DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build $(NOCACHE) $(QUIET) -t '$(CONSUL_DEV_IMAGE)' -t 'consul:local' --build-arg CONSUL_IMAGE_VERSION=$(CONSUL_IMAGE_VERSION) $(CURDIR)/pkg/bin/linux_amd64 -f $(CURDIR)/build-support/docker/Consul-Dev.dockerfile + @docker buildx use default && docker buildx build -t 'consul:local' \ + --platform linux/$(GOARCH) \ + --build-arg CONSUL_IMAGE_VERSION=$(CONSUL_IMAGE_VERSION) \ + --load \ + -f $(CURDIR)/build-support/docker/Consul-Dev-Multiarch.dockerfile $(CURDIR)/pkg/bin/ + +check-remote-dev-image-env: +ifndef REMOTE_DEV_IMAGE + $(error REMOTE_DEV_IMAGE is undefined: set this image to /:, e.g. hashicorp/consul-k8s-dev:latest) +endif + +remote-docker: check-remote-dev-image-env + $(MAKE) GOARCH=amd64 linux + $(MAKE) GOARCH=arm64 linux + @echo "Pulling consul container image - $(CONSUL_IMAGE_VERSION)" + @docker pull consul:$(CONSUL_IMAGE_VERSION) >/dev/null + @echo "Building and Pushing Consul Development container - $(REMOTE_DEV_IMAGE)" + @docker buildx use default && docker buildx build -t '$(REMOTE_DEV_IMAGE)' \ + --platform linux/amd64,linux/arm64 \ + --build-arg CONSUL_IMAGE_VERSION=$(CONSUL_IMAGE_VERSION) \ + --push \ + -f $(CURDIR)/build-support/docker/Consul-Dev-Multiarch.dockerfile $(CURDIR)/pkg/bin/ # In CircleCI, the linux binary will be attached from a previous step at bin/. This make target # should only run in CI and not locally. @@ -174,10 +196,10 @@ ifeq ($(CIRCLE_BRANCH), main) @docker push $(CI_DEV_DOCKER_NAMESPACE)/$(CI_DEV_DOCKER_IMAGE_NAME):latest endif -# linux builds a linux binary independent of the source platform +# linux builds a linux binary compatible with the source platform linux: - @mkdir -p ./pkg/bin/linux_amd64 - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./pkg/bin/linux_amd64 -ldflags "$(GOLDFLAGS)" -tags "$(GOTAGS)" + @mkdir -p ./pkg/bin/linux_$(GOARCH) + CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -o ./pkg/bin/linux_$(GOARCH) -ldflags "$(GOLDFLAGS)" -tags "$(GOTAGS)" # dist builds binaries for all platforms and packages them for distribution dist: @@ -324,8 +346,22 @@ consul-docker: go-build-image ui-docker: ui-build-image @$(SHELL) $(CURDIR)/build-support/scripts/build-docker.sh ui +# Build image used to run integration tests locally. +docker-envoy-integ: + $(MAKE) GOARCH=amd64 linux + docker build \ + --platform linux/amd64 $(NOCACHE) $(QUIET) \ + -t 'consul:local' \ + --build-arg CONSUL_IMAGE_VERSION=$(CONSUL_IMAGE_VERSION) \ + $(CURDIR)/pkg/bin/linux_amd64 \ + -f $(CURDIR)/build-support/docker/Consul-Dev.dockerfile + +# Run integration tests. +# Use GO_TEST_FLAGS to run specific tests: +# make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/case-basic" +# NOTE: Always uses amd64 images, even when running on M1 macs, to match CI/CD environment. test-envoy-integ: $(ENVOY_INTEG_DEPS) - @go test -v -timeout=30m -tags integration ./test/integration/connect/envoy + @go test -v -timeout=30m -tags integration $(GO_TEST_FLAGS) ./test/integration/connect/envoy .PHONY: test-compat-integ test-compat-integ: dev-docker diff --git a/agent/agent.go b/agent/agent.go index 8a263647e..92f4a06e9 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -213,7 +213,7 @@ type Agent struct { // depending on the configuration delegate delegate - // externalGRPCServer is the gRPC server exposed on the dedicated gRPC port (as + // externalGRPCServer is the gRPC server exposed on dedicated gRPC ports (as // opposed to the multiplexed "server" port). externalGRPCServer *grpc.Server @@ -384,18 +384,18 @@ type Agent struct { // New process the desired options and creates a new Agent. // This process will -// * parse the config given the config Flags -// * setup logging -// * using predefined logger given in an option -// OR -// * initialize a new logger from the configuration -// including setting up gRPC logging -// * initialize telemetry -// * create a TLS Configurator -// * build a shared connection pool -// * create the ServiceManager -// * setup the NodeID if one isn't provided in the configuration -// * create the AutoConfig object for future use in fully +// - parse the config given the config Flags +// - setup logging +// - using predefined logger given in an option +// OR +// - initialize a new logger from the configuration +// including setting up gRPC logging +// - initialize telemetry +// - create a TLS Configurator +// - build a shared connection pool +// - create the ServiceManager +// - setup the NodeID if one isn't provided in the configuration +// - create the AutoConfig object for future use in fully // resolving the configuration func New(bd BaseDeps) (*Agent, error) { a := Agent{ @@ -539,7 +539,7 @@ func (a *Agent) Start(ctx context.Context) error { // This needs to happen after the initial auto-config is loaded, because TLS // can only be configured on the gRPC server at the point of creation. - a.buildExternalGRPCServer() + a.externalGRPCServer = external.NewServer(a.logger.Named("grpc.external")) if err := a.startLicenseManager(ctx); err != nil { return err @@ -702,11 +702,14 @@ func (a *Agent) Start(ctx context.Context) error { a.apiServers.Start(srv) } - // Start gRPC server. + // Start grpc and grpc_tls servers. if err := a.listenAndServeGRPC(); err != nil { return err } + // Start a goroutine to terminate excess xDS sessions. + go a.baseDeps.XDSStreamLimiter.Run(&lib.StopChannelContext{StopCh: a.shutdownCh}) + // register watches if err := a.reloadWatches(a.config); err != nil { return err @@ -760,15 +763,10 @@ func (a *Agent) Failed() <-chan struct{} { return a.apiServers.failed } -func (a *Agent) buildExternalGRPCServer() { - a.externalGRPCServer = external.NewServer(a.logger.Named("grpc.external"), a.tlsConfigurator) -} - func (a *Agent) listenAndServeGRPC() error { - if len(a.config.GRPCAddrs) < 1 { + if len(a.config.GRPCAddrs) < 1 && len(a.config.GRPCTLSAddrs) < 1 { return nil } - // TODO(agentless): rather than asserting the concrete type of delegate, we // should add a method to the Delegate interface to build a ConfigSource. var cfg xds.ProxyConfigSource = localproxycfg.NewConfigSource(a.proxyConfig) @@ -787,7 +785,6 @@ func (a *Agent) listenAndServeGRPC() error { }() cfg = catalogCfg } - a.xdsServer = xds.NewServer( a.config.NodeName, a.logger.Named(logging.Envoy), @@ -797,25 +794,65 @@ func (a *Agent) listenAndServeGRPC() error { return a.delegate.ResolveTokenAndDefaultMeta(id, nil, nil) }, a, + a.baseDeps.XDSStreamLimiter, ) a.xdsServer.Register(a.externalGRPCServer) - ln, err := a.startListeners(a.config.GRPCAddrs) - if err != nil { - return err + // Attempt to spawn listeners + var listeners []net.Listener + start := func(port_name string, addrs []net.Addr, tlsConf *tls.Config) error { + if len(addrs) < 1 { + return nil + } + + ln, err := a.startListeners(addrs) + if err != nil { + return err + } + for i := range ln { + // Wrap with TLS, if provided. + if tlsConf != nil { + ln[i] = tls.NewListener(ln[i], tlsConf) + } + listeners = append(listeners, ln[i]) + } + + for _, l := range ln { + go func(innerL net.Listener) { + a.logger.Info("Started gRPC listeners", + "port_name", port_name, + "address", innerL.Addr().String(), + "network", innerL.Addr().Network(), + ) + err := a.externalGRPCServer.Serve(innerL) + if err != nil { + a.logger.Error("gRPC server failed", "port_name", port_name, "error", err) + } + }(l) + } + return nil } - for _, l := range ln { - go func(innerL net.Listener) { - a.logger.Info("Started gRPC server", - "address", innerL.Addr().String(), - "network", innerL.Addr().Network(), - ) - err := a.externalGRPCServer.Serve(innerL) - if err != nil { - a.logger.Error("gRPC server failed", "error", err) - } - }(l) + // The original grpc port may spawn in either plain-text or TLS mode (for backwards compatibility). + // TODO: Simplify this block to only spawn plain-text after 1.14 when deprecated TLS support is removed. + if a.config.GRPCPort > 0 { + // Only allow the grpc port to spawn TLS connections if the other grpc_tls port is NOT defined. + var tlsConf *tls.Config = nil + if a.config.GRPCTLSPort <= 0 && a.tlsConfigurator.GRPCServerUseTLS() { + a.logger.Warn("deprecated gRPC TLS configuration detected. Consider using `ports.grpc_tls` instead") + tlsConf = a.tlsConfigurator.IncomingGRPCConfig() + } + if err := start("grpc", a.config.GRPCAddrs, tlsConf); err != nil { + closeListeners(listeners) + return err + } + } + // Only allow grpc_tls to spawn with a TLS listener. + if a.config.GRPCTLSPort > 0 { + if err := start("grpc_tls", a.config.GRPCTLSAddrs, a.tlsConfigurator.IncomingGRPCConfig()); err != nil { + closeListeners(listeners) + return err + } } return nil } @@ -939,8 +976,9 @@ func (a *Agent) listenHTTP() ([]apiServer, error) { } srv := &HTTPHandlers{ - agent: a, - denylist: NewDenylist(a.config.HTTPBlockEndpoints), + agent: a, + denylist: NewDenylist(a.config.HTTPBlockEndpoints), + proxyTransport: http.DefaultTransport, } a.configReloaders = append(a.configReloaders, srv.ReloadConfig) a.httpHandlers = srv @@ -1202,6 +1240,7 @@ func newConsulConfig(runtimeCfg *config.RuntimeConfig, logger hclog.Logger) (*co cfg.RPCAdvertise = runtimeCfg.RPCAdvertiseAddr cfg.GRPCPort = runtimeCfg.GRPCPort + cfg.GRPCTLSPort = runtimeCfg.GRPCTLSPort cfg.Segment = runtimeCfg.SegmentName if len(runtimeCfg.Segments) > 0 { @@ -1505,7 +1544,9 @@ func (a *Agent) ShutdownAgent() error { } // Stop gRPC - a.externalGRPCServer.Stop() + if a.externalGRPCServer != nil { + a.externalGRPCServer.Stop() + } // Stop the proxy config manager if a.proxyConfig != nil { @@ -2104,6 +2145,21 @@ func (a *Agent) AddService(req AddServiceRequest) error { // addServiceLocked adds a service entry to the service manager if enabled, or directly // to the local state if it is not. This function assumes the state lock is already held. func (a *Agent) addServiceLocked(req addServiceLockedRequest) error { + // Must auto-assign the port and default checks (if needed) here to avoid race collisions. + if req.Service.LocallyRegisteredAsSidecar { + if req.Service.Port < 1 { + port, err := a.sidecarPortFromServiceIDLocked(req.Service.CompoundServiceID()) + if err != nil { + return err + } + req.Service.Port = port + } + // Setup default check if none given. + if len(req.chkTypes) < 1 { + req.chkTypes = sidecarDefaultChecks(req.Service.ID, req.Service.Address, req.Service.Proxy.LocalServiceAddress, req.Service.Port) + } + } + req.Service.EnterpriseMeta.Normalize() if err := a.validateService(req.Service, req.chkTypes); err != nil { @@ -2231,7 +2287,7 @@ func (a *Agent) addServiceInternal(req addServiceInternalRequest) error { intervalStr = chkType.Interval.String() } if chkType.Timeout != 0 { - timeoutStr = chkType.Interval.String() + timeoutStr = chkType.Timeout.String() } check := &structs.HealthCheck{ @@ -2556,7 +2612,7 @@ func (a *Agent) removeServiceLocked(serviceID structs.ServiceID, persist bool) e } func (a *Agent) removeServiceSidecars(serviceID structs.ServiceID, persist bool) error { - sidecarSID := structs.NewServiceID(sidecarServiceID(serviceID.ID), &serviceID.EnterpriseMeta) + sidecarSID := structs.NewServiceID(sidecarIDFromServiceID(serviceID.ID), &serviceID.EnterpriseMeta) if sidecar := a.State.Service(sidecarSID); sidecar != nil { // Double check that it's not just an ID collision and we actually added // this from a sidecar. @@ -3368,7 +3424,7 @@ func (a *Agent) loadServices(conf *config.RuntimeConfig, snap map[structs.CheckI } // Grab and validate sidecar if there is one too - sidecar, sidecarChecks, sidecarToken, err := a.sidecarServiceFromNodeService(ns, service.Token) + sidecar, sidecarChecks, sidecarToken, err := sidecarServiceFromNodeService(ns, service.Token) if err != nil { return fmt.Errorf("Failed to validate sidecar for service %q: %v", service.Name, err) } @@ -4268,7 +4324,10 @@ func (a *Agent) proxyDataSources() proxycfg.DataSources { sources.Health = proxycfgglue.ServerHealth(deps, proxycfgglue.ClientHealth(a.rpcClientHealth)) sources.Intentions = proxycfgglue.ServerIntentions(deps) sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps) + sources.IntentionUpstreamsDestination = proxycfgglue.ServerIntentionUpstreamsDestination(deps) + sources.InternalServiceDump = proxycfgglue.ServerInternalServiceDump(deps, proxycfgglue.CacheInternalServiceDump(a.cache)) sources.PeeredUpstreams = proxycfgglue.ServerPeeredUpstreams(deps) + sources.ResolvedServiceConfig = proxycfgglue.ServerResolvedServiceConfig(deps, proxycfgglue.CacheResolvedServiceConfig(a.cache)) sources.ServiceList = proxycfgglue.ServerServiceList(deps, proxycfgglue.CacheServiceList(a.cache)) sources.TrustBundle = proxycfgglue.ServerTrustBundle(deps) sources.TrustBundleList = proxycfgglue.ServerTrustBundleList(deps) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index b2d68e304..08324c2c4 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -45,7 +45,19 @@ type Self struct { type XDSSelf struct { SupportedProxies map[string][]string - Port int + // Port could be used for either TLS or plain-text communication + // up through version 1.14. In order to maintain backwards-compatibility, + // Port will now default to TLS and fallback to the standard port value. + // DEPRECATED: Use Ports field instead + Port int + Ports GRPCPorts +} + +// GRPCPorts is used to hold the external GRPC server's port numbers. +type GRPCPorts struct { + // Technically, this port is not always plain-text as of 1.14, but will be in a future release. + Plaintext int + TLS int } func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -78,7 +90,16 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i SupportedProxies: map[string][]string{ "envoy": proxysupport.EnvoyVersions, }, - Port: s.agent.config.GRPCPort, + // Prefer the TLS port. See comment on the XDSSelf struct for details. + Port: s.agent.config.GRPCTLSPort, + Ports: GRPCPorts{ + Plaintext: s.agent.config.GRPCPort, + TLS: s.agent.config.GRPCTLSPort, + }, + } + // Fallback to standard port if TLS is not enabled. + if s.agent.config.GRPCTLSPort <= 0 { + xds.Port = s.agent.config.GRPCPort } } @@ -1159,7 +1180,7 @@ func (s *HTTPHandlers) AgentRegisterService(resp http.ResponseWriter, req *http. } // See if we have a sidecar to register too - sidecar, sidecarChecks, sidecarToken, err := s.agent.sidecarServiceFromNodeService(ns, token) + sidecar, sidecarChecks, sidecarToken, err := sidecarServiceFromNodeService(ns, token) if err != nil { return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: fmt.Sprintf("Invalid SidecarService: %s", err)} } diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 67850f9eb..4f8f18882 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1434,15 +1434,8 @@ func TestAgent_Self(t *testing.T) { cases := map[string]struct { hcl string expectXDS bool + grpcTLS bool }{ - "normal": { - hcl: ` - node_meta { - somekey = "somevalue" - } - `, - expectXDS: true, - }, "no grpc": { hcl: ` node_meta { @@ -1453,13 +1446,35 @@ func TestAgent_Self(t *testing.T) { } `, expectXDS: false, + grpcTLS: false, + }, + "plaintext grpc": { + hcl: ` + node_meta { + somekey = "somevalue" + } + `, + expectXDS: true, + grpcTLS: false, + }, + "tls grpc": { + hcl: ` + node_meta { + somekey = "somevalue" + } + `, + expectXDS: true, + grpcTLS: true, }, } for name, tc := range cases { tc := tc t.Run(name, func(t *testing.T) { - a := NewTestAgent(t, tc.hcl) + a := StartTestAgent(t, TestAgent{ + HCL: tc.hcl, + UseGRPCTLS: tc.grpcTLS, + }) defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") @@ -1487,6 +1502,13 @@ func TestAgent_Self(t *testing.T) { map[string][]string{"envoy": proxysupport.EnvoyVersions}, val.XDS.SupportedProxies, ) + require.Equal(t, a.Config.GRPCTLSPort, val.XDS.Ports.TLS) + require.Equal(t, a.Config.GRPCPort, val.XDS.Ports.Plaintext) + if tc.grpcTLS { + require.Equal(t, a.Config.GRPCTLSPort, val.XDS.Port) + } else { + require.Equal(t, a.Config.GRPCPort, val.XDS.Port) + } } else { require.Nil(t, val.XDS, "xds component should be missing when gRPC is disabled") @@ -3764,7 +3786,7 @@ func testAgent_RegisterService_TranslateKeys(t *testing.T, extraHCL string) { fmt.Println("TCP Check:= ", v) } if hasNoCorrectTCPCheck { - t.Fatalf("Did not find the expected TCP Healtcheck '%s' in %#v ", tt.expectedTCPCheckStart, a.checkTCPs) + t.Fatalf("Did not find the expected TCP Healthcheck '%s' in %#v ", tt.expectedTCPCheckStart, a.checkTCPs) } require.Equal(t, sidecarSvc, gotSidecar) }) diff --git a/agent/agent_test.go b/agent/agent_test.go index 8bae81ce4..ea1535821 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -418,6 +418,9 @@ func testAgent_AddService(t *testing.T, extraHCL string) { `+extraHCL) defer a.Shutdown() + duration3s, _ := time.ParseDuration("3s") + duration10s, _ := time.ParseDuration("10s") + tests := []struct { desc string srv *structs.NodeService @@ -467,6 +470,50 @@ func testAgent_AddService(t *testing.T, extraHCL string) { }, }, }, + { + "one http check with interval and duration", + &structs.NodeService{ + ID: "svcid1", + Service: "svcname1", + Tags: []string{"tag1"}, + Weights: nil, // nil weights... + Port: 8100, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + // ... should be populated to avoid "IsSame" returning true during AE. + func(ns *structs.NodeService) { + ns.Weights = &structs.Weights{ + Passing: 1, + Warning: 1, + } + }, + []*structs.CheckType{ + { + CheckID: "check1", + Name: "name1", + HTTP: "http://localhost:8100/", + Interval: duration10s, + Timeout: duration3s, + Notes: "note1", + }, + }, + map[string]*structs.HealthCheck{ + "check1": { + Node: "node1", + CheckID: "check1", + Name: "name1", + Interval: "10s", + Timeout: "3s", + Status: "critical", + Notes: "note1", + ServiceID: "svcid1", + ServiceName: "svcname1", + ServiceTags: []string{"tag1"}, + Type: "http", + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + }, + }, { "multiple checks", &structs.NodeService{ @@ -2095,7 +2142,7 @@ func TestAgent_HTTPCheck_EnableAgentTLSForChecks(t *testing.T) { run := func(t *testing.T, ca string) { a := StartTestAgent(t, TestAgent{ - UseTLS: true, + UseHTTPS: true, HCL: ` enable_agent_tls_for_checks = true @@ -2786,7 +2833,7 @@ func TestAgent_DeregisterPersistedSidecarAfterRestart(t *testing.T) { }, } - connectSrv, _, _, err := a.sidecarServiceFromNodeService(srv, "") + connectSrv, _, _, err := sidecarServiceFromNodeService(srv, "") require.NoError(t, err) // First persist the check @@ -2959,11 +3006,24 @@ func testAgent_loadServices_sidecar(t *testing.T, extraHCL string) { if token := a.State.ServiceToken(structs.NewServiceID("rabbitmq", nil)); token != "abc123" { t.Fatalf("bad: %s", token) } - requireServiceExists(t, a, "rabbitmq-sidecar-proxy") + sidecarSvc := requireServiceExists(t, a, "rabbitmq-sidecar-proxy") if token := a.State.ServiceToken(structs.NewServiceID("rabbitmq-sidecar-proxy", nil)); token != "abc123" { t.Fatalf("bad: %s", token) } + // Verify default checks have been added + wantChecks := sidecarDefaultChecks(sidecarSvc.ID, sidecarSvc.Address, sidecarSvc.Proxy.LocalServiceAddress, sidecarSvc.Port) + gotChecks := a.State.ChecksForService(sidecarSvc.CompoundServiceID(), true) + gotChkNames := make(map[string]types.CheckID) + for _, check := range gotChecks { + requireCheckExists(t, a, check.CheckID) + gotChkNames[check.Name] = check.CheckID + } + for _, check := range wantChecks { + chkName := check.Name + require.NotNil(t, gotChkNames[chkName]) + } + // Sanity check rabbitmq service should NOT have sidecar info in state since // it's done it's job and should be a registration syntax sugar only. assert.Nil(t, svc.Connect.SidecarService) @@ -3860,7 +3920,7 @@ func TestAgent_reloadWatchesHTTPS(t *testing.T) { } t.Parallel() - a := TestAgent{UseTLS: true} + a := TestAgent{UseHTTPS: true} if err := a.Start(t); err != nil { t.Fatal(err) } @@ -5207,7 +5267,7 @@ func TestAgent_AutoEncrypt(t *testing.T) { server = ` + strconv.Itoa(srv.Config.RPCBindAddr.Port) + ` } retry_join = ["` + srv.Config.SerfBindAddrLAN.String() + `"]`, - UseTLS: true, + UseHTTPS: true, }) defer client.Shutdown() diff --git a/agent/cache/cache.go b/agent/cache/cache.go index 29690012b..0252c2dcf 100644 --- a/agent/cache/cache.go +++ b/agent/cache/cache.go @@ -37,6 +37,10 @@ import ( var Gauges = []prometheus.GaugeDefinition{ { Name: []string{"consul", "cache", "entries_count"}, + Help: "Deprecated - please use cache_entries_count instead.", + }, + { + Name: []string{"cache", "entries_count"}, Help: "Represents the number of entries in this cache.", }, } @@ -45,18 +49,34 @@ var Gauges = []prometheus.GaugeDefinition{ var Counters = []prometheus.CounterDefinition{ { Name: []string{"consul", "cache", "bypass"}, + Help: "Deprecated - please use cache_bypass instead.", + }, + { + Name: []string{"cache", "bypass"}, Help: "Counts how many times a request bypassed the cache because no cache-key was provided.", }, { Name: []string{"consul", "cache", "fetch_success"}, + Help: "Deprecated - please use cache_fetch_success instead.", + }, + { + Name: []string{"cache", "fetch_success"}, Help: "Counts the number of successful fetches by the cache.", }, { Name: []string{"consul", "cache", "fetch_error"}, + Help: "Deprecated - please use cache_fetch_error instead.", + }, + { + Name: []string{"cache", "fetch_error"}, Help: "Counts the number of failed fetches by the cache.", }, { Name: []string{"consul", "cache", "evict_expired"}, + Help: "Deprecated - please use cache_evict_expired instead.", + }, + { + Name: []string{"cache", "evict_expired"}, Help: "Counts the number of expired entries that are evicted.", }, } @@ -397,6 +417,7 @@ func entryExceedsMaxAge(maxAge time.Duration, entry cacheEntry) bool { func (c *Cache) getWithIndex(ctx context.Context, r getOptions) (interface{}, ResultMeta, error) { if r.Info.Key == "" { metrics.IncrCounter([]string{"consul", "cache", "bypass"}, 1) + metrics.IncrCounter([]string{"cache", "bypass"}, 1) // If no key is specified, then we do not cache this request. // Pass directly through to the backend. @@ -443,6 +464,7 @@ RETRY_GET: meta := ResultMeta{Index: entry.Index} if first { metrics.IncrCounter([]string{"consul", "cache", r.TypeEntry.Name, "hit"}, 1) + metrics.IncrCounter([]string{"cache", r.TypeEntry.Name, "hit"}, 1) meta.Hit = true } @@ -496,6 +518,7 @@ RETRY_GET: missKey = "miss_new" } metrics.IncrCounter([]string{"consul", "cache", r.TypeEntry.Name, missKey}, 1) + metrics.IncrCounter([]string{"cache", r.TypeEntry.Name, missKey}, 1) } // Set our timeout channel if we must @@ -588,6 +611,7 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign entry.Fetching = true c.entries[key] = entry metrics.SetGauge([]string{"consul", "cache", "entries_count"}, float32(len(c.entries))) + metrics.SetGauge([]string{"cache", "entries_count"}, float32(len(c.entries))) tEntry := r.TypeEntry // The actual Fetch must be performed in a goroutine. @@ -694,7 +718,9 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign 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{"cache", "fetch_success"}, 1, labels) metrics.IncrCounterWithLabels([]string{"consul", "cache", tEntry.Name, "fetch_success"}, 1, labels) + metrics.IncrCounterWithLabels([]string{"cache", tEntry.Name, "fetch_success"}, 1, labels) if result.Index > 0 { // Reset the attempts counter so we don't have any backoff @@ -728,7 +754,9 @@ func (c *Cache) fetch(key string, r getOptions, allowNew bool, attempt uint, ign // TODO(kit): Add tEntry.Name to label on fetch_error and deprecate second write metrics.IncrCounterWithLabels([]string{"consul", "cache", "fetch_error"}, 1, labels) + metrics.IncrCounterWithLabels([]string{"cache", "fetch_error"}, 1, labels) metrics.IncrCounterWithLabels([]string{"consul", "cache", tEntry.Name, "fetch_error"}, 1, labels) + metrics.IncrCounterWithLabels([]string{"cache", tEntry.Name, "fetch_error"}, 1, labels) // Increment attempt counter attempt++ @@ -858,7 +886,9 @@ func (c *Cache) runExpiryLoop() { // Set some metrics metrics.IncrCounter([]string{"consul", "cache", "evict_expired"}, 1) + metrics.IncrCounter([]string{"cache", "evict_expired"}, 1) metrics.SetGauge([]string{"consul", "cache", "entries_count"}, float32(len(c.entries))) + metrics.SetGauge([]string{"cache", "entries_count"}, float32(len(c.entries))) c.entriesLock.Unlock() } diff --git a/agent/config/builder.go b/agent/config/builder.go index 40389553d..721fd05ea 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -125,10 +125,10 @@ type LoadResult struct { // // The sources are merged in the following order: // -// * default configuration -// * config files in alphabetical order -// * command line arguments -// * overrides +// - default configuration +// - config files in alphabetical order +// - command line arguments +// - overrides // // The config sources are merged sequentially and later values overwrite // previously set values. Slice values are merged by concatenating the two slices. @@ -433,6 +433,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) { httpsPort := b.portVal("ports.https", c.Ports.HTTPS) serverPort := b.portVal("ports.server", c.Ports.Server) grpcPort := b.portVal("ports.grpc", c.Ports.GRPC) + grpcTlsPort := b.portVal("ports.grpc_tls", c.Ports.GRPCTLS) serfPortLAN := b.portVal("ports.serf_lan", c.Ports.SerfLAN) serfPortWAN := b.portVal("ports.serf_wan", c.Ports.SerfWAN) proxyMinPort := b.portVal("ports.proxy_min_port", c.Ports.ProxyMinPort) @@ -563,6 +564,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) { httpAddrs := b.makeAddrs(b.expandAddrs("addresses.http", c.Addresses.HTTP), clientAddrs, httpPort) httpsAddrs := b.makeAddrs(b.expandAddrs("addresses.https", c.Addresses.HTTPS), clientAddrs, httpsPort) grpcAddrs := b.makeAddrs(b.expandAddrs("addresses.grpc", c.Addresses.GRPC), clientAddrs, grpcPort) + grpcTlsAddrs := b.makeAddrs(b.expandAddrs("addresses.grpc_tls", c.Addresses.GRPCTLS), clientAddrs, grpcTlsPort) for _, a := range dnsAddrs { if x, ok := a.(*net.TCPAddr); ok { @@ -987,8 +989,10 @@ func (b *builder) build() (rt RuntimeConfig, err error) { EnableRemoteScriptChecks: enableRemoteScriptChecks, EnableLocalScriptChecks: enableLocalScriptChecks, EncryptKey: stringVal(c.EncryptKey), - GRPCPort: grpcPort, GRPCAddrs: grpcAddrs, + GRPCPort: grpcPort, + GRPCTLSAddrs: grpcTlsAddrs, + GRPCTLSPort: grpcTlsPort, HTTPMaxConnsPerClient: intVal(c.Limits.HTTPMaxConnsPerClient), HTTPSHandshakeTimeout: b.durationVal("limits.https_handshake_timeout", c.Limits.HTTPSHandshakeTimeout), KVMaxValueSize: uint64Val(c.Limits.KVMaxValueSize), @@ -2531,10 +2535,9 @@ func (b *builder) buildTLSConfig(rt RuntimeConfig, t TLS) (tlsutil.Config, error return c, errors.New("verify_server_hostname is only valid in the tls.internal_rpc stanza") } - // TLS is only enabled on the gRPC listener if there's an HTTPS port configured - // for historic and backwards-compatibility reasons. - if rt.HTTPSPort <= 0 && (t.GRPC != TLSProtocolConfig{} && t.GRPCModifiedByDeprecatedConfig == nil) { - b.warn("tls.grpc was provided but TLS will NOT be enabled on the gRPC listener without an HTTPS listener configured (e.g. via ports.https)") + // And UseAutoCert right now only applies to external gRPC interface. + if t.Defaults.UseAutoCert != nil || t.HTTPS.UseAutoCert != nil || t.InternalRPC.UseAutoCert != nil { + return c, errors.New("use_auto_cert is only valid in the tls.grpc stanza") } defaultTLSMinVersion := b.tlsVersion("tls.defaults.tls_min_version", t.Defaults.TLSMinVersion) @@ -2591,6 +2594,7 @@ func (b *builder) buildTLSConfig(rt RuntimeConfig, t TLS) (tlsutil.Config, error mapCommon("https", t.HTTPS, &c.HTTPS) mapCommon("grpc", t.GRPC, &c.GRPC) + c.GRPC.UseAutoCert = boolValWithDefault(t.GRPC.UseAutoCert, false) c.ServerName = rt.ServerName c.NodeName = rt.NodeName diff --git a/agent/config/config.go b/agent/config/config.go index 145c74db7..de82d9876 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -332,10 +332,11 @@ type Consul struct { } type Addresses struct { - DNS *string `mapstructure:"dns"` - HTTP *string `mapstructure:"http"` - HTTPS *string `mapstructure:"https"` - GRPC *string `mapstructure:"grpc"` + DNS *string `mapstructure:"dns"` + HTTP *string `mapstructure:"http"` + HTTPS *string `mapstructure:"https"` + GRPC *string `mapstructure:"grpc"` + GRPCTLS *string `mapstructure:"grpc_tls"` } type AdvertiseAddrsConfig struct { @@ -694,6 +695,7 @@ type Ports struct { SerfWAN *int `mapstructure:"serf_wan"` Server *int `mapstructure:"server"` GRPC *int `mapstructure:"grpc"` + GRPCTLS *int `mapstructure:"grpc_tls"` ProxyMinPort *int `mapstructure:"proxy_min_port"` ProxyMaxPort *int `mapstructure:"proxy_max_port"` SidecarMinPort *int `mapstructure:"sidecar_min_port"` @@ -867,6 +869,7 @@ type TLSProtocolConfig struct { VerifyIncoming *bool `mapstructure:"verify_incoming"` VerifyOutgoing *bool `mapstructure:"verify_outgoing"` VerifyServerHostname *bool `mapstructure:"verify_server_hostname"` + UseAutoCert *bool `mapstructure:"use_auto_cert"` } type TLS struct { diff --git a/agent/config/flags.go b/agent/config/flags.go index b2e3c35ba..44554bd5e 100644 --- a/agent/config/flags.go +++ b/agent/config/flags.go @@ -53,7 +53,8 @@ func AddFlags(fs *flag.FlagSet, f *LoadOpts) { add(&f.FlagValues.EnableLocalScriptChecks, "enable-local-script-checks", "Enables health check scripts from configuration file.") add(&f.FlagValues.HTTPConfig.AllowWriteHTTPFrom, "allow-write-http-from", "Only allow write endpoint calls from given network. CIDR format, can be specified multiple times.") add(&f.FlagValues.EncryptKey, "encrypt", "Provides the gossip encryption key.") - add(&f.FlagValues.Ports.GRPC, "grpc-port", "Sets the gRPC API port to listen on (currently needed for Envoy xDS only).") + add(&f.FlagValues.Ports.GRPC, "grpc-port", "Sets the gRPC API port to listen on.") + add(&f.FlagValues.Ports.GRPCTLS, "grpc-tls-port", "Sets the gRPC-TLS API port to listen on.") add(&f.FlagValues.Ports.HTTP, "http-port", "Sets the HTTP API port to listen on.") add(&f.FlagValues.Ports.HTTPS, "https-port", "Sets the HTTPS API port to listen on.") add(&f.FlagValues.StartJoinAddrsLAN, "join", "Address of an agent to join at start time. Can be specified multiple times.") diff --git a/agent/config/runtime.go b/agent/config/runtime.go index e607efcf3..ee82ea477 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -670,13 +670,18 @@ type RuntimeConfig struct { // flag: -encrypt string EncryptKey string - // GRPCPort is the port the gRPC server listens on. Currently this only - // exposes the xDS and ext_authz APIs for Envoy and it is disabled by default. + // GRPCPort is the port the gRPC server listens on. It is disabled by default. // // hcl: ports { grpc = int } // flags: -grpc-port int GRPCPort int + // GRPCTLSPort is the port the gRPC server listens on. It is disabled by default. + // + // hcl: ports { grpc_tls = int } + // flags: -grpc-tls-port int + GRPCTLSPort int + // GRPCAddrs contains the list of TCP addresses and UNIX sockets the gRPC // server will bind to. If the gRPC endpoint is disabled (ports.grpc <= 0) // the list is empty. @@ -692,6 +697,21 @@ type RuntimeConfig struct { // hcl: client_addr = string addresses { grpc = string } ports { grpc = int } GRPCAddrs []net.Addr + // GRPCTLSAddrs contains the list of TCP addresses and UNIX sockets the gRPC + // server will bind to. If the gRPC endpoint is disabled (ports.grpc <= 0) + // the list is empty. + // + // The addresses are taken from 'addresses.grpc_tls' which should contain a + // space separated list of ip addresses, UNIX socket paths and/or + // go-sockaddr templates. UNIX socket paths must be written as + // 'unix://', e.g. 'unix:///var/run/consul-grpc.sock'. + // + // If 'addresses.grpc_tls' was not provided the 'client_addr' addresses are + // used. + // + // hcl: client_addr = string addresses { grpc_tls = string } ports { grpc_tls = int } + GRPCTLSAddrs []net.Addr + // HTTPAddrs contains the list of TCP addresses and UNIX sockets the HTTP // server will bind to. If the HTTP endpoint is disabled (ports.http <= 0) // the list is empty. diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index e0266811e..597899886 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -5516,7 +5516,70 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { }, }) run(t, testCase{ - desc: "tls.grpc without ports.https", + desc: "tls.grpc.use_auto_cert defaults to false", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": { + "grpc": {} + } + } + `}, + hcl: []string{` + tls { + grpc {} + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert defaults to false (II)", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": {} + } + `}, + hcl: []string{` + tls { + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert defaults to false (III)", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + } + `}, + hcl: []string{` + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false + }, + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert enabled when true", args: []string{ `-data-dir=` + dataDir, }, @@ -5524,7 +5587,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { { "tls": { "grpc": { - "cert_file": "cert-1234" + "use_auto_cert": true } } } @@ -5532,20 +5595,43 @@ func TestLoad_IntegrationWithFlags(t *testing.T) { hcl: []string{` tls { grpc { - cert_file = "cert-1234" + use_auto_cert = true } } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir - rt.TLS.Domain = "consul." rt.TLS.NodeName = "thehostname" - - rt.TLS.GRPC.CertFile = "cert-1234" + rt.TLS.GRPC.UseAutoCert = true }, - expectedWarnings: []string{ - "tls.grpc was provided but TLS will NOT be enabled on the gRPC listener without an HTTPS listener configured (e.g. via ports.https)", + }) + run(t, testCase{ + desc: "tls.grpc.use_auto_cert disabled when false", + args: []string{ + `-data-dir=` + dataDir, + }, + json: []string{` + { + "tls": { + "grpc": { + "use_auto_cert": false + } + } + } + `}, + hcl: []string{` + tls { + grpc { + use_auto_cert = false + } + } + `}, + expected: func(rt *RuntimeConfig) { + rt.DataDir = dataDir + rt.TLS.Domain = "consul." + rt.TLS.NodeName = "thehostname" + rt.TLS.GRPC.UseAutoCert = false }, }) } @@ -5930,6 +6016,8 @@ func TestLoad_FullConfig(t *testing.T) { GRPCPort: 4881, GRPCAddrs: []net.Addr{tcpAddr("32.31.61.91:4881")}, + GRPCTLSPort: 5201, + GRPCTLSAddrs: []net.Addr{tcpAddr("23.14.88.19:5201")}, HTTPAddrs: []net.Addr{tcpAddr("83.39.91.39:7999")}, HTTPBlockEndpoints: []string{"RBvAFcGD", "fWOWFznh"}, AllowWriteHTTPFrom: []*net.IPNet{cidr("127.0.0.0/8"), cidr("22.33.44.55/32"), cidr("0.0.0.0/0")}, @@ -6340,6 +6428,7 @@ func TestLoad_FullConfig(t *testing.T) { TLSMinVersion: types.TLSv1_0, CipherSuites: []types.TLSCipherSuite{types.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, types.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA}, VerifyOutgoing: false, + UseAutoCert: true, }, HTTPS: tlsutil.ProtocolConfig{ VerifyIncoming: true, diff --git a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden index 09ecd4cfe..a48d90971 100644 --- a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden +++ b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden @@ -192,6 +192,8 @@ "ExposeMinPort": 0, "GRPCAddrs": [], "GRPCPort": 0, + "GRPCTLSAddrs": [], + "GRPCTLSPort": 0, "GossipLANGossipInterval": "0s", "GossipLANGossipNodes": 0, "GossipLANProbeInterval": "0s", @@ -374,7 +376,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "HTTPS": { "CAFile": "", @@ -385,7 +388,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "InternalRPC": { "CAFile": "", @@ -396,7 +400,8 @@ "TLSMinVersion": "", "VerifyIncoming": false, "VerifyOutgoing": false, - "VerifyServerHostname": false + "VerifyServerHostname": false, + "UseAutoCert": false }, "NodeName": "", "ServerName": "" @@ -466,4 +471,4 @@ "VersionMetadata": "", "VersionPrerelease": "", "Watches": [] -} \ No newline at end of file +} diff --git a/agent/config/testdata/full-config.hcl b/agent/config/testdata/full-config.hcl index ed8203296..09e9aabd5 100644 --- a/agent/config/testdata/full-config.hcl +++ b/agent/config/testdata/full-config.hcl @@ -44,6 +44,7 @@ addresses = { http = "83.39.91.39" https = "95.17.17.19" grpc = "32.31.61.91" + grpc_tls = "23.14.88.19" } advertise_addr = "17.99.29.16" advertise_addr_wan = "78.63.37.19" @@ -320,6 +321,7 @@ ports { https = 15127 server = 3757 grpc = 4881 + grpc_tls = 5201 proxy_min_port = 2000 proxy_max_port = 3000 sidecar_min_port = 8888 @@ -697,6 +699,7 @@ tls { tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" tls_min_version = "TLSv1_0" verify_incoming = true + use_auto_cert = true } } tls_cipher_suites = "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" diff --git a/agent/config/testdata/full-config.json b/agent/config/testdata/full-config.json index 8294a27b7..946b27e73 100644 --- a/agent/config/testdata/full-config.json +++ b/agent/config/testdata/full-config.json @@ -44,7 +44,8 @@ "dns": "93.95.95.81", "http": "83.39.91.39", "https": "95.17.17.19", - "grpc": "32.31.61.91" + "grpc": "32.31.61.91", + "grpc_tls": "23.14.88.19" }, "advertise_addr": "17.99.29.16", "advertise_addr_wan": "78.63.37.19", @@ -320,6 +321,7 @@ "https": 15127, "server": 3757, "grpc": 4881, + "grpc_tls": 5201, "sidecar_min_port": 8888, "sidecar_max_port": 9999, "expose_min_port": 1111, @@ -692,7 +694,8 @@ "key_file": "1y4prKjl", "tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "tls_min_version": "TLSv1_0", - "verify_incoming": true + "verify_incoming": true, + "use_auto_cert": true } }, "tls_cipher_suites": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", diff --git a/agent/configentry/resolve.go b/agent/configentry/resolve.go new file mode 100644 index 000000000..f6090e98f --- /dev/null +++ b/agent/configentry/resolve.go @@ -0,0 +1,243 @@ +package configentry + +import ( + "fmt" + + "github.com/hashicorp/go-hclog" + "github.com/mitchellh/copystructure" + + "github.com/hashicorp/consul/agent/structs" +) + +func ComputeResolvedServiceConfig( + args *structs.ServiceConfigRequest, + upstreamIDs []structs.ServiceID, + legacyUpstreams bool, + entries *ResolvedServiceConfigSet, + logger hclog.Logger, +) (*structs.ServiceConfigResponse, error) { + var thisReply structs.ServiceConfigResponse + + thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault + + // TODO(freddy) Refactor this into smaller set of state store functions + // Pass the WatchSet to both the service and proxy config lookups. If either is updated during the + // blocking query, this function will be rerun and these state store lookups will both be current. + // We use the default enterprise meta to look up the global proxy defaults because they are not namespaced. + var proxyConfGlobalProtocol string + proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault()) + if proxyConf != nil { + // Apply the proxy defaults to the sidecar's proxy config + mapCopy, err := copystructure.Copy(proxyConf.Config) + if err != nil { + return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err) + } + thisReply.ProxyConfig = mapCopy.(map[string]interface{}) + thisReply.Mode = proxyConf.Mode + thisReply.TransparentProxy = proxyConf.TransparentProxy + thisReply.MeshGateway = proxyConf.MeshGateway + thisReply.Expose = proxyConf.Expose + + // Extract the global protocol from proxyConf for upstream configs. + rawProtocol := proxyConf.Config["protocol"] + if rawProtocol != nil { + var ok bool + proxyConfGlobalProtocol, ok = rawProtocol.(string) + if !ok { + return nil, fmt.Errorf("invalid protocol type %T", rawProtocol) + } + } + } + + serviceConf := entries.GetServiceDefaults( + structs.NewServiceID(args.Name, &args.EnterpriseMeta), + ) + if serviceConf != nil { + if serviceConf.Expose.Checks { + thisReply.Expose.Checks = true + } + if len(serviceConf.Expose.Paths) >= 1 { + thisReply.Expose.Paths = serviceConf.Expose.Paths + } + if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault { + thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode + } + if serviceConf.Protocol != "" { + if thisReply.ProxyConfig == nil { + thisReply.ProxyConfig = make(map[string]interface{}) + } + thisReply.ProxyConfig["protocol"] = serviceConf.Protocol + } + if serviceConf.TransparentProxy.OutboundListenerPort != 0 { + thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort + } + if serviceConf.TransparentProxy.DialedDirectly { + thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly + } + if serviceConf.Mode != structs.ProxyModeDefault { + thisReply.Mode = serviceConf.Mode + } + if serviceConf.Destination != nil { + thisReply.Destination = *serviceConf.Destination + } + + if serviceConf.MaxInboundConnections > 0 { + if thisReply.ProxyConfig == nil { + thisReply.ProxyConfig = map[string]interface{}{} + } + thisReply.ProxyConfig["max_inbound_connections"] = serviceConf.MaxInboundConnections + } + + if serviceConf.LocalConnectTimeoutMs > 0 { + if thisReply.ProxyConfig == nil { + thisReply.ProxyConfig = map[string]interface{}{} + } + thisReply.ProxyConfig["local_connect_timeout_ms"] = serviceConf.LocalConnectTimeoutMs + } + + if serviceConf.LocalRequestTimeoutMs > 0 { + if thisReply.ProxyConfig == nil { + thisReply.ProxyConfig = map[string]interface{}{} + } + thisReply.ProxyConfig["local_request_timeout_ms"] = serviceConf.LocalRequestTimeoutMs + } + + thisReply.Meta = serviceConf.Meta + } + + // First collect all upstreams into a set of seen upstreams. + // Upstreams can come from: + // - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint + // - Implicitly from centralized upstream config in service-defaults + seenUpstreams := map[structs.ServiceID]struct{}{} + + var ( + noUpstreamArgs = len(upstreamIDs) == 0 && len(args.Upstreams) == 0 + + // Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode + // will never be transparent because the service config request does not use the resolved value. + tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent + ) + + // The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration. + // If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode. + // Otherwise we would return a resolved upstream config to a proxy with no configured upstreams. + if noUpstreamArgs && !tproxy { + return &thisReply, nil + } + + // First store all upstreams that were provided in the request + for _, sid := range upstreamIDs { + if _, ok := seenUpstreams[sid]; !ok { + seenUpstreams[sid] = struct{}{} + } + } + + // Then store upstreams inferred from service-defaults and mapify the overrides. + var ( + upstreamConfigs = make(map[structs.ServiceID]*structs.UpstreamConfig) + upstreamDefaults *structs.UpstreamConfig + // usConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID. + usConfigs = make(map[structs.ServiceID]map[string]interface{}) + ) + if serviceConf != nil && serviceConf.UpstreamConfig != nil { + for i, override := range serviceConf.UpstreamConfig.Overrides { + if override.Name == "" { + logger.Warn( + "Skipping UpstreamConfig.Overrides entry without a required name field", + "entryIndex", i, + "kind", serviceConf.GetKind(), + "name", serviceConf.GetName(), + "namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(), + ) + continue // skip this impossible condition + } + seenUpstreams[override.ServiceID()] = struct{}{} + upstreamConfigs[override.ServiceID()] = override + } + if serviceConf.UpstreamConfig.Defaults != nil { + upstreamDefaults = serviceConf.UpstreamConfig.Defaults + + // Store the upstream defaults under a wildcard key so that they can be applied to + // upstreams that are inferred from intentions and do not have explicit upstream configuration. + cfgMap := make(map[string]interface{}) + upstreamDefaults.MergeInto(cfgMap) + + wildcard := structs.NewServiceID(structs.WildcardSpecifier, args.WithWildcardNamespace()) + usConfigs[wildcard] = cfgMap + } + } + + for upstream := range seenUpstreams { + resolvedCfg := make(map[string]interface{}) + + // The protocol of an upstream is resolved in this order: + // 1. Default protocol from proxy-defaults (how all services should be addressed) + // 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed) + // 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream + // (how the downstream wants to address it) + protocol := proxyConfGlobalProtocol + + upstreamSvcDefaults := entries.GetServiceDefaults( + structs.NewServiceID(upstream.ID, &upstream.EnterpriseMeta), + ) + if upstreamSvcDefaults != nil { + if upstreamSvcDefaults.Protocol != "" { + protocol = upstreamSvcDefaults.Protocol + } + } + + if protocol != "" { + resolvedCfg["protocol"] = protocol + } + + // Merge centralized defaults for all upstreams before configuration for specific upstreams + if upstreamDefaults != nil { + upstreamDefaults.MergeInto(resolvedCfg) + } + + // The MeshGateway value from the proxy registration overrides the one from upstream_defaults + // because it is specific to the proxy instance. + // + // The goal is to flatten the mesh gateway mode in this order: + // 0. Value from centralized upstream_defaults + // 1. Value from local proxy registration + // 2. Value from centralized upstream_config + // 3. Value from local upstream definition. This last step is done in the client's service manager. + if !args.MeshGateway.IsZero() { + resolvedCfg["mesh_gateway"] = args.MeshGateway + } + + if upstreamConfigs[upstream] != nil { + upstreamConfigs[upstream].MergeInto(resolvedCfg) + } + + if len(resolvedCfg) > 0 { + usConfigs[upstream] = resolvedCfg + } + } + + // don't allocate the slices just to not fill them + if len(usConfigs) == 0 { + return &thisReply, nil + } + + if legacyUpstreams { + // For legacy upstreams we return a map that is only keyed on the string ID, since they precede namespaces + thisReply.UpstreamConfigs = make(map[string]map[string]interface{}) + + for us, conf := range usConfigs { + thisReply.UpstreamConfigs[us.ID] = conf + } + + } else { + thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(usConfigs)) + + for us, conf := range usConfigs { + thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs, + structs.OpaqueUpstreamConfig{Upstream: us, Config: conf}) + } + } + + return &thisReply, nil +} diff --git a/agent/configentry/resolve_test.go b/agent/configentry/resolve_test.go new file mode 100644 index 000000000..301472c1c --- /dev/null +++ b/agent/configentry/resolve_test.go @@ -0,0 +1,80 @@ +package configentry + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent/structs" +) + +func Test_ComputeResolvedServiceConfig(t *testing.T) { + type args struct { + scReq *structs.ServiceConfigRequest + upstreamIDs []structs.ServiceID + entries *ResolvedServiceConfigSet + } + + sid := structs.ServiceID{ + ID: "sid", + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + } + tests := []struct { + name string + args args + want *structs.ServiceConfigResponse + }{ + { + name: "proxy with maxinboundsconnections", + args: args{ + scReq: &structs.ServiceConfigRequest{ + Name: "sid", + }, + entries: &ResolvedServiceConfigSet{ + ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{ + sid: { + MaxInboundConnections: 20, + }, + }, + }, + }, + want: &structs.ServiceConfigResponse{ + ProxyConfig: map[string]interface{}{ + "max_inbound_connections": 20, + }, + }, + }, + { + name: "proxy with local_connect_timeout_ms and local_request_timeout_ms", + args: args{ + scReq: &structs.ServiceConfigRequest{ + Name: "sid", + }, + entries: &ResolvedServiceConfigSet{ + ServiceDefaults: map[structs.ServiceID]*structs.ServiceConfigEntry{ + sid: { + MaxInboundConnections: 20, + LocalConnectTimeoutMs: 20000, + LocalRequestTimeoutMs: 30000, + }, + }, + }, + }, + want: &structs.ServiceConfigResponse{ + ProxyConfig: map[string]interface{}{ + "max_inbound_connections": 20, + "local_connect_timeout_ms": 20000, + "local_request_timeout_ms": 30000, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ComputeResolvedServiceConfig(tt.args.scReq, tt.args.upstreamIDs, + false, tt.args.entries, nil) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 270d53a01..fd57366bd 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -66,11 +66,10 @@ type VaultProvider struct { stopWatcher func() - isPrimary bool - clusterID string - spiffeID *connect.SpiffeIDSigning - setupIntermediatePKIPathDone bool - logger hclog.Logger + isPrimary bool + clusterID string + spiffeID *connect.SpiffeIDSigning + logger hclog.Logger } func NewVaultProvider(logger hclog.Logger) *VaultProvider { @@ -174,6 +173,11 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error { go v.renewToken(ctx, lifetimeWatcher) } + // Update the intermediate (managed) PKI mount and role + if err := v.setupIntermediatePKIPath(); err != nil { + return err + } + return nil } @@ -363,8 +367,8 @@ func (v *VaultProvider) GenerateIntermediateCSR() (string, error) { } func (v *VaultProvider) setupIntermediatePKIPath() error { - if v.setupIntermediatePKIPathDone { - return nil + mountConfig := vaultapi.MountConfigInput{ + MaxLeaseTTL: v.config.IntermediateCertTTL.String(), } _, err := v.getCA(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath) @@ -373,9 +377,7 @@ func (v *VaultProvider) setupIntermediatePKIPath() error { err := v.mountNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath, &vaultapi.MountInput{ Type: "pki", Description: "intermediate CA backend for Consul Connect", - Config: vaultapi.MountConfigInput{ - MaxLeaseTTL: v.config.IntermediateCertTTL.String(), - }, + Config: mountConfig, }) if err != nil { return err @@ -383,39 +385,28 @@ func (v *VaultProvider) setupIntermediatePKIPath() error { } else { return err } - } - - // Create the role for issuing leaf certs if it doesn't exist yet - rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole - role, err := v.readNamespaced(v.config.IntermediatePKINamespace, rolePath) - - if err != nil { - return err - } - if role == nil { - _, err := v.writeNamespaced(v.config.IntermediatePKINamespace, rolePath, map[string]interface{}{ - "allow_any_name": true, - "allowed_uri_sans": "spiffe://*", - "key_type": "any", - "max_ttl": v.config.LeafCertTTL.String(), - "no_store": true, - "require_cn": false, - }) - + } else { + err := v.tuneMountNamespaced(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath, &mountConfig) if err != nil { return err } } - v.setupIntermediatePKIPathDone = true - return nil + + // Create the role for issuing leaf certs if it doesn't exist yet + rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole + _, err = v.writeNamespaced(v.config.IntermediatePKINamespace, rolePath, map[string]interface{}{ + "allow_any_name": true, + "allowed_uri_sans": "spiffe://*", + "key_type": "any", + "max_ttl": v.config.LeafCertTTL.String(), + "no_store": true, + "require_cn": false, + }) + + return err } func (v *VaultProvider) generateIntermediateCSR() (string, error) { - err := v.setupIntermediatePKIPath() - if err != nil { - return "", err - } - // Generate a new intermediate CSR for the root to sign. uid, err := connect.CompactUID() if err != nil { @@ -465,10 +456,6 @@ func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error { // ActiveIntermediate returns the current intermediate certificate. func (v *VaultProvider) ActiveIntermediate() (string, error) { - if err := v.setupIntermediatePKIPath(); err != nil { - return "", err - } - cert, err := v.getCA(v.config.IntermediatePKINamespace, v.config.IntermediatePKIPath) // This error is expected when calling initializeSecondaryCA for the @@ -737,6 +724,19 @@ func (v *VaultProvider) mountNamespaced(namespace, path string, mountInfo *vault return err } +func (v *VaultProvider) tuneMountNamespaced(namespace, path string, mountConfig *vaultapi.MountConfigInput) error { + defer v.setNamespace(namespace)() + r := v.client.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s/tune", path)) + if err := r.SetJSONBody(mountConfig); err != nil { + return err + } + resp, err := v.client.RawRequest(r) + if resp != nil { + defer resp.Body.Close() + } + return err +} + func (v *VaultProvider) unmountNamespaced(namespace, path string) error { defer v.setNamespace(namespace)() r := v.client.NewRequest("DELETE", fmt.Sprintf("/v1/sys/mounts/%s", path)) diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index 11689ae69..66b89ba9f 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -19,6 +19,16 @@ import ( "github.com/hashicorp/consul/sdk/testutil/retry" ) +const pkiTestPolicy = ` +path "sys/mounts/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +} +path "pki-intermediate/*" +{ + capabilities = ["create", "read", "update", "delete", "list", "sudo"] +}` + func TestVaultCAProvider_ParseVaultCAConfig(t *testing.T) { cases := map[string]struct { rawConfig map[string]interface{} @@ -653,7 +663,7 @@ func TestVaultProvider_ConfigureWithAuthMethod(t *testing.T) { authMethodType: "userpass", configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} { _, err := vaultClient.Logical().Write("/auth/userpass/users/test", - map[string]interface{}{"password": "foo", "policies": "admins"}) + map[string]interface{}{"password": "foo", "policies": "pki"}) require.NoError(t, err) return map[string]interface{}{ "Type": "userpass", @@ -667,7 +677,8 @@ func TestVaultProvider_ConfigureWithAuthMethod(t *testing.T) { { authMethodType: "approle", configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} { - _, err := vaultClient.Logical().Write("auth/approle/role/my-role", nil) + _, err := vaultClient.Logical().Write("auth/approle/role/my-role", + map[string]interface{}{"token_policies": "pki"}) require.NoError(t, err) resp, err := vaultClient.Logical().Read("auth/approle/role/my-role/role-id") require.NoError(t, err) @@ -695,6 +706,9 @@ func TestVaultProvider_ConfigureWithAuthMethod(t *testing.T) { err := testVault.Client().Sys().EnableAuthWithOptions(c.authMethodType, &vaultapi.EnableAuthOptions{Type: c.authMethodType}) require.NoError(t, err) + err = testVault.Client().Sys().PutPolicy("pki", pkiTestPolicy) + require.NoError(t, err) + authMethodConf := c.configureAuthMethodFunc(t, testVault.Client()) conf := map[string]interface{}{ @@ -726,11 +740,18 @@ func TestVaultProvider_RotateAuthMethodToken(t *testing.T) { testVault := NewTestVaultServer(t) - err := testVault.Client().Sys().EnableAuthWithOptions("approle", &vaultapi.EnableAuthOptions{Type: "approle"}) + err := testVault.Client().Sys().PutPolicy("pki", pkiTestPolicy) + require.NoError(t, err) + + err = testVault.Client().Sys().EnableAuthWithOptions("approle", &vaultapi.EnableAuthOptions{Type: "approle"}) require.NoError(t, err) _, err = testVault.Client().Logical().Write("auth/approle/role/my-role", - map[string]interface{}{"token_ttl": "2s", "token_explicit_max_ttl": "2s"}) + map[string]interface{}{ + "token_ttl": "2s", + "token_explicit_max_ttl": "2s", + "token_policies": "pki", + }) require.NoError(t, err) resp, err := testVault.Client().Logical().Read("auth/approle/role/my-role/role-id") require.NoError(t, err) diff --git a/agent/connect/sni_test.go b/agent/connect/sni_test.go index 26fae1da7..59e9f41fc 100644 --- a/agent/connect/sni_test.go +++ b/agent/connect/sni_test.go @@ -178,20 +178,43 @@ func TestQuerySNI(t *testing.T) { func TestTargetSNI(t *testing.T) { // empty namespace, empty subset require.Equal(t, "api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "", "", "default", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain1)) require.Equal(t, "api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "", "", "", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Datacenter: "foo", + }), testTrustDomain1)) // set namespace, empty subset require.Equal(t, "api.neighbor.foo."+testTrustDomainSuffix2, - TargetSNI(structs.NewDiscoveryTarget("api", "", "neighbor", "default", "foo"), testTrustDomain2)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + Namespace: "neighbor", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain2)) // empty namespace, set subset require.Equal(t, "v2.api.default.foo."+testTrustDomainSuffix1, - TargetSNI(structs.NewDiscoveryTarget("api", "v2", "", "default", "foo"), testTrustDomain1)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + ServiceSubset: "v2", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain1)) // set namespace, set subset require.Equal(t, "canary.api.neighbor.foo."+testTrustDomainSuffix2, - TargetSNI(structs.NewDiscoveryTarget("api", "canary", "neighbor", "default", "foo"), testTrustDomain2)) + TargetSNI(structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "api", + ServiceSubset: "canary", + Namespace: "neighbor", + Partition: "default", + Datacenter: "foo", + }), testTrustDomain2)) } diff --git a/agent/connect/uri.go b/agent/connect/uri.go index 18f888d25..6d64be3b4 100644 --- a/agent/connect/uri.go +++ b/agent/connect/uri.go @@ -24,6 +24,8 @@ var ( `^(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`) spiffeIDAgentRegexp = regexp.MustCompile( `^(?:/ap/([^/]+))?/agent/client/dc/([^/]+)/id/([^/]+)$`) + spiffeIDServerRegexp = regexp.MustCompile( + `^/agent/server/dc/([^/]+)$`) spiffeIDMeshGatewayRegexp = regexp.MustCompile( `^(?:/ap/([^/]+))?/gateway/mesh/dc/([^/]+)$`) ) @@ -144,6 +146,19 @@ func ParseCertURI(input *url.URL) (CertURI, error) { Partition: ap, Datacenter: dc, }, nil + } else if v := spiffeIDServerRegexp.FindStringSubmatch(path); v != nil { + dc := v[1] + if input.RawPath != "" { + var err error + if dc, err = url.PathUnescape(v[1]); err != nil { + return nil, fmt.Errorf("Invalid datacenter: %s", err) + } + } + + return &SpiffeIDServer{ + Host: input.Host, + Datacenter: dc, + }, nil } // Test for signing ID diff --git a/agent/connect/uri_server.go b/agent/connect/uri_server.go new file mode 100644 index 000000000..3d120b5b9 --- /dev/null +++ b/agent/connect/uri_server.go @@ -0,0 +1,20 @@ +package connect + +import ( + "fmt" + "net/url" +) + +type SpiffeIDServer struct { + Host string + Datacenter string +} + +// URI returns the *url.URL for this SPIFFE ID. +func (id SpiffeIDServer) URI() *url.URL { + var result url.URL + result.Scheme = "spiffe" + result.Host = id.Host + result.Path = fmt.Sprintf("/agent/server/dc/%s", id.Datacenter) + return &result +} diff --git a/agent/connect/uri_signing.go b/agent/connect/uri_signing.go index 926674880..67ef7ecea 100644 --- a/agent/connect/uri_signing.go +++ b/agent/connect/uri_signing.go @@ -54,6 +54,12 @@ func (id SpiffeIDSigning) CanSign(cu CertURI) bool { // worry about Unicode domains if we start allowing customisation beyond the // built-in cluster ids. return strings.ToLower(other.Host) == id.Host() + case *SpiffeIDServer: + // The host component of the service must be an exact match for now under + // ascii case folding (since hostnames are case-insensitive). Later we might + // worry about Unicode domains if we start allowing customisation beyond the + // built-in cluster ids. + return strings.ToLower(other.Host) == id.Host() default: return false } diff --git a/agent/connect/uri_signing_test.go b/agent/connect/uri_signing_test.go index e09ac78c6..6fef1f811 100644 --- a/agent/connect/uri_signing_test.go +++ b/agent/connect/uri_signing_test.go @@ -78,7 +78,7 @@ func TestSpiffeIDSigning_CanSign(t *testing.T) { want: true, }, { - name: "service - good midex case", + name: "service - good mixed case", id: testSigning, input: &SpiffeIDService{Host: strings.ToUpper(TestClusterID) + ".CONsuL", Namespace: "defAUlt", Datacenter: "dc1", Service: "WEB"}, want: true, @@ -102,7 +102,7 @@ func TestSpiffeIDSigning_CanSign(t *testing.T) { want: true, }, { - name: "mesh gateway - good midex case", + name: "mesh gateway - good mixed case", id: testSigning, input: &SpiffeIDMeshGateway{Host: strings.ToUpper(TestClusterID) + ".CONsuL", Datacenter: "dc1"}, want: true, @@ -119,6 +119,30 @@ func TestSpiffeIDSigning_CanSign(t *testing.T) { input: &SpiffeIDMeshGateway{Host: TestClusterID + ".fake", Datacenter: "dc1"}, want: false, }, + { + name: "server - good", + id: testSigning, + input: &SpiffeIDServer{Host: TestClusterID + ".consul", Datacenter: "dc1"}, + want: true, + }, + { + name: "server - good mixed case", + id: testSigning, + input: &SpiffeIDServer{Host: strings.ToUpper(TestClusterID) + ".CONsuL", Datacenter: "dc1"}, + want: true, + }, + { + name: "server - different cluster", + id: testSigning, + input: &SpiffeIDServer{Host: "55555555-4444-3333-2222-111111111111.consul", Datacenter: "dc1"}, + want: false, + }, + { + name: "server - different TLD", + id: testSigning, + input: &SpiffeIDServer{Host: TestClusterID + ".fake", Datacenter: "dc1"}, + want: false, + }, } for _, tt := range tests { diff --git a/agent/connect/uri_test.go b/agent/connect/uri_test.go index 9c2849c4c..1cc3f7a1f 100644 --- a/agent/connect/uri_test.go +++ b/agent/connect/uri_test.go @@ -19,109 +19,118 @@ func TestParseCertURIFromString(t *testing.T) { ParseError string }{ { - "invalid scheme", - "http://google.com/", - nil, - "scheme", + Name: "invalid scheme", + URI: "http://google.com/", + Struct: nil, + ParseError: "scheme", }, { - "basic service ID", - "spiffe://1234.consul/ns/default/dc/dc01/svc/web", - &SpiffeIDService{ + Name: "basic service ID", + URI: "spiffe://1234.consul/ns/default/dc/dc01/svc/web", + Struct: &SpiffeIDService{ Host: "1234.consul", Partition: defaultEntMeta.PartitionOrDefault(), Namespace: "default", Datacenter: "dc01", Service: "web", }, - "", + ParseError: "", }, { - "basic service ID with partition", - "spiffe://1234.consul/ap/bizdev/ns/default/dc/dc01/svc/web", - &SpiffeIDService{ + Name: "basic service ID with partition", + URI: "spiffe://1234.consul/ap/bizdev/ns/default/dc/dc01/svc/web", + Struct: &SpiffeIDService{ Host: "1234.consul", Partition: "bizdev", Namespace: "default", Datacenter: "dc01", Service: "web", }, - "", + ParseError: "", }, { - "basic agent ID", - "spiffe://1234.consul/agent/client/dc/dc1/id/uuid", - &SpiffeIDAgent{ + Name: "basic agent ID", + URI: "spiffe://1234.consul/agent/client/dc/dc1/id/uuid", + Struct: &SpiffeIDAgent{ Host: "1234.consul", Partition: defaultEntMeta.PartitionOrDefault(), Datacenter: "dc1", Agent: "uuid", }, - "", + ParseError: "", }, { - "basic agent ID with partition", - "spiffe://1234.consul/ap/bizdev/agent/client/dc/dc1/id/uuid", - &SpiffeIDAgent{ + Name: "basic agent ID with partition", + URI: "spiffe://1234.consul/ap/bizdev/agent/client/dc/dc1/id/uuid", + Struct: &SpiffeIDAgent{ Host: "1234.consul", Partition: "bizdev", Datacenter: "dc1", Agent: "uuid", }, - "", + ParseError: "", }, { - "mesh-gateway with no partition", - "spiffe://1234.consul/gateway/mesh/dc/dc1", - &SpiffeIDMeshGateway{ + Name: "basic server", + URI: "spiffe://1234.consul/agent/server/dc/dc1", + Struct: &SpiffeIDServer{ + Host: "1234.consul", + Datacenter: "dc1", + }, + ParseError: "", + }, + { + Name: "mesh-gateway with no partition", + URI: "spiffe://1234.consul/gateway/mesh/dc/dc1", + Struct: &SpiffeIDMeshGateway{ Host: "1234.consul", Partition: "default", Datacenter: "dc1", }, - "", + ParseError: "", }, { - "mesh-gateway with partition", - "spiffe://1234.consul/ap/bizdev/gateway/mesh/dc/dc1", - &SpiffeIDMeshGateway{ + Name: "mesh-gateway with partition", + URI: "spiffe://1234.consul/ap/bizdev/gateway/mesh/dc/dc1", + Struct: &SpiffeIDMeshGateway{ Host: "1234.consul", Partition: "bizdev", Datacenter: "dc1", }, - "", + ParseError: "", }, { - "service with URL-encoded values", - "spiffe://1234.consul/ns/foo%2Fbar/dc/bar%2Fbaz/svc/baz%2Fqux", - &SpiffeIDService{ + Name: "service with URL-encoded values", + URI: "spiffe://1234.consul/ns/foo%2Fbar/dc/bar%2Fbaz/svc/baz%2Fqux", + Struct: &SpiffeIDService{ Host: "1234.consul", Partition: defaultEntMeta.PartitionOrDefault(), Namespace: "foo/bar", Datacenter: "bar/baz", Service: "baz/qux", }, - "", + ParseError: "", }, { - "service with URL-encoded values with partition", - "spiffe://1234.consul/ap/biz%2Fdev/ns/foo%2Fbar/dc/bar%2Fbaz/svc/baz%2Fqux", - &SpiffeIDService{ + Name: "service with URL-encoded values with partition", + URI: "spiffe://1234.consul/ap/biz%2Fdev/ns/foo%2Fbar/dc/bar%2Fbaz/svc/baz%2Fqux", + Struct: &SpiffeIDService{ Host: "1234.consul", Partition: "biz/dev", Namespace: "foo/bar", Datacenter: "bar/baz", Service: "baz/qux", }, - "", + ParseError: "", }, { - "signing ID", - "spiffe://1234.consul", - &SpiffeIDSigning{ + Name: "signing ID", + URI: "spiffe://1234.consul", + Struct: &SpiffeIDSigning{ ClusterID: "1234", Domain: "consul", }, - "", + ParseError: "", }, } @@ -139,3 +148,12 @@ func TestParseCertURIFromString(t *testing.T) { }) } } + +func TestSpiffeIDServer_URI(t *testing.T) { + srv := &SpiffeIDServer{ + Host: "1234.consul", + Datacenter: "dc1", + } + + require.Equal(t, "spiffe://1234.consul/agent/server/dc/dc1", srv.URI().String()) +} diff --git a/agent/consul/auto_config_endpoint.go b/agent/consul/auto_config_endpoint.go index 088c9a3e0..7eda55b67 100644 --- a/agent/consul/auto_config_endpoint.go +++ b/agent/consul/auto_config_endpoint.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/base64" "fmt" + "regexp" "github.com/hashicorp/consul/acl" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/consul/authmethod/ssoauth" + "github.com/hashicorp/consul/agent/dns" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib/template" "github.com/hashicorp/consul/proto/pbautoconf" @@ -51,6 +53,11 @@ type jwtAuthorizer struct { claimAssertions []string } +// Invalidate any quote or whitespace characters that could cause an escape with bexpr. +// This includes an extra single-quote character not specified in the grammar for safety in case it is later added. +// https://github.com/hashicorp/go-bexpr/blob/v0.1.11/grammar/grammar.peg#L188-L191 +var invalidSegmentName = regexp.MustCompile("[`'\"\\s]+") + func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) { // perform basic JWT Authorization identity, err := a.validator.ValidateLogin(context.Background(), req.JWT) @@ -59,6 +66,21 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err) } + // Ensure provided data cannot escape the RHS of a bexpr for security. + // This is not the cleanest way to prevent this behavior. Ideally, the bexpr would allow us to + // inject a variable on the RHS for comparison as well, but it would be a complex change to implement + // that would likely break backwards-compatibility in certain circumstances. + if dns.InvalidNameRe.MatchString(req.Node) { + return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "node", req.Node) + } + if invalidSegmentName.MatchString(req.Segment) { + return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "segment", req.Segment) + } + if req.Partition != "" && !dns.IsValidLabel(req.Partition) { + return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "partition", req.Partition) + } + + // Ensure that every value in this mapping is safe to interpolate before using it. varMap := map[string]string{ "node": req.Node, "segment": req.Segment, @@ -372,9 +394,12 @@ func parseAutoConfigCSR(csr string) (*x509.CertificateRequest, *connect.SpiffeID return nil, nil, fmt.Errorf("Failed to parse CSR: %w", err) } - // ensure that a URI SAN is present - if len(x509CSR.URIs) < 1 { - return nil, nil, fmt.Errorf("CSR didn't include any URI SANs") + // ensure that exactly one URI SAN is present + if len(x509CSR.URIs) != 1 { + return nil, nil, fmt.Errorf("CSR SAN contains an invalid number of URIs: %v", len(x509CSR.URIs)) + } + if len(x509CSR.EmailAddresses) > 0 { + return nil, nil, fmt.Errorf("CSR SAN does not allow specifying email addresses") } // Parse the SPIFFE ID diff --git a/agent/consul/auto_config_endpoint_test.go b/agent/consul/auto_config_endpoint_test.go index 43df5fdab..1036044fa 100644 --- a/agent/consul/auto_config_endpoint_test.go +++ b/agent/consul/auto_config_endpoint_test.go @@ -1,12 +1,17 @@ package consul import ( + "bytes" + "crypto" + crand "crypto/rand" "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "io/ioutil" "math/rand" "net" + "net/url" "path" "testing" "time" @@ -92,9 +97,9 @@ func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{}) // TestAutoConfigInitialConfiguration is really an integration test of all the moving parts of the AutoConfig.InitialConfiguration RPC. // Full testing of the individual parts will not be done in this test: // -// * Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer) -// * Each of the individual config generation functions. These can be unit tested separately and should NOT -// require running test servers +// - Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer) +// - Each of the individual config generation functions. These can be unit tested separately and should NOT +// require running test servers func TestAutoConfigInitialConfiguration(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -236,6 +241,29 @@ func TestAutoConfigInitialConfiguration(t *testing.T) { }, err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature", }, + "bad-req-node": { + request: &pbautoconf.AutoConfigRequest{ + Node: "bad node", + JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}), + }, + err: "Invalid request field. node =", + }, + "bad-req-segment": { + request: &pbautoconf.AutoConfigRequest{ + Node: "test-node", + Segment: "bad segment", + JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}), + }, + err: "Invalid request field. segment =", + }, + "bad-req-partition": { + request: &pbautoconf.AutoConfigRequest{ + Node: "test-node", + Partition: "bad partition", + JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}), + }, + err: "Invalid request field. partition =", + }, "claim-assertion-failed": { request: &pbautoconf.AutoConfigRequest{ Node: "test-node", @@ -850,3 +878,159 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) { backend.AssertExpectations(t) } + +func TestAutoConfig_parseAutoConfigCSR(t *testing.T) { + // createCSR copies the behavior of connect.CreateCSR with some + // customizations to allow for better unit testing. + createCSR := func(tmpl *x509.CertificateRequest, privateKey crypto.Signer) (string, error) { + connect.HackSANExtensionForCSR(tmpl) + bs, err := x509.CreateCertificateRequest(crand.Reader, tmpl, privateKey) + require.NoError(t, err) + var csrBuf bytes.Buffer + err = pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) + require.NoError(t, err) + return csrBuf.String(), nil + } + pk, _, err := connect.GeneratePrivateKey() + require.NoError(t, err) + + agentURI := connect.SpiffeIDAgent{ + Host: "test-host", + Datacenter: "tdc1", + Agent: "test-agent", + }.URI() + + tests := []struct { + name string + setup func() string + expectErr string + }{ + { + name: "err_garbage_data", + expectErr: "Failed to parse CSR", + setup: func() string { return "garbage" }, + }, + { + name: "err_not_one_uri", + expectErr: "CSR SAN contains an invalid number of URIs", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI, agentURI}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_email", + expectErr: "CSR SAN does not allow specifying email addresses", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI}, + EmailAddresses: []string{"test@example.com"}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_spiffe_parse_uri", + expectErr: "Failed to parse the SPIFFE URI", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{connect.SpiffeIDAgent{}.URI()}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "err_not_agent", + expectErr: "SPIFFE ID is not an Agent ID", + setup: func() string { + spiffe := connect.SpiffeIDService{ + Namespace: "tns", + Datacenter: "tdc1", + Service: "test-service", + } + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{spiffe.URI()}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + { + name: "success", + setup: func() string { + tmpl := &x509.CertificateRequest{ + URIs: []*url.URL{agentURI}, + SignatureAlgorithm: connect.SigAlgoForKey(pk), + } + csr, err := createCSR(tmpl, pk) + require.NoError(t, err) + return csr + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req, spif, err := parseAutoConfigCSR(tc.setup()) + if tc.expectErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + // TODO better verification of these + require.NotNil(t, req) + require.NotNil(t, spif) + } + + }) + } +} + +func TestAutoConfig_invalidSegmentName(t *testing.T) { + invalid := []string{ + "\n", + "\r", + "\t", + "`", + `'`, + `"`, + ` `, + `a b`, + `a'b`, + `a or b`, + `a and b`, + `segment name`, + `segment"name`, + `"segment"name`, + `"segment" name`, + `segment'name'`, + } + valid := []string{ + ``, + `a`, + `a.b`, + `a.b.c`, + `a-b-c`, + `segment.name`, + } + + for _, s := range invalid { + require.True(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s) + } + for _, s := range valid { + require.False(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s) + } +} diff --git a/agent/consul/autopilot.go b/agent/consul/autopilot.go index 58acd20ff..a41febb05 100644 --- a/agent/consul/autopilot.go +++ b/agent/consul/autopilot.go @@ -55,6 +55,14 @@ func (d *AutopilotDelegate) NotifyState(state *autopilot.State) { } d.readyServersPublisher.PublishReadyServersEvents(state) + + var readyServers uint32 + for _, server := range state.Servers { + if autopilotevents.IsServerReady(server) { + readyServers++ + } + } + d.server.xdsCapacityController.SetServerCount(readyServers) } func (d *AutopilotDelegate) RemoveFailedServer(srv *autopilot.Server) { diff --git a/agent/consul/autopilotevents/mock_StateStore_test.go b/agent/consul/autopilotevents/mock_StateStore_test.go index 0262b410a..dd048e58e 100644 --- a/agent/consul/autopilotevents/mock_StateStore_test.go +++ b/agent/consul/autopilotevents/mock_StateStore_test.go @@ -4,6 +4,8 @@ package autopilotevents import ( acl "github.com/hashicorp/consul/acl" + memdb "github.com/hashicorp/go-memdb" + mock "github.com/stretchr/testify/mock" structs "github.com/hashicorp/consul/agent/structs" @@ -48,6 +50,36 @@ func (_m *MockStateStore) GetNodeID(_a0 types.NodeID, _a1 *acl.EnterpriseMeta, _ return r0, r1, r2 } +// NodeService provides a mock function with given fields: ws, nodeName, serviceID, entMeta, peerName +func (_m *MockStateStore) NodeService(ws memdb.WatchSet, nodeName string, serviceID string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, *structs.NodeService, error) { + ret := _m.Called(ws, nodeName, serviceID, entMeta, peerName) + + var r0 uint64 + if rf, ok := ret.Get(0).(func(memdb.WatchSet, string, string, *acl.EnterpriseMeta, string) uint64); ok { + r0 = rf(ws, nodeName, serviceID, entMeta, peerName) + } else { + r0 = ret.Get(0).(uint64) + } + + var r1 *structs.NodeService + if rf, ok := ret.Get(1).(func(memdb.WatchSet, string, string, *acl.EnterpriseMeta, string) *structs.NodeService); ok { + r1 = rf(ws, nodeName, serviceID, entMeta, peerName) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*structs.NodeService) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(memdb.WatchSet, string, string, *acl.EnterpriseMeta, string) error); ok { + r2 = rf(ws, nodeName, serviceID, entMeta, peerName) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // NewMockStateStore creates a new instance of MockStateStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewMockStateStore(t testing.TB) *MockStateStore { mock := &MockStateStore{} diff --git a/agent/consul/autopilotevents/ready_servers_events.go b/agent/consul/autopilotevents/ready_servers_events.go index ad3221e9a..6e7734fee 100644 --- a/agent/consul/autopilotevents/ready_servers_events.go +++ b/agent/consul/autopilotevents/ready_servers_events.go @@ -4,9 +4,11 @@ import ( "fmt" "net" "sort" + "strconv" "sync" "time" + "github.com/hashicorp/go-memdb" autopilot "github.com/hashicorp/raft-autopilot" "github.com/hashicorp/consul/acl" @@ -26,6 +28,7 @@ type ReadyServerInfo struct { ID string Address string TaggedAddresses map[string]string + ExtGRPCPort int Version string } @@ -122,6 +125,7 @@ func NewReadyServersEventPublisher(config Config) *ReadyServersEventPublisher { //go:generate mockery --name StateStore --inpackage --filename mock_StateStore_test.go type StateStore interface { GetNodeID(types.NodeID, *acl.EnterpriseMeta, string) (uint64, *structs.Node, error) + NodeService(ws memdb.WatchSet, nodeName string, serviceID string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, *structs.NodeService, error) } //go:generate mockery --name Publisher --inpackage --filename mock_Publisher_test.go @@ -194,25 +198,32 @@ func (r *ReadyServersEventPublisher) readyServersEvents(state *autopilot.State) return []stream.Event{r.newReadyServersEvent(servers)}, true } +// IsServerReady determines whether the given server (from the autopilot state) +// is "ready" - by which we mean that they would be an acceptable target for +// stale queries. +func IsServerReady(srv *autopilot.ServerState) bool { + // All healthy servers are caught up enough to be considered ready. + // Servers with voting rights that are still healthy according to Serf are + // also included as they have likely just fallen behind the leader a little + // after initially replicating state. They are still acceptable targets + // for most stale queries and clients can bound the staleness if necessary. + // Including them is a means to prevent flapping the list of servers we + // advertise as ready and flooding the network with notifications to all + // dataplanes of server updates. + // + // TODO (agentless) for a non-voting server that is still alive but fell + // behind, should we cause it to be removed. For voters we know they were caught + // up at some point but for non-voters we cannot know the same thing. + return srv.Health.Healthy || (srv.HasVotingRights() && srv.Server.NodeStatus == autopilot.NodeAlive) +} + // autopilotStateToReadyServers will iterate through all servers in the autopilot // state and compile a list of servers which are "ready". Readiness means that // they would be an acceptable target for stale queries. func (r *ReadyServersEventPublisher) autopilotStateToReadyServers(state *autopilot.State) EventPayloadReadyServers { var servers EventPayloadReadyServers for _, srv := range state.Servers { - // All healthy servers are caught up enough to be included in a ready servers. - // Servers with voting rights that are still healthy according to Serf are - // also included as they have likely just fallen behind the leader a little - // after initially replicating state. They are still acceptable targets - // for most stale queries and clients can bound the staleness if necessary. - // Including them is a means to prevent flapping the list of servers we - // advertise as ready and flooding the network with notifications to all - // dataplanes of server updates. - // - // TODO (agentless) for a non-voting server that is still alive but fell - // behind, should we cause it to be removed. For voters we know they were caught - // up at some point but for non-voters we cannot know the same thing. - if srv.Health.Healthy || (srv.HasVotingRights() && srv.Server.NodeStatus == autopilot.NodeAlive) { + if IsServerReady(srv) { // autopilot information contains addresses in the : form. We only care about the // the host so we parse it out here and discard the port. host, err := extractHost(string(srv.Server.Address)) @@ -226,6 +237,7 @@ func (r *ReadyServersEventPublisher) autopilotStateToReadyServers(state *autopil Address: host, Version: srv.Server.Version, TaggedAddresses: r.getTaggedAddresses(srv), + ExtGRPCPort: r.getGRPCPort(srv), }) } } @@ -254,7 +266,7 @@ func (r *ReadyServersEventPublisher) getTaggedAddresses(srv *autopilot.ServerSta // code and reason about and having those addresses be updated within 30s is good enough. _, node, err := r.GetStore().GetNodeID(types.NodeID(srv.Server.ID), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword) if err != nil || node == nil { - // no catalog information means we should return a nil addres map + // no catalog information means we should return a nil address map return nil } @@ -276,6 +288,38 @@ func (r *ReadyServersEventPublisher) getTaggedAddresses(srv *autopilot.ServerSta return addrs } +// getGRPCPort will get the external gRPC port for a Consul server. +// Returns 0 if there is none assigned or if an error is encountered. +func (r *ReadyServersEventPublisher) getGRPCPort(srv *autopilot.ServerState) int { + if r.GetStore == nil { + return 0 + } + + _, n, err := r.GetStore().GetNodeID(types.NodeID(srv.Server.ID), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword) + if err != nil || n == nil { + return 0 + } + + _, ns, err := r.GetStore().NodeService( + nil, + n.Node, + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ) + if err != nil || ns == nil || ns.Meta == nil { + return 0 + } + if str, ok := ns.Meta["grpc_port"]; ok { + grpcPort, err := strconv.Atoi(str) + if err == nil { + return grpcPort + } + } + + return 0 +} + // newReadyServersEvent will create a stream.Event with the provided ready server info. func (r *ReadyServersEventPublisher) newReadyServersEvent(servers EventPayloadReadyServers) stream.Event { now := time.Now() diff --git a/agent/consul/autopilotevents/ready_servers_events_test.go b/agent/consul/autopilotevents/ready_servers_events_test.go index 223292404..0f686fbc5 100644 --- a/agent/consul/autopilotevents/ready_servers_events_test.go +++ b/agent/consul/autopilotevents/ready_servers_events_test.go @@ -4,6 +4,7 @@ import ( "testing" time "time" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/raft" autopilot "github.com/hashicorp/raft-autopilot" mock "github.com/stretchr/testify/mock" @@ -164,9 +165,21 @@ func TestAutopilotStateToReadyServersWithTaggedAddresses(t *testing.T) { types.NodeID("792ae13c-d765-470b-852c-e073fdb6e849"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-1", TaggedAddresses: map[string]string{"wan": "5.4.3.2"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-1", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "5.4.3.2"}}, + nil, nil, ) @@ -174,9 +187,21 @@ func TestAutopilotStateToReadyServersWithTaggedAddresses(t *testing.T) { types.NodeID("65e79ff4-bbce-467b-a9d6-725c709fa985"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-2", TaggedAddresses: map[string]string{"wan": "1.2.3.4"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-2", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "1.2.3.4"}}, + nil, nil, ) @@ -184,9 +209,119 @@ func TestAutopilotStateToReadyServersWithTaggedAddresses(t *testing.T) { types.NodeID("db11f0ac-0cbe-4215-80cc-b4e843f4df1e"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-3", TaggedAddresses: map[string]string{"wan": "9.8.7.6"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-3", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "9.8.7.6"}}, + nil, + nil, + ) + + r := NewReadyServersEventPublisher(Config{ + GetStore: func() StateStore { return store }, + }) + + actual := r.autopilotStateToReadyServers(exampleState) + require.ElementsMatch(t, expected, actual) +} + +func TestAutopilotStateToReadyServersWithExtGRPCPort(t *testing.T) { + expected := EventPayloadReadyServers{ + { + ID: "792ae13c-d765-470b-852c-e073fdb6e849", + Address: "198.18.0.2", + ExtGRPCPort: 1234, + Version: "v1.12.0", + }, + { + ID: "65e79ff4-bbce-467b-a9d6-725c709fa985", + Address: "198.18.0.3", + ExtGRPCPort: 2345, + Version: "v1.12.0", + }, + { + ID: "db11f0ac-0cbe-4215-80cc-b4e843f4df1e", + Address: "198.18.0.4", + ExtGRPCPort: 3456, + Version: "v1.12.0", + }, + } + + store := &MockStateStore{} + t.Cleanup(func() { store.AssertExpectations(t) }) + store.On("GetNodeID", + types.NodeID("792ae13c-d765-470b-852c-e073fdb6e849"), + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-1"}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-1", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Once().Return( + uint64(0), + &structs.NodeService{Meta: map[string]string{"grpc_port": "1234"}}, + nil, + ) + + store.On("GetNodeID", + types.NodeID("65e79ff4-bbce-467b-a9d6-725c709fa985"), + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-2"}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-2", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Once().Return( + uint64(0), + &structs.NodeService{Meta: map[string]string{"grpc_port": "2345"}}, + nil, + ) + + store.On("GetNodeID", + types.NodeID("db11f0ac-0cbe-4215-80cc-b4e843f4df1e"), + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-3"}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-3", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, + ).Once().Return( + uint64(0), + &structs.NodeService{Meta: map[string]string{"grpc_port": "3456"}}, nil, ) @@ -493,9 +628,21 @@ func TestReadyServerEventsSnapshotHandler(t *testing.T) { types.NodeID("792ae13c-d765-470b-852c-e073fdb6e849"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-1", TaggedAddresses: map[string]string{"wan": "5.4.3.2"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-1", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "5.4.3.2"}}, + nil, nil, ) @@ -503,9 +650,21 @@ func TestReadyServerEventsSnapshotHandler(t *testing.T) { types.NodeID("65e79ff4-bbce-467b-a9d6-725c709fa985"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-2", TaggedAddresses: map[string]string{"wan": "1.2.3.4"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-2", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "1.2.3.4"}}, + nil, nil, ) @@ -513,9 +672,21 @@ func TestReadyServerEventsSnapshotHandler(t *testing.T) { types.NodeID("db11f0ac-0cbe-4215-80cc-b4e843f4df1e"), structs.NodeEnterpriseMetaInDefaultPartition(), structs.DefaultPeerKeyword, + ).Times(2).Return( + uint64(0), + &structs.Node{Node: "node-3", TaggedAddresses: map[string]string{"wan": "9.8.7.6"}}, + nil, + ) + + store.On("NodeService", + memdb.WatchSet(nil), + "node-3", + structs.ConsulServiceID, + structs.NodeEnterpriseMetaInDefaultPartition(), + structs.DefaultPeerKeyword, ).Once().Return( uint64(0), - &structs.Node{TaggedAddresses: map[string]string{"wan": "9.8.7.6"}}, + nil, nil, ) diff --git a/agent/consul/catalog_endpoint.go b/agent/consul/catalog_endpoint.go index 111ee7b2b..696ae314a 100644 --- a/agent/consul/catalog_endpoint.go +++ b/agent/consul/catalog_endpoint.go @@ -565,6 +565,11 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I return err } + filter, err := bexpr.CreateFilter(args.Filter, nil, []*structs.ServiceNode{}) + if err != nil { + return err + } + // Set reply enterprise metadata after resolving and validating the token so // that we can properly infer metadata from the token. reply.EnterpriseMeta = args.EnterpriseMeta @@ -574,10 +579,11 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { var err error + var serviceNodes structs.ServiceNodes if len(args.NodeMetaFilters) > 0 { - reply.Index, reply.Services, err = state.ServicesByNodeMeta(ws, args.NodeMetaFilters, &args.EnterpriseMeta, args.PeerName) + reply.Index, serviceNodes, err = state.ServicesByNodeMeta(ws, args.NodeMetaFilters, &args.EnterpriseMeta, args.PeerName) } else { - reply.Index, reply.Services, err = state.Services(ws, &args.EnterpriseMeta, args.PeerName) + reply.Index, serviceNodes, err = state.Services(ws, &args.EnterpriseMeta, args.PeerName) } if err != nil { return err @@ -588,11 +594,43 @@ func (c *Catalog) ListServices(args *structs.DCSpecificRequest, reply *structs.I return nil } + raw, err := filter.Execute(serviceNodes) + if err != nil { + return err + } + + reply.Services = servicesTagsByName(raw.(structs.ServiceNodes)) + c.srv.filterACLWithAuthorizer(authz, reply) + return nil }) } +func servicesTagsByName(services []*structs.ServiceNode) structs.Services { + unique := make(map[string]map[string]struct{}) + for _, svc := range services { + tags, ok := unique[svc.ServiceName] + if !ok { + unique[svc.ServiceName] = make(map[string]struct{}) + tags = unique[svc.ServiceName] + } + for _, tag := range svc.ServiceTags { + tags[tag] = struct{}{} + } + } + + // Generate the output structure. + var results = make(structs.Services) + for service, tags := range unique { + results[service] = make([]string, 0, len(tags)) + for tag := range tags { + results[service] = append(results[service], tag) + } + } + return results +} + // ServiceList is used to query the services in a DC. // Returns services as a list of ServiceNames. func (c *Catalog) ServiceList(args *structs.DCSpecificRequest, reply *structs.IndexedServiceList) error { diff --git a/agent/consul/catalog_endpoint_test.go b/agent/consul/catalog_endpoint_test.go index ca00efaea..daa22c90c 100644 --- a/agent/consul/catalog_endpoint_test.go +++ b/agent/consul/catalog_endpoint_test.go @@ -1523,6 +1523,45 @@ func TestCatalog_ListServices_NodeMetaFilter(t *testing.T) { } } +func TestCatalog_ListServices_Filter(t *testing.T) { + t.Parallel() + _, s1 := testServer(t) + codec := rpcClient(t, s1) + + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + // prep the cluster with some data we can use in our filters + registerTestCatalogEntries(t, codec) + + // Run the tests against the test server + + t.Run("ListServices", func(t *testing.T) { + args := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + + args.Filter = "ServiceName == redis" + out := new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Contains(t, out.Services, "redis") + require.ElementsMatch(t, []string{"v1", "v2"}, out.Services["redis"]) + + args.Filter = "NodeMeta.os == NoSuchOS" + out = new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Len(t, out.Services, 0) + + args.Filter = "NodeMeta.NoSuchMetadata == linux" + out = new(structs.IndexedServices) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + require.Len(t, out.Services, 0) + + args.Filter = "InvalidField == linux" + out = new(structs.IndexedServices) + require.Error(t, msgpackrpc.CallWithCodec(codec, "Catalog.ListServices", &args, out)) + }) +} + func TestCatalog_ListServices_Blocking(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/agent/consul/client_serf.go b/agent/consul/client_serf.go index 55df7a547..7d993c4ac 100644 --- a/agent/consul/client_serf.go +++ b/agent/consul/client_serf.go @@ -62,6 +62,8 @@ func (c *Client) setupSerf(conf *serf.Config, ch chan serf.Event, path string) ( return nil, err } + addSerfMetricsLabels(conf, false, c.config.Segment, c.config.AgentEnterpriseMeta().PartitionOrDefault(), "") + addEnterpriseSerfTags(conf.Tags, c.config.AgentEnterpriseMeta()) conf.ReconnectTimeoutOverride = libserf.NewReconnectOverride(c.logger) diff --git a/agent/consul/client_test.go b/agent/consul/client_test.go index 84135ee18..ff150d545 100644 --- a/agent/consul/client_test.go +++ b/agent/consul/client_test.go @@ -18,6 +18,7 @@ import ( msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc" "github.com/hashicorp/consul/agent/consul/stream" + "github.com/hashicorp/consul/agent/grpc-external/limiter" grpc "github.com/hashicorp/consul/agent/grpc-internal" "github.com/hashicorp/consul/agent/grpc-internal/resolver" "github.com/hashicorp/consul/agent/pool" @@ -553,6 +554,7 @@ func newDefaultDeps(t *testing.T, c *Config) Deps { NewRequestRecorderFunc: middleware.NewRequestRecorder, GetNetRPCInterceptorFunc: middleware.GetNetRPCInterceptor, EnterpriseDeps: newDefaultDepsEnterprise(t, logger, c), + XDSStreamLimiter: limiter.NewSessionLimiter(), } } @@ -893,8 +895,8 @@ func TestClient_RPC_Timeout(t *testing.T) { } }) - // waiter will sleep for 50ms - require.NoError(t, s1.RegisterEndpoint("Wait", &waiter{duration: 50 * time.Millisecond})) + // waiter will sleep for 101ms which is 1ms more than the DefaultQueryTime + require.NoError(t, s1.RegisterEndpoint("Wait", &waiter{duration: 101 * time.Millisecond})) // Requests with QueryOptions have a default timeout of RPCHoldTimeout (10ms) // so we expect the RPC call to timeout. @@ -903,7 +905,8 @@ func TestClient_RPC_Timeout(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "rpc error making call: i/o deadline reached") - // Blocking requests have a longer timeout (100ms) so this should pass + // Blocking requests have a longer timeout (100ms) so this should pass since we + // add the maximum jitter which should be 16ms out = struct{}{} err = c1.RPC("Wait.Wait", &structs.NodeSpecificRequest{ QueryOptions: structs.QueryOptions{ diff --git a/agent/consul/config.go b/agent/consul/config.go index b897c4f23..69d4fddee 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -133,6 +133,9 @@ type Config struct { // GRPCPort is the port the public gRPC server listens on. GRPCPort int + // GRPCTLSPort is the port the public gRPC TLS server listens on. + GRPCTLSPort int + // (Enterprise-only) The network segment this agent is part of. Segment string @@ -584,6 +587,7 @@ func CloneSerfLANConfig(base *serf.Config) *serf.Config { cfg.MemberlistConfig.ProbeTimeout = base.MemberlistConfig.ProbeTimeout cfg.MemberlistConfig.SuspicionMult = base.MemberlistConfig.SuspicionMult cfg.MemberlistConfig.RetransmitMult = base.MemberlistConfig.RetransmitMult + cfg.MemberlistConfig.MetricLabels = base.MemberlistConfig.MetricLabels // agent/keyring.go cfg.MemberlistConfig.Keyring = base.MemberlistConfig.Keyring @@ -593,6 +597,7 @@ func CloneSerfLANConfig(base *serf.Config) *serf.Config { cfg.ReapInterval = base.ReapInterval cfg.TombstoneTimeout = base.TombstoneTimeout cfg.MemberlistConfig.SecretKey = base.MemberlistConfig.SecretKey + cfg.MetricLabels = base.MetricLabels return cfg } diff --git a/agent/consul/config_endpoint.go b/agent/consul/config_endpoint.go index 0926fe9f5..ddf19916a 100644 --- a/agent/consul/config_endpoint.go +++ b/agent/consul/config_endpoint.go @@ -12,6 +12,7 @@ import ( hashstructure_v2 "github.com/mitchellh/hashstructure/v2" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" ) @@ -510,7 +511,7 @@ func (c *ConfigEntry) ResolveServiceConfig(args *structs.ServiceConfigRequest, r ranOnce = true } - thisReply, err := computeResolvedServiceConfig( + thisReply, err := configentry.ComputeResolvedServiceConfig( args, upstreamIDs, legacyUpstreams, diff --git a/agent/consul/config_endpoint_test.go b/agent/consul/config_endpoint_test.go index 3f79b3d1b..aaf7cba94 100644 --- a/agent/consul/config_endpoint_test.go +++ b/agent/consul/config_endpoint_test.go @@ -1399,8 +1399,9 @@ func TestConfigEntry_ResolveServiceConfig_Upstreams(t *testing.T) { Protocol: "http", MeshGateway: structs.MeshGatewayConfig{Mode: structs.MeshGatewayModeRemote}, PassiveHealthCheck: &structs.PassiveHealthCheck{ - Interval: 10, - MaxFailures: 2, + Interval: 10, + MaxFailures: 2, + EnforcingConsecutive5xx: uintPointer(60), }, }, Overrides: []*structs.UpstreamConfig{ @@ -1432,8 +1433,9 @@ func TestConfigEntry_ResolveServiceConfig_Upstreams(t *testing.T) { Upstream: wildcard, Config: map[string]interface{}{ "passive_health_check": map[string]interface{}{ - "Interval": int64(10), - "MaxFailures": int64(2), + "Interval": int64(10), + "MaxFailures": int64(2), + "EnforcingConsecutive5xx": int64(60), }, "mesh_gateway": map[string]interface{}{ "Mode": "remote", @@ -1445,8 +1447,9 @@ func TestConfigEntry_ResolveServiceConfig_Upstreams(t *testing.T) { Upstream: mysql, Config: map[string]interface{}{ "passive_health_check": map[string]interface{}{ - "Interval": int64(10), - "MaxFailures": int64(2), + "Interval": int64(10), + "MaxFailures": int64(2), + "EnforcingConsecutive5xx": int64(60), }, "mesh_gateway": map[string]interface{}{ "Mode": "local", @@ -2507,3 +2510,7 @@ func Test_gateWriteToSecondary_AllowedKinds(t *testing.T) { }) } } + +func uintPointer(v uint32) *uint32 { + return &v +} diff --git a/agent/consul/discovery_chain_endpoint_test.go b/agent/consul/discovery_chain_endpoint_test.go index 21c34aa86..c1ad0fef3 100644 --- a/agent/consul/discovery_chain_endpoint_test.go +++ b/agent/consul/discovery_chain_endpoint_test.go @@ -56,8 +56,17 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { return &resp, nil } - newTarget := func(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) + newTarget := func(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, connect.TestClusterID+".consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default @@ -119,7 +128,7 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, }, } @@ -245,7 +254,7 @@ func TestDiscoveryChainEndpoint_Get(t *testing.T) { }, Targets: map[string]*structs.DiscoveryTarget{ "web.default.default.dc1": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 33*time.Second, ), }, diff --git a/agent/consul/discoverychain/compile.go b/agent/consul/discoverychain/compile.go index ed664878b..3a9a1f0ed 100644 --- a/agent/consul/discoverychain/compile.go +++ b/agent/consul/discoverychain/compile.go @@ -8,6 +8,7 @@ import ( "github.com/mitchellh/hashstructure" "github.com/mitchellh/mapstructure" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" @@ -576,7 +577,10 @@ func (c *compiler) assembleChain() error { if router == nil { // If no router is configured, move on down the line to the next hop of // the chain. - node, err := c.getSplitterOrResolverNode(c.newTarget(c.serviceName, "", "", "", "")) + node, err := c.getSplitterOrResolverNode(c.newTarget(structs.DiscoveryTargetOpts{ + Service: c.serviceName, + })) + if err != nil { return err } @@ -626,11 +630,20 @@ func (c *compiler) assembleChain() error { ) if dest.ServiceSubset == "" { node, err = c.getSplitterOrResolverNode( - c.newTarget(svc, "", destNamespace, destPartition, ""), - ) + c.newTarget(structs.DiscoveryTargetOpts{ + Service: svc, + Namespace: destNamespace, + Partition: destPartition, + }, + )) } else { node, err = c.getResolverNode( - c.newTarget(svc, dest.ServiceSubset, destNamespace, destPartition, ""), + c.newTarget(structs.DiscoveryTargetOpts{ + Service: svc, + ServiceSubset: dest.ServiceSubset, + Namespace: destNamespace, + Partition: destPartition, + }), false, ) } @@ -642,7 +655,12 @@ func (c *compiler) assembleChain() error { // If we have a router, we'll add a catch-all route at the end to send // unmatched traffic to the next hop in the chain. - defaultDestinationNode, err := c.getSplitterOrResolverNode(c.newTarget(router.Name, "", router.NamespaceOrDefault(), router.PartitionOrDefault(), "")) + opts := structs.DiscoveryTargetOpts{ + Service: router.Name, + Namespace: router.NamespaceOrDefault(), + Partition: router.PartitionOrDefault(), + } + defaultDestinationNode, err := c.getSplitterOrResolverNode(c.newTarget(opts)) if err != nil { return err } @@ -674,26 +692,36 @@ func newDefaultServiceRoute(serviceName, namespace, partition string) *structs.S } } -func (c *compiler) newTarget(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - if service == "" { +func (c *compiler) newTarget(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Service == "" { panic("newTarget called with empty service which makes no sense") } - t := structs.NewDiscoveryTarget( - service, - serviceSubset, - defaultIfEmpty(namespace, c.evaluateInNamespace), - defaultIfEmpty(partition, c.evaluateInPartition), - defaultIfEmpty(datacenter, c.evaluateInDatacenter), - ) + if opts.Peer == "" { + opts.Datacenter = defaultIfEmpty(opts.Datacenter, c.evaluateInDatacenter) + opts.Namespace = defaultIfEmpty(opts.Namespace, c.evaluateInNamespace) + opts.Partition = defaultIfEmpty(opts.Partition, c.evaluateInPartition) + } else { + // Don't allow Peer and Datacenter. + opts.Datacenter = "" + // Peer and Partition cannot both be set. + opts.Partition = acl.PartitionOrDefault("") + // Default to "default" rather than c.evaluateInNamespace. + opts.Namespace = acl.PartitionOrDefault(opts.Namespace) + } - // Set default connect SNI. This will be overridden later if the service - // has an explicit SNI value configured in service-defaults. - t.SNI = connect.TargetSNI(t, c.evaluateInTrustDomain) + t := structs.NewDiscoveryTarget(opts) - // Use the same representation for the name. This will NOT be overridden - // later. - t.Name = t.SNI + // We don't have the peer's trust domain yet so we can't construct the SNI. + if opts.Peer == "" { + // Set default connect SNI. This will be overridden later if the service + // has an explicit SNI value configured in service-defaults. + t.SNI = connect.TargetSNI(t, c.evaluateInTrustDomain) + + // Use the same representation for the name. This will NOT be overridden + // later. + t.Name = t.SNI + } prev, ok := c.loadedTargets[t.ID] if ok { @@ -703,34 +731,30 @@ func (c *compiler) newTarget(service, serviceSubset, namespace, partition, datac return t } -func (c *compiler) rewriteTarget(t *structs.DiscoveryTarget, service, serviceSubset, partition, namespace, datacenter string) *structs.DiscoveryTarget { - var ( - service2 = t.Service - serviceSubset2 = t.ServiceSubset - partition2 = t.Partition - namespace2 = t.Namespace - datacenter2 = t.Datacenter - ) +func (c *compiler) rewriteTarget(t *structs.DiscoveryTarget, opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + mergedOpts := t.ToDiscoveryTargetOpts() - if service != "" && service != service2 { - service2 = service + if opts.Service != "" && opts.Service != mergedOpts.Service { + mergedOpts.Service = opts.Service // Reset the chosen subset if we reference a service other than our own. - serviceSubset2 = "" + mergedOpts.ServiceSubset = "" } - if serviceSubset != "" { - serviceSubset2 = serviceSubset + if opts.ServiceSubset != "" { + mergedOpts.ServiceSubset = opts.ServiceSubset } - if partition != "" { - partition2 = partition + if opts.Partition != "" { + mergedOpts.Partition = opts.Partition } - if namespace != "" { - namespace2 = namespace + // Only use explicit Namespace with Peer + if opts.Namespace != "" || opts.Peer != "" { + mergedOpts.Namespace = opts.Namespace } - if datacenter != "" { - datacenter2 = datacenter + if opts.Datacenter != "" { + mergedOpts.Datacenter = opts.Datacenter } + mergedOpts.Peer = opts.Peer - return c.newTarget(service2, serviceSubset2, namespace2, partition2, datacenter2) + return c.newTarget(mergedOpts) } func (c *compiler) getSplitterOrResolverNode(target *structs.DiscoveryTarget) (*structs.DiscoveryGraphNode, error) { @@ -803,10 +827,13 @@ func (c *compiler) getSplitterNode(sid structs.ServiceID) (*structs.DiscoveryGra // fall through to group-resolver } - node, err := c.getResolverNode( - c.newTarget(splitID.ID, split.ServiceSubset, splitID.NamespaceOrDefault(), splitID.PartitionOrDefault(), ""), - false, - ) + opts := structs.DiscoveryTargetOpts{ + Service: splitID.ID, + ServiceSubset: split.ServiceSubset, + Namespace: splitID.NamespaceOrDefault(), + Partition: splitID.PartitionOrDefault(), + } + node, err := c.getResolverNode(c.newTarget(opts), false) if err != nil { return nil, err } @@ -881,11 +908,7 @@ RESOLVE_AGAIN: redirectedTarget := c.rewriteTarget( target, - redirect.Service, - redirect.ServiceSubset, - redirect.Partition, - redirect.Namespace, - redirect.Datacenter, + redirect.ToDiscoveryTargetOpts(), ) if redirectedTarget.ID != target.ID { target = redirectedTarget @@ -895,14 +918,9 @@ RESOLVE_AGAIN: // Handle default subset. if target.ServiceSubset == "" && resolver.DefaultSubset != "" { - target = c.rewriteTarget( - target, - "", - resolver.DefaultSubset, - "", - "", - "", - ) + target = c.rewriteTarget(target, structs.DiscoveryTargetOpts{ + ServiceSubset: resolver.DefaultSubset, + }) goto RESOLVE_AGAIN } @@ -1027,56 +1045,54 @@ RESOLVE_AGAIN: failover, ok = f["*"] } - if ok { - // Determine which failover definitions apply. - var failoverTargets []*structs.DiscoveryTarget - if len(failover.Datacenters) > 0 { - for _, dc := range failover.Datacenters { - // Rewrite the target as per the failover policy. - failoverTarget := c.rewriteTarget( - target, - failover.Service, - failover.ServiceSubset, - target.Partition, - failover.Namespace, - dc, - ) - if failoverTarget.ID != target.ID { // don't failover to yourself - failoverTargets = append(failoverTargets, failoverTarget) - } - } - } else { + if !ok { + return node, nil + } + + // Determine which failover definitions apply. + var failoverTargets []*structs.DiscoveryTarget + if len(failover.Datacenters) > 0 { + opts := failover.ToDiscoveryTargetOpts() + for _, dc := range failover.Datacenters { // Rewrite the target as per the failover policy. - failoverTarget := c.rewriteTarget( - target, - failover.Service, - failover.ServiceSubset, - target.Partition, - failover.Namespace, - "", - ) + opts.Datacenter = dc + failoverTarget := c.rewriteTarget(target, opts) if failoverTarget.ID != target.ID { // don't failover to yourself failoverTargets = append(failoverTargets, failoverTarget) } } - - // If we filtered everything out then no point in having a failover. - if len(failoverTargets) > 0 { - df := &structs.DiscoveryFailover{} - node.Resolver.Failover = df - - // Take care of doing any redirects or configuration loading - // related to targets by cheating a bit and recursing into - // ourselves. - for _, target := range failoverTargets { - failoverResolveNode, err := c.getResolverNode(target, true) - if err != nil { - return nil, err - } - failoverTarget := failoverResolveNode.Resolver.Target - df.Targets = append(df.Targets, failoverTarget) + } else if len(failover.Targets) > 0 { + for _, t := range failover.Targets { + // Rewrite the target as per the failover policy. + failoverTarget := c.rewriteTarget(target, t.ToDiscoveryTargetOpts()) + if failoverTarget.ID != target.ID { // don't failover to yourself + failoverTargets = append(failoverTargets, failoverTarget) } } + } else { + // Rewrite the target as per the failover policy. + failoverTarget := c.rewriteTarget(target, failover.ToDiscoveryTargetOpts()) + if failoverTarget.ID != target.ID { // don't failover to yourself + failoverTargets = append(failoverTargets, failoverTarget) + } + } + + // If we filtered everything out then no point in having a failover. + if len(failoverTargets) > 0 { + df := &structs.DiscoveryFailover{} + node.Resolver.Failover = df + + // Take care of doing any redirects or configuration loading + // related to targets by cheating a bit and recursing into + // ourselves. + for _, target := range failoverTargets { + failoverResolveNode, err := c.getResolverNode(target, true) + if err != nil { + return nil, err + } + failoverTarget := failoverResolveNode.Resolver.Target + df.Targets = append(df.Targets, failoverTarget) + } } } diff --git a/agent/consul/discoverychain/compile_test.go b/agent/consul/discoverychain/compile_test.go index 221ac757f..a4c9c65ed 100644 --- a/agent/consul/discoverychain/compile_test.go +++ b/agent/consul/discoverychain/compile_test.go @@ -39,6 +39,7 @@ func TestCompile(t *testing.T) { "service redirect": testcase_ServiceRedirect(), "service and subset redirect": testcase_ServiceAndSubsetRedirect(), "datacenter redirect": testcase_DatacenterRedirect(), + "redirect to cluster peer": testcase_PeerRedirect(), "datacenter redirect with mesh gateways": testcase_DatacenterRedirect_WithMeshGateways(), "service failover": testcase_ServiceFailover(), "service failover through redirect": testcase_ServiceFailoverThroughRedirect(), @@ -46,6 +47,7 @@ func TestCompile(t *testing.T) { "service and subset failover": testcase_ServiceAndSubsetFailover(), "datacenter failover": testcase_DatacenterFailover(), "datacenter failover with mesh gateways": testcase_DatacenterFailover_WithMeshGateways(), + "target failover": testcase_Failover_Targets(), "noop split to resolver with default subset": testcase_NoopSplit_WithDefaultSubset(), "resolver with default subset": testcase_Resolve_WithDefaultSubset(), "default resolver with external sni": testcase_DefaultResolver_ExternalSNI(), @@ -182,7 +184,7 @@ func testcase_JustRouterWithDefaults() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -244,7 +246,7 @@ func testcase_JustRouterWithNoDestination() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -294,7 +296,7 @@ func testcase_RouterWithDefaults_NoSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -361,7 +363,7 @@ func testcase_RouterWithDefaults_WithNoopSplit_DefaultResolver() compileTestCase }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -426,7 +428,10 @@ func testcase_NoopSplit_DefaultResolver_ProtocolFromProxyDefaults() compileTestC }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc1", + }, nil), }, } @@ -498,7 +503,7 @@ func testcase_RouterWithDefaults_WithNoopSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -584,8 +589,11 @@ func testcase_RouteBypassesSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "bypass.other.default.default.dc1": newTarget("other", "bypass", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "bypass.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "other", + ServiceSubset: "bypass", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } @@ -638,7 +646,7 @@ func testcase_NoopSplit_DefaultResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -694,7 +702,7 @@ func testcase_NoopSplit_WithResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc1": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc1", nil), + newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), 33*time.Second, ), }, @@ -776,12 +784,19 @@ func testcase_SubsetSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), - "v1.main.default.default.dc1": newTarget("main", "v1", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v1", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } @@ -855,8 +870,8 @@ func testcase_ServiceSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "foo.default.default.dc1": newTarget("foo", "", "default", "default", "dc1", nil), - "bar.default.default.dc1": newTarget("bar", "", "default", "default", "dc1", nil), + "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), + "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil), }, } @@ -935,7 +950,10 @@ func testcase_SplitBypassesSplit() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "bypassed.next.default.default.dc1": newTarget("next", "bypassed", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "bypassed.next.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "next", + ServiceSubset: "bypassed", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == bypass", } @@ -973,7 +991,7 @@ func testcase_ServiceRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1019,7 +1037,10 @@ func testcase_ServiceAndSubsetRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.other.default.default.dc1": newTarget("other", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "other", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1055,7 +1076,51 @@ func testcase_DatacenterRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc9": newTarget("main", "", "default", "default", "dc9", nil), + "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc9", + }, nil), + }, + } + return compileTestCase{entries: entries, expect: expect} +} + +func testcase_PeerRedirect() compileTestCase { + entries := newEntries() + entries.AddResolvers( + &structs.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + Redirect: &structs.ServiceResolverRedirect{ + Service: "other", + Peer: "cluster-01", + }, + }, + ) + + expect := &structs.CompiledDiscoveryChain{ + Protocol: "tcp", + StartNode: "resolver:other.default.default.external.cluster-01", + Nodes: map[string]*structs.DiscoveryGraphNode{ + "resolver:other.default.default.external.cluster-01": { + Type: structs.DiscoveryGraphNodeTypeResolver, + Name: "other.default.default.external.cluster-01", + Resolver: &structs.DiscoveryResolver{ + Default: true, + ConnectTimeout: 5 * time.Second, + Target: "other.default.default.external.cluster-01", + }, + }, + }, + Targets: map[string]*structs.DiscoveryTarget{ + "other.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ + Service: "other", + Peer: "cluster-01", + }, func(t *structs.DiscoveryTarget) { + t.SNI = "" + t.Name = "" + t.Datacenter = "" + }), }, } return compileTestCase{entries: entries, expect: expect} @@ -1095,7 +1160,10 @@ func testcase_DatacenterRedirect_WithMeshGateways() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc9": newTarget("main", "", "default", "default", "dc9", func(t *structs.DiscoveryTarget) { + "main.default.default.dc9": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc9", + }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1134,8 +1202,8 @@ func testcase_ServiceFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.default.default.dc1": newTarget("backup", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1177,8 +1245,8 @@ func testcase_ServiceFailoverThroughRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "actual.default.default.dc1": newTarget("actual", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "actual.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "actual"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1220,8 +1288,8 @@ func testcase_Resolver_CircularFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.default.default.dc1": newTarget("backup", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "backup"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1261,8 +1329,11 @@ func testcase_ServiceAndSubsetFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "backup.main.default.default.dc1": newTarget("main", "backup", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "backup.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "backup", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == backup", } @@ -1301,9 +1372,15 @@ func testcase_DatacenterFailover() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), - "main.default.default.dc2": newTarget("main", "", "default", "default", "dc2", nil), - "main.default.default.dc4": newTarget("main", "", "default", "default", "dc4", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), + "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, nil), + "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc4", + }, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1350,17 +1427,105 @@ func testcase_DatacenterFailover_WithMeshGateways() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), - "main.default.default.dc2": newTarget("main", "", "default", "default", "dc2", func(t *structs.DiscoveryTarget) { + "main.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } }), - "main.default.default.dc4": newTarget("main", "", "default", "default", "dc4", func(t *structs.DiscoveryTarget) { + "main.default.default.dc4": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc4", + }, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + }, + } + return compileTestCase{entries: entries, expect: expect} +} + +func testcase_Failover_Targets() compileTestCase { + entries := newEntries() + + entries.AddProxyDefaults(&structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + MeshGateway: structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + }, + }) + + entries.AddResolvers( + &structs.ServiceResolverConfigEntry{ + Kind: "service-resolver", + Name: "main", + Failover: map[string]structs.ServiceResolverFailover{ + "*": { + Targets: []structs.ServiceResolverFailoverTarget{ + {Datacenter: "dc3"}, + {Service: "new-main"}, + {Peer: "cluster-01"}, + }, + }, + }, + }, + ) + + expect := &structs.CompiledDiscoveryChain{ + Protocol: "tcp", + StartNode: "resolver:main.default.default.dc1", + Nodes: map[string]*structs.DiscoveryGraphNode{ + "resolver:main.default.default.dc1": { + Type: structs.DiscoveryGraphNodeTypeResolver, + Name: "main.default.default.dc1", + Resolver: &structs.DiscoveryResolver{ + ConnectTimeout: 5 * time.Second, + Target: "main.default.default.dc1", + Failover: &structs.DiscoveryFailover{ + Targets: []string{ + "main.default.default.dc3", + "new-main.default.default.dc1", + "main.default.default.external.cluster-01", + }, + }, + }, + }, + }, + Targets: map[string]*structs.DiscoveryTarget{ + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "main.default.default.dc3": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc3", + }, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "new-main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "new-main"}, func(t *structs.DiscoveryTarget) { + t.MeshGateway = structs.MeshGatewayConfig{ + Mode: structs.MeshGatewayModeRemote, + } + }), + "main.default.default.external.cluster-01": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Peer: "cluster-01", + }, func(t *structs.DiscoveryTarget) { + t.SNI = "" + t.Name = "" + t.Datacenter = "" t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1422,7 +1587,10 @@ func testcase_NoopSplit_WithDefaultSubset() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1452,7 +1620,7 @@ func testcase_DefaultResolver() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect} @@ -1488,7 +1656,7 @@ func testcase_DefaultResolver_WithProxyDefaults() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.MeshGateway = structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, } @@ -1530,7 +1698,7 @@ func testcase_ServiceMetaProjection() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -1588,7 +1756,7 @@ func testcase_ServiceMetaProjectionWithRedirect() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1623,7 +1791,7 @@ func testcase_RedirectToDefaultResolverIsNotDefaultChain() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "other.default.default.dc1": newTarget("other", "", "default", "default", "dc1", nil), + "other.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "other"}, nil), }, } @@ -1658,7 +1826,10 @@ func testcase_Resolve_WithDefaultSubset() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } @@ -1692,7 +1863,7 @@ func testcase_DefaultResolver_ExternalSNI() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, func(t *structs.DiscoveryTarget) { t.SNI = "main.some.other.service.mesh" t.External = true }), @@ -1857,11 +2028,17 @@ func testcase_MultiDatacenterCanary() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ "main.default.default.dc2": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc2", nil), + newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc2", + }, nil), 33*time.Second, ), "main.default.default.dc3": targetWithConnectTimeout( - newTarget("main", "", "default", "default", "dc3", nil), + newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + Datacenter: "dc3", + }, nil), 33*time.Second, ), }, @@ -2155,27 +2332,42 @@ func testcase_AllBellsAndWhistles() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "prod.redirected.default.default.dc1": newTarget("redirected", "prod", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "prod.redirected.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "redirected", + ServiceSubset: "prod", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "ServiceMeta.env == prod", } }), - "v1.main.default.default.dc1": newTarget("main", "v1", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v1.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v1", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 1", } }), - "v2.main.default.default.dc1": newTarget("main", "v2", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v2.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v2", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 2", } }), - "v3.main.default.default.dc1": newTarget("main", "v3", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "v3.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "v3", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{ Filter: "Service.Meta.version == 3", } }), - "default-subset.main.default.default.dc1": newTarget("main", "default-subset", "default", "default", "dc1", func(t *structs.DiscoveryTarget) { + "default-subset.main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{ + Service: "main", + ServiceSubset: "default-subset", + }, func(t *structs.DiscoveryTarget) { t.Subset = structs.ServiceResolverSubset{OnlyPassing: true} }), }, @@ -2379,7 +2571,7 @@ func testcase_ResolverProtocolOverride() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2413,7 +2605,7 @@ func testcase_ResolverProtocolOverrideIgnored() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2451,7 +2643,7 @@ func testcase_RouterIgnored_ResolverProtocolOverride() compileTestCase { }, Targets: map[string]*structs.DiscoveryTarget{ // TODO-TARGET - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } return compileTestCase{entries: entries, expect: expect, @@ -2685,9 +2877,9 @@ func testcase_LBSplitterAndResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "foo.default.default.dc1": newTarget("foo", "", "default", "default", "dc1", nil), - "bar.default.default.dc1": newTarget("bar", "", "default", "default", "dc1", nil), - "baz.default.default.dc1": newTarget("baz", "", "default", "default", "dc1", nil), + "foo.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "foo"}, nil), + "bar.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "bar"}, nil), + "baz.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "baz"}, nil), }, } @@ -2743,7 +2935,7 @@ func testcase_LBResolver() compileTestCase { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "main.default.default.dc1": newTarget("main", "", "default", "default", "dc1", nil), + "main.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "main"}, nil), }, } @@ -2791,8 +2983,17 @@ func newEntries() *configentry.DiscoveryChainSet { } } -func newTarget(service, serviceSubset, namespace, partition, datacenter string, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) +func newTarget(opts structs.DiscoveryTargetOpts, modFn func(t *structs.DiscoveryTarget)) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, "trustdomain.consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index a5e99f366..3c938f22d 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -21,7 +21,7 @@ import ( var IntentionSummaries = []prometheus.SummaryDefinition{ { Name: []string{"consul", "intention", "apply"}, - Help: "", + Help: "Deprecated - please use intention_apply", }, { Name: []string{"intention", "apply"}, diff --git a/agent/consul/internal_endpoint.go b/agent/consul/internal_endpoint.go index 28d7f365e..534513c8a 100644 --- a/agent/consul/internal_endpoint.go +++ b/agent/consul/internal_endpoint.go @@ -153,64 +153,87 @@ func (m *Internal) ServiceDump(args *structs.ServiceDumpRequest, reply *structs. &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { - // we don't support calling this endpoint for a specific peer - if args.PeerName != "" { - return fmt.Errorf("this endpoint does not support specifying a peer: %q", args.PeerName) - } - // this maxIndex will be the max of the ServiceDump calls and the PeeringList call var maxIndex uint64 - // get a local dump for services - index, nodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, structs.DefaultPeerKeyword) - if err != nil { - return fmt.Errorf("could not get a service dump for local nodes: %w", err) - } - - if index > maxIndex { - maxIndex = index - } - reply.Nodes = nodes - - // get a list of all peerings - index, listedPeerings, err := state.PeeringList(ws, args.EnterpriseMeta) - if err != nil { - return fmt.Errorf("could not list peers for service dump %w", err) - } - - if index > maxIndex { - maxIndex = index - } - - for _, p := range listedPeerings { - index, importedNodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, p.Name) + // If PeerName is not empty, we return only the imported services from that peer + if args.PeerName != "" { + // get a local dump for services + index, nodes, err := state.ServiceDump(ws, + args.ServiceKind, + args.UseServiceKind, + // Note we fetch imported services with wildcard namespace because imported services' namespaces + // are in a different locality; regardless of our local namespace, we return all imported services + // of the local partition. + args.EnterpriseMeta.WithWildcardNamespace(), + args.PeerName) if err != nil { - return fmt.Errorf("could not get a service dump for peer %q: %w", p.Name, err) + return fmt.Errorf("could not get a service dump for peer %q: %w", args.PeerName, err) } if index > maxIndex { maxIndex = index } - reply.ImportedNodes = append(reply.ImportedNodes, importedNodes...) - } + reply.Index = maxIndex + reply.ImportedNodes = nodes - // Get, store, and filter gateway services - idx, gatewayServices, err := state.DumpGatewayServices(ws) - if err != nil { - return err - } - reply.Gateways = gatewayServices + } else { + // otherwise return both local and all imported services - if idx > maxIndex { - maxIndex = idx - } - reply.Index = maxIndex + // get a local dump for services + index, nodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, structs.DefaultPeerKeyword) + if err != nil { + return fmt.Errorf("could not get a service dump for local nodes: %w", err) + } - raw, err := filter.Execute(reply.Nodes) - if err != nil { - return fmt.Errorf("could not filter local service dump: %w", err) + if index > maxIndex { + maxIndex = index + } + reply.Nodes = nodes + + // get a list of all peerings + index, listedPeerings, err := state.PeeringList(ws, args.EnterpriseMeta) + if err != nil { + return fmt.Errorf("could not list peers for service dump %w", err) + } + + if index > maxIndex { + maxIndex = index + } + + for _, p := range listedPeerings { + // Note we fetch imported services with wildcard namespace because imported services' namespaces + // are in a different locality; regardless of our local namespace, we return all imported services + // of the local partition. + index, importedNodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, args.EnterpriseMeta.WithWildcardNamespace(), p.Name) + if err != nil { + return fmt.Errorf("could not get a service dump for peer %q: %w", p.Name, err) + } + + if index > maxIndex { + maxIndex = index + } + reply.ImportedNodes = append(reply.ImportedNodes, importedNodes...) + } + + // Get, store, and filter gateway services + idx, gatewayServices, err := state.DumpGatewayServices(ws) + if err != nil { + return err + } + reply.Gateways = gatewayServices + + if idx > maxIndex { + maxIndex = idx + } + reply.Index = maxIndex + + raw, err := filter.Execute(reply.Nodes) + if err != nil { + return fmt.Errorf("could not filter local service dump: %w", err) + } + reply.Nodes = raw.(structs.CheckServiceNodes) } - reply.Nodes = raw.(structs.CheckServiceNodes) importedRaw, err := filter.Execute(reply.ImportedNodes) if err != nil { diff --git a/agent/consul/kvs_endpoint.go b/agent/consul/kvs_endpoint.go index 434ebcada..3f2cbe1cc 100644 --- a/agent/consul/kvs_endpoint.go +++ b/agent/consul/kvs_endpoint.go @@ -49,7 +49,7 @@ func kvsPreApply(logger hclog.Logger, srv *Server, authz resolver.Result, op api return false, err } - case api.KVGet, api.KVGetTree: + case api.KVGet, api.KVGetTree, api.KVGetOrEmpty: // Filtering for GETs is done on the output side. case api.KVCheckSession, api.KVCheckIndex: diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 389b79056..29cd216c9 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -1073,9 +1073,11 @@ func (s *Server) handleAliveMember(member serf.Member, nodeEntMeta *acl.Enterpri }, } - grpcPortStr := member.Tags["grpc_port"] - if v, err := strconv.Atoi(grpcPortStr); err == nil && v > 0 { - service.Meta["grpc_port"] = grpcPortStr + if parts.ExternalGRPCPort > 0 { + service.Meta["grpc_port"] = strconv.Itoa(parts.ExternalGRPCPort) + } + if parts.ExternalGRPCTLSPort > 0 { + service.Meta["grpc_tls_port"] = strconv.Itoa(parts.ExternalGRPCTLSPort) } // Attempt to join the consul server diff --git a/agent/consul/leader_connect_ca.go b/agent/consul/leader_connect_ca.go index 5e2268164..8d2bf9cb8 100644 --- a/agent/consul/leader_connect_ca.go +++ b/agent/consul/leader_connect_ca.go @@ -564,22 +564,9 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf return nil } - // Get the highest index - idx, _, err := state.CARoots(nil) - if err != nil { + if err := c.persistNewRootAndConfig(provider, rootCA, conf); err != nil { return err } - - // Store the root cert in raft - _, err = c.delegate.ApplyCARequest(&structs.CARequest{ - Op: structs.CAOpSetRoots, - Index: idx, - Roots: []*structs.CARoot{rootCA}, - }) - if err != nil { - return fmt.Errorf("raft apply failed: %w", err) - } - c.setCAProvider(provider, rootCA) c.logger.Info("initialized primary datacenter CA with provider", "provider", conf.Provider) @@ -1098,11 +1085,36 @@ func setLeafSigningCert(caRoot *structs.CARoot, pem string) error { return fmt.Errorf("error parsing leaf signing cert: %w", err) } + if err := pruneExpiredIntermediates(caRoot); err != nil { + return err + } + caRoot.IntermediateCerts = append(caRoot.IntermediateCerts, pem) caRoot.SigningKeyID = connect.EncodeSigningKeyID(cert.SubjectKeyId) return nil } +// pruneExpiredIntermediates removes expired intermediate certificates +// from the given CARoot. +func pruneExpiredIntermediates(caRoot *structs.CARoot) error { + var newIntermediates []string + now := time.Now() + for _, intermediatePEM := range caRoot.IntermediateCerts { + cert, err := connect.ParseCert(intermediatePEM) + if err != nil { + return fmt.Errorf("error parsing leaf signing cert: %w", err) + } + + // Only keep the intermediate cert if it's still valid. + if cert.NotAfter.After(now) { + newIntermediates = append(newIntermediates, intermediatePEM) + } + } + + caRoot.IntermediateCerts = newIntermediates + return nil +} + // runRenewIntermediate periodically attempts to renew the intermediate cert. func (c *CAManager) runRenewIntermediate(ctx context.Context) error { isPrimary := c.serverConf.Datacenter == c.serverConf.PrimaryDatacenter @@ -1381,10 +1393,15 @@ func (l *connectSignRateLimiter) getCSRRateLimiterWithLimit(limit rate.Limit) *r // identified by the SPIFFE ID in the given CSR's SAN. It performs authorization // using the given acl.Authorizer. func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, authz acl.Authorizer) (*structs.IssuedCert, error) { - // Parse the SPIFFE ID from the CSR SAN. - if len(csr.URIs) == 0 { - return nil, connect.InvalidCSRError("CSR SAN does not contain a SPIFFE ID") + // Note that only one spiffe id is allowed currently. If more than one is desired + // in future implmentations, then each ID should have authorization checks. + if len(csr.URIs) != 1 { + return nil, connect.InvalidCSRError("CSR SAN contains an invalid number of URIs: %v", len(csr.URIs)) } + if len(csr.EmailAddresses) > 0 { + return nil, connect.InvalidCSRError("CSR SAN does not allow specifying email addresses") + } + // Parse the SPIFFE ID from the CSR SAN. spiffeID, err := connect.ParseCertURI(csr.URIs[0]) if err != nil { return nil, err @@ -1426,8 +1443,21 @@ func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, au return nil, connect.InvalidCSRError("SPIFFE ID in CSR from a different datacenter: %s, "+ "we are %s", v.Datacenter, dc) } + case *connect.SpiffeIDServer: + // The authorizer passed in should have unlimited permissions. + if err := allow.ACLWriteAllowed(&authzContext); err != nil { + return nil, err + } + + // Verify that the DC in the URI matches us. + // The request must have been issued by a local server. + dc := c.serverConf.Datacenter + if v.Datacenter != dc { + return nil, connect.InvalidCSRError("SPIFFE ID in CSR from a different datacenter: %s, "+ + "we are %s", v.Datacenter, dc) + } default: - return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service or agent ID") + return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service, mesh-gateway, or agent ID") } return c.SignCertificate(csr, spiffeID) @@ -1447,9 +1477,11 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne if err != nil { return nil, err } + signingID := connect.SpiffeIDSigningForCluster(config.ClusterID) serviceID, isService := spiffeID.(*connect.SpiffeIDService) agentID, isAgent := spiffeID.(*connect.SpiffeIDAgent) + serverID, isServer := spiffeID.(*connect.SpiffeIDServer) mgwID, isMeshGateway := spiffeID.(*connect.SpiffeIDMeshGateway) var entMeta acl.EnterpriseMeta @@ -1468,6 +1500,12 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne } entMeta.Merge(mgwID.GetEnterpriseMeta()) + case isServer: + if !signingID.CanSign(spiffeID) { + return nil, connect.InvalidCSRError("SPIFFE ID in CSR from a different trust domain: %s, "+ + "we are %s", serverID.Host, signingID.Host()) + } + entMeta.Normalize() case isAgent: // isAgent - if we support more ID types then this would need to be an else if // here we are just automatically fixing the trust domain. For auto-encrypt and @@ -1494,7 +1532,7 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne entMeta.Merge(agentID.GetEnterpriseMeta()) default: - return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service, agent, or mesh gateway ID") + return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service, agent, server, or mesh gateway ID") } commonCfg, err := config.GetCommonConfig() @@ -1583,6 +1621,8 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne case isAgent: reply.Agent = agentID.Agent reply.AgentURI = cert.URIs[0].String() + case isServer: + reply.ServerURI = cert.URIs[0].String() default: return nil, errors.New("not possible") } diff --git a/agent/consul/leader_connect_ca_test.go b/agent/consul/leader_connect_ca_test.go index 37756eb20..d78f38d9e 100644 --- a/agent/consul/leader_connect_ca_test.go +++ b/agent/consul/leader_connect_ca_test.go @@ -24,6 +24,7 @@ import ( msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc" "github.com/hashicorp/consul-net-rpc/net/rpc" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/connect" ca "github.com/hashicorp/consul/agent/connect/ca" "github.com/hashicorp/consul/agent/consul/fsm" @@ -435,7 +436,6 @@ func TestCAManager_SignCertificate_WithExpiredCert(t *testing.T) { errorMsg string }{ {"intermediate valid", time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), false, ""}, - {"intermediate expired", time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), time.Now().AddDate(-2, 0, 0), time.Now().AddDate(0, 0, -1), true, "intermediate expired: certificate expired, expiration date"}, {"root expired", time.Now().AddDate(-2, 0, 0), time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), true, "root expired: certificate expired, expiration date"}, // a cert that is not yet valid is ok, assume it will be valid soon enough {"intermediate in the future", time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, 2), time.Now().AddDate(0, 0, 1), time.Now().AddDate(0, 0, 2), false, ""}, @@ -1043,3 +1043,180 @@ func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string, rootPEM require.NoError(t, err, "failed to set signed intermediate") return lib.EnsureTrailingNewline(buf.String()) } + +func TestCAManager_Sign_SpiffeIDServer(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + _, s1 := testServerWithConfig(t) + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + codec := rpcClient(t, s1) + roots := structs.IndexedCARoots{} + + retry.Run(t, func(r *retry.R) { + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) + require.NoError(r, err) + require.Len(r, roots.Roots, 1) + }) + + pk, _, err := connect.GeneratePrivateKey() + require.NoError(t, err) + + // Request a leaf certificate for a server. + spiffeID := &connect.SpiffeIDServer{ + Host: roots.TrustDomain, + Datacenter: "dc1", + } + csr, err := connect.CreateCSR(spiffeID, pk, nil, nil) + require.NoError(t, err) + + req := structs.CASignRequest{CSR: csr} + cert := structs.IssuedCert{} + err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert) + require.NoError(t, err) + + // Verify the chain of trust. + verifyLeafCert(t, roots.Roots[0], cert.CertPEM) + + // Verify the Server's URI. + require.Equal(t, fmt.Sprintf("spiffe://%s/agent/server/dc/dc1", roots.TrustDomain), cert.ServerURI) +} + +func TestCAManager_AuthorizeAndSignCertificate(t *testing.T) { + conf := DefaultConfig() + conf.PrimaryDatacenter = "dc1" + conf.Datacenter = "dc2" + manager := NewCAManager(nil, nil, testutil.Logger(t), conf) + + agentURL := connect.SpiffeIDAgent{ + Agent: "test-agent", + Datacenter: conf.PrimaryDatacenter, + Host: "test-host", + }.URI() + serviceURL := connect.SpiffeIDService{ + Datacenter: conf.PrimaryDatacenter, + Namespace: "ns1", + Service: "test-service", + }.URI() + meshURL := connect.SpiffeIDMeshGateway{ + Datacenter: conf.PrimaryDatacenter, + Host: "test-host", + Partition: "test-partition", + }.URI() + + tests := []struct { + name string + expectErr string + getCSR func() *x509.CertificateRequest + authAllow bool + }{ + { + name: "err_not_one_uri", + expectErr: "CSR SAN contains an invalid number of URIs", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL, agentURL}, + } + }, + }, + { + name: "err_email", + expectErr: "CSR SAN does not allow specifying email addresses", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL}, + EmailAddresses: []string{"test@example.com"}, + } + }, + }, + { + name: "err_invalid_spiffe_id", + expectErr: "SPIFFE ID is not in the expected format", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{connect.SpiffeIDAgent{}.URI()}, + } + }, + }, + { + name: "err_service_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{serviceURL}, + } + }, + }, + { + name: "err_service_different_dc", + expectErr: "SPIFFE ID in CSR from a different datacenter", + authAllow: true, + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{serviceURL}, + } + }, + }, + { + name: "err_agent_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{agentURL}, + } + }, + }, + { + name: "err_meshgw_write_not_allowed", + expectErr: "Permission denied", + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{meshURL}, + } + }, + }, + { + name: "err_meshgw_different_dc", + expectErr: "SPIFFE ID in CSR from a different datacenter", + authAllow: true, + getCSR: func() *x509.CertificateRequest { + return &x509.CertificateRequest{ + URIs: []*url.URL{meshURL}, + } + }, + }, + { + name: "err_invalid_spiffe_type", + expectErr: "SPIFFE ID in CSR must be a service, mesh-gateway, or agent ID", + getCSR: func() *x509.CertificateRequest { + u := connect.SpiffeIDSigning{ + ClusterID: "test-cluster-id", + Domain: "test-domain", + }.URI() + return &x509.CertificateRequest{ + URIs: []*url.URL{u}, + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + authz := acl.DenyAll() + if tc.authAllow { + authz = acl.AllowAll() + } + + cert, err := manager.AuthorizeAndSignCertificate(tc.getCSR(), authz) + if tc.expectErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + require.NotNil(t, cert) + } + }) + } +} diff --git a/agent/consul/leader_connect_test.go b/agent/consul/leader_connect_test.go index d9b386386..bf6fa9522 100644 --- a/agent/consul/leader_connect_test.go +++ b/agent/consul/leader_connect_test.go @@ -401,6 +401,18 @@ func TestCAManager_RenewIntermediate_Vault_Primary(t *testing.T) { err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert) require.NoError(t, err) verifyLeafCert(t, activeRoot, cert.CertPEM) + + // Wait for the primary's old intermediate to be pruned after expiring. + oldIntermediate := activeRoot.IntermediateCerts[0] + retry.Run(t, func(r *retry.R) { + store := s1.caManager.delegate.State() + _, storedRoot, err := store.CARootActive(nil) + r.Check(err) + + if storedRoot.IntermediateCerts[0] == oldIntermediate { + r.Fatal("old intermediate should be gone") + } + }) } func patchIntermediateCertRenewInterval(t *testing.T) { @@ -516,6 +528,18 @@ func TestCAManager_RenewIntermediate_Secondary(t *testing.T) { err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", &req, &cert) require.NoError(t, err) verifyLeafCert(t, activeRoot, cert.CertPEM) + + // Wait for dc2's old intermediate to be pruned after expiring. + oldIntermediate := activeRoot.IntermediateCerts[0] + retry.Run(t, func(r *retry.R) { + store := s2.caManager.delegate.State() + _, storedRoot, err := store.CARootActive(nil) + r.Check(err) + + if storedRoot.IntermediateCerts[0] == oldIntermediate { + r.Fatal("old intermediate should be gone") + } + }) } func TestConnectCA_ConfigurationSet_RootRotation_Secondary(t *testing.T) { @@ -667,6 +691,71 @@ func TestConnectCA_ConfigurationSet_RootRotation_Secondary(t *testing.T) { require.NoError(t, err) } +func TestCAManager_Initialize_Vault_KeepOldRoots_Primary(t *testing.T) { + ca.SkipIfVaultNotPresent(t) + + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + testVault := ca.NewTestVaultServer(t) + defer testVault.Stop() + + dir1pre, s1pre := testServer(t) + defer os.RemoveAll(dir1pre) + defer s1pre.Shutdown() + codec := rpcClient(t, s1pre) + defer codec.Close() + + testrpc.WaitForLeader(t, s1pre.RPC, "dc1") + + // Update the CA config to use Vault - this should force the generation of a new root cert. + vaultCAConf := &structs.CAConfiguration{ + Provider: "vault", + Config: map[string]interface{}{ + "Address": testVault.Addr, + "Token": testVault.RootToken, + "RootPKIPath": "pki-root/", + "IntermediatePKIPath": "pki-intermediate/", + }, + } + + args := &structs.CARequest{ + Datacenter: "dc1", + Config: vaultCAConf, + } + var reply interface{} + + require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply)) + + // Should have 2 roots now. + _, roots, err := s1pre.fsm.State().CARoots(nil) + require.NoError(t, err) + require.Len(t, roots, 2) + + // Shutdown s1pre and restart it to trigger the primary CA init. + s1pre.Shutdown() + + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.DataDir = s1pre.config.DataDir + c.NodeName = s1pre.config.NodeName + c.NodeID = s1pre.config.NodeID + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Roots should be unchanged + _, rootsAfterRestart, err := s1.fsm.State().CARoots(nil) + require.NoError(t, err) + require.Len(t, rootsAfterRestart, 2) + require.Equal(t, roots[0].ID, rootsAfterRestart[0].ID) + require.Equal(t, roots[1].ID, rootsAfterRestart[1].ID) +} + func TestCAManager_Initialize_Vault_FixesSigningKeyID_Primary(t *testing.T) { ca.SkipIfVaultNotPresent(t) diff --git a/agent/consul/leader_peering.go b/agent/consul/leader_peering.go index bc5b669cd..cf4a4e054 100644 --- a/agent/consul/leader_peering.go +++ b/agent/consul/leader_peering.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math" + "strings" "time" "github.com/armon/go-metrics" @@ -30,12 +31,29 @@ import ( "github.com/hashicorp/consul/proto/pbpeerstream" ) -var leaderExportedServicesCountKey = []string{"consul", "peering", "exported_services"} +var leaderExportedServicesCountKeyDeprecated = []string{"consul", "peering", "exported_services"} +var leaderExportedServicesCountKey = []string{"peering", "exported_services"} +var leaderHealthyPeeringKeyDeprecated = []string{"consul", "peering", "healthy"} +var leaderHealthyPeeringKey = []string{"peering", "healthy"} var LeaderPeeringMetrics = []prometheus.GaugeDefinition{ + { + Name: leaderExportedServicesCountKeyDeprecated, + Help: fmt.Sprint("Deprecated - please use ", strings.Join(leaderExportedServicesCountKey, "_")), + }, { Name: leaderExportedServicesCountKey, Help: "A gauge that tracks how many services are exported for the peering. " + - "The labels are \"peering\" and, for enterprise, \"partition\". " + + "The labels are \"peer_name\", \"peer_id\" and, for enterprise, \"partition\". " + + "We emit this metric every 9 seconds", + }, + { + Name: leaderHealthyPeeringKeyDeprecated, + Help: fmt.Sprint("Deprecated - please use ", strings.Join(leaderExportedServicesCountKey, "_")), + }, + { + Name: leaderHealthyPeeringKey, + Help: "A gauge that tracks how if a peering is healthy (1) or not (0). " + + "The labels are \"peer_name\", \"peer_id\" and, for enterprise, \"partition\". " + "We emit this metric every 9 seconds", }, } @@ -68,6 +86,7 @@ func (s *Server) runPeeringMetrics(ctx context.Context) error { // "Zero-out" the metric on exit so that when prometheus scrapes this // metric from a non-leader, it does not get a stale value. + metrics.SetGauge(leaderExportedServicesCountKeyDeprecated, float32(0)) metrics.SetGauge(leaderExportedServicesCountKey, float32(0)) return nil case <-ticker.C: @@ -85,13 +104,6 @@ func (s *Server) emitPeeringMetricsOnce(logger hclog.Logger, metricsImpl *metric } for _, peer := range peers { - status, found := s.peerStreamServer.StreamStatus(peer.ID) - if !found { - logger.Trace("did not find status for", "peer_name", peer.Name) - continue - } - - esc := status.GetExportedServicesCount() part := peer.Partition labels := []metrics.Label{ {Name: "peer_name", Value: peer.Name}, @@ -101,7 +113,28 @@ func (s *Server) emitPeeringMetricsOnce(logger hclog.Logger, metricsImpl *metric labels = append(labels, metrics.Label{Name: "partition", Value: part}) } - metricsImpl.SetGaugeWithLabels(leaderExportedServicesCountKey, float32(esc), labels) + status, found := s.peerStreamServer.StreamStatus(peer.ID) + if found { + // exported services count metric + esc := status.GetExportedServicesCount() + metricsImpl.SetGaugeWithLabels(leaderExportedServicesCountKeyDeprecated, float32(esc), labels) + metricsImpl.SetGaugeWithLabels(leaderExportedServicesCountKey, float32(esc), labels) + } + + // peering health metric + if status.NeverConnected { + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKeyDeprecated, float32(math.NaN()), labels) + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKey, float32(math.NaN()), labels) + } else { + healthy := s.peerStreamServer.Tracker.IsHealthy(status) + healthyInt := 0 + if healthy { + healthyInt = 1 + } + + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKeyDeprecated, float32(healthyInt), labels) + metricsImpl.SetGaugeWithLabels(leaderHealthyPeeringKey, float32(healthyInt), labels) + } } return nil @@ -277,13 +310,6 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me return fmt.Errorf("failed to build TLS dial option from peering: %w", err) } - // Create a ring buffer to cycle through peer addresses in the retry loop below. - buffer := ring.New(len(peer.PeerServerAddresses)) - for _, addr := range peer.PeerServerAddresses { - buffer.Value = addr - buffer = buffer.Next() - } - secret, err := s.fsm.State().PeeringSecretsRead(ws, peer.ID) if err != nil { return fmt.Errorf("failed to read secret for peering: %w", err) @@ -294,27 +320,26 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me logger.Trace("establishing stream to peer") - retryCtx, cancel := context.WithCancel(ctx) - cancelFns[peer.ID] = cancel - - streamStatus, err := s.peerStreamTracker.Register(peer.ID) + streamStatus, err := s.peerStreamServer.Tracker.Register(peer.ID) if err != nil { return fmt.Errorf("failed to register stream: %v", err) } + streamCtx, cancel := context.WithCancel(ctx) + cancelFns[peer.ID] = cancel + + // Start a goroutine to watch for updates to peer server addresses. + // The latest valid server address can be received from nextServerAddr. + nextServerAddr := make(chan string) + go s.watchPeerServerAddrs(streamCtx, peer, nextServerAddr) + // Establish a stream-specific retry so that retrying stream/conn errors isn't dependent on state store changes. - go retryLoopBackoffPeering(retryCtx, logger, func() error { + go retryLoopBackoffPeering(streamCtx, logger, func() error { // Try a new address on each iteration by advancing the ring buffer on errors. - defer func() { - buffer = buffer.Next() - }() - addr, ok := buffer.Value.(string) - if !ok { - return fmt.Errorf("peer server address type %T is not a string", buffer.Value) - } + addr := <-nextServerAddr logger.Trace("dialing peer", "addr", addr) - conn, err := grpc.DialContext(retryCtx, addr, + conn, err := grpc.DialContext(streamCtx, addr, // TODO(peering): use a grpc.WithStatsHandler here?) tlsOption, // For keep alive parameters there is a larger comment in ClientConnPool.dial about that. @@ -331,7 +356,7 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me defer conn.Close() client := pbpeerstream.NewPeerStreamServiceClient(conn) - stream, err := client.StreamResources(retryCtx) + stream, err := client.StreamResources(streamCtx) if err != nil { return err } @@ -379,6 +404,74 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, ws me return nil } +// watchPeerServerAddrs sends an up-to-date peer server address to nextServerAddr. +// It loads the server addresses into a ring buffer and cycles through them until: +// 1. streamCtx is cancelled (peer is deleted) +// 2. the peer is modified and the watchset fires. +// +// In case (2) we refetch the peering and rebuild the ring buffer. +func (s *Server) watchPeerServerAddrs(ctx context.Context, peer *pbpeering.Peering, nextServerAddr chan<- string) { + defer close(nextServerAddr) + + // we initialize the ring buffer with the peer passed to `establishStream` + // because the caller has pre-checked `peer.ShouldDial`, guaranteeing + // at least one server address. + // + // IMPORTANT: ringbuf must always be length > 0 or else `<-nextServerAddr` may block. + ringbuf := ring.New(len(peer.PeerServerAddresses)) + for _, addr := range peer.PeerServerAddresses { + ringbuf.Value = addr + ringbuf = ringbuf.Next() + } + innerWs := memdb.NewWatchSet() + _, _, err := s.fsm.State().PeeringReadByID(innerWs, peer.ID) + if err != nil { + s.logger.Warn("failed to watch for changes to peer; server addresses may become stale over time.", + "peer_id", peer.ID, + "error", err) + } + + fetchAddrs := func() error { + // reinstantiate innerWs to prevent it from growing indefinitely + innerWs = memdb.NewWatchSet() + _, peering, err := s.fsm.State().PeeringReadByID(innerWs, peer.ID) + if err != nil { + return fmt.Errorf("failed to fetch peer %q: %w", peer.ID, err) + } + if !peering.IsActive() { + return fmt.Errorf("peer %q is no longer active", peer.ID) + } + if len(peering.PeerServerAddresses) == 0 { + return fmt.Errorf("peer %q has no addresses to dial", peer.ID) + } + + ringbuf = ring.New(len(peering.PeerServerAddresses)) + for _, addr := range peering.PeerServerAddresses { + ringbuf.Value = addr + ringbuf = ringbuf.Next() + } + return nil + } + + for { + select { + case nextServerAddr <- ringbuf.Value.(string): + ringbuf = ringbuf.Next() + case err := <-innerWs.WatchCh(ctx): + if err != nil { + // context was cancelled + return + } + // watch fired so we refetch the peering and rebuild the ring buffer + if err := fetchAddrs(); err != nil { + s.logger.Warn("watchset for peer was fired but failed to update server addresses", + "peer_id", peer.ID, + "error", err) + } + } + } +} + func (s *Server) startPeeringDeferredDeletion(ctx context.Context) { s.leaderRoutineManager.Start(ctx, peeringDeletionRoutineName, s.runPeeringDeletions) } @@ -391,6 +484,12 @@ func (s *Server) runPeeringDeletions(ctx context.Context) error { // process. This includes deletion of the peerings themselves in addition to any peering data raftLimiter := rate.NewLimiter(defaultDeletionApplyRate, int(defaultDeletionApplyRate)) for { + select { + case <-ctx.Done(): + return nil + default: + } + ws := memdb.NewWatchSet() state := s.fsm.State() _, peerings, err := s.fsm.State().PeeringListDeleted(ws) diff --git a/agent/consul/leader_peering_test.go b/agent/consul/leader_peering_test.go index 46a74b6ad..331e7324a 100644 --- a/agent/consul/leader_peering_test.go +++ b/agent/consul/leader_peering_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io/ioutil" + "math" "testing" "time" @@ -17,6 +18,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" grpcstatus "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" @@ -24,6 +26,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/sdk/freeport" + "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/types" @@ -37,6 +40,7 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) { testLeader_PeeringSync_Lifecycle_ClientDeletion(t, true) }) } + func testLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T, enableTLS bool) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -134,9 +138,11 @@ func testLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T, enableTLS boo // Delete the peering to trigger the termination sequence. deleted := &pbpeering.Peering{ - ID: p.Peering.ID, - Name: "my-peer-acceptor", - DeletedAt: structs.TimeToProto(time.Now()), + ID: p.Peering.ID, + Name: "my-peer-acceptor", + State: pbpeering.PeeringState_DELETING, + PeerServerAddresses: p.Peering.PeerServerAddresses, + DeletedAt: structs.TimeToProto(time.Now()), } require.NoError(t, dialer.fsm.State().PeeringWrite(2000, &pbpeering.PeeringWriteRequest{Peering: deleted})) dialer.logger.Trace("deleted peering for my-peer-acceptor") @@ -259,6 +265,7 @@ func testLeader_PeeringSync_Lifecycle_AcceptorDeletion(t *testing.T, enableTLS b deleted := &pbpeering.Peering{ ID: p.Peering.PeerID, Name: "my-peer-dialer", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), } @@ -428,6 +435,7 @@ func TestLeader_Peering_DeferredDeletion(t *testing.T) { Peering: &pbpeering.Peering{ ID: peerID, Name: peerName, + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, })) @@ -974,6 +982,7 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { var ( s2PeerID1 = generateUUID() s2PeerID2 = generateUUID() + s2PeerID3 = generateUUID() testContextTimeout = 60 * time.Second lastIdx = uint64(0) ) @@ -1063,6 +1072,24 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { // mimic tracking exported services mst2.TrackExportedService(structs.ServiceName{Name: "d-service"}) mst2.TrackExportedService(structs.ServiceName{Name: "e-service"}) + + // pretend that the hearbeat happened + mst2.TrackRecvHeartbeat() + } + + // Simulate a peering that never connects + { + p3 := &pbpeering.Peering{ + ID: s2PeerID3, + Name: "my-peer-s4", + PeerID: token.PeerID, // doesn't much matter what these values are + PeerCAPems: token.CA, + PeerServerName: token.ServerName, + PeerServerAddresses: token.ServerAddresses, + } + require.True(t, p3.ShouldDial()) + lastIdx++ + require.NoError(t, s2.fsm.State().PeeringWrite(lastIdx, &pbpeering.PeeringWriteRequest{Peering: p3})) } // set up a metrics sink @@ -1092,6 +1119,18 @@ func TestLeader_PeeringMetrics_emitPeeringMetrics(t *testing.T) { require.True(r, ok, fmt.Sprintf("did not find the key %q", keyMetric2)) require.Equal(r, float32(2), metric2.Value) // for d, e services + + keyHealthyMetric2 := fmt.Sprintf("us-west.consul.peering.healthy;peer_name=my-peer-s3;peer_id=%s", s2PeerID2) + healthyMetric2, ok := intv.Gauges[keyHealthyMetric2] + require.True(r, ok, fmt.Sprintf("did not find the key %q", keyHealthyMetric2)) + + require.Equal(r, float32(1), healthyMetric2.Value) + + keyHealthyMetric3 := fmt.Sprintf("us-west.consul.peering.healthy;peer_name=my-peer-s4;peer_id=%s", s2PeerID3) + healthyMetric3, ok := intv.Gauges[keyHealthyMetric3] + require.True(r, ok, fmt.Sprintf("did not find the key %q", keyHealthyMetric3)) + + require.True(r, math.IsNaN(float64(healthyMetric3.Value))) }) } @@ -1131,6 +1170,7 @@ func TestLeader_Peering_NoDeletionWhenPeeringDisabled(t *testing.T) { Peering: &pbpeering.Peering{ ID: peerID, Name: peerName, + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, })) @@ -1182,7 +1222,7 @@ func TestLeader_Peering_NoEstablishmentWhenPeeringDisabled(t *testing.T) { })) require.Never(t, func() bool { - _, found := s1.peerStreamTracker.StreamStatus(peerID) + _, found := s1.peerStreamServer.StreamStatus(peerID) return found }, 7*time.Second, 1*time.Second, "peering should not have been established") } @@ -1343,3 +1383,138 @@ func Test_isFailedPreconditionErr(t *testing.T) { werr := fmt.Errorf("wrapped: %w", err) assert.True(t, isFailedPreconditionErr(werr)) } + +func Test_Leader_PeeringSync_ServerAddressUpdates(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + // We want 1s retries for this test + orig := maxRetryBackoff + maxRetryBackoff = 1 + t.Cleanup(func() { maxRetryBackoff = orig }) + + _, acceptor := testServerWithConfig(t, func(c *Config) { + c.NodeName = "acceptor" + c.Datacenter = "dc1" + c.TLSConfig.Domain = "consul" + }) + testrpc.WaitForLeader(t, acceptor.RPC, "dc1") + + // Create a peering by generating a token + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + conn, err := grpc.DialContext(ctx, acceptor.config.RPCAddr.String(), + grpc.WithContextDialer(newServerDialer(acceptor.config.RPCAddr.String())), + grpc.WithInsecure(), + grpc.WithBlock()) + require.NoError(t, err) + defer conn.Close() + + acceptorClient := pbpeering.NewPeeringServiceClient(conn) + + req := pbpeering.GenerateTokenRequest{ + PeerName: "my-peer-dialer", + } + resp, err := acceptorClient.GenerateToken(ctx, &req) + require.NoError(t, err) + + // Bring up dialer and establish a peering with acceptor's token so that it attempts to dial. + _, dialer := testServerWithConfig(t, func(c *Config) { + c.NodeName = "dialer" + c.Datacenter = "dc2" + c.PrimaryDatacenter = "dc2" + }) + testrpc.WaitForLeader(t, dialer.RPC, "dc2") + + // Create a peering at dialer by establishing a peering with acceptor's token + ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + conn, err = grpc.DialContext(ctx, dialer.config.RPCAddr.String(), + grpc.WithContextDialer(newServerDialer(dialer.config.RPCAddr.String())), + grpc.WithInsecure(), + grpc.WithBlock()) + require.NoError(t, err) + defer conn.Close() + + dialerClient := pbpeering.NewPeeringServiceClient(conn) + + establishReq := pbpeering.EstablishRequest{ + PeerName: "my-peer-acceptor", + PeeringToken: resp.PeeringToken, + } + _, err = dialerClient.Establish(ctx, &establishReq) + require.NoError(t, err) + + p, err := dialerClient.PeeringRead(ctx, &pbpeering.PeeringReadRequest{Name: "my-peer-acceptor"}) + require.NoError(t, err) + + retry.Run(t, func(r *retry.R) { + status, found := dialer.peerStreamServer.StreamStatus(p.Peering.ID) + require.True(r, found) + require.True(r, status.Connected) + }) + + testutil.RunStep(t, "calling establish with active connection does not overwrite server addresses", func(t *testing.T) { + ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // generate a new token from the acceptor + req := pbpeering.GenerateTokenRequest{ + PeerName: "my-peer-dialer", + } + resp, err := acceptorClient.GenerateToken(ctx, &req) + require.NoError(t, err) + + token, err := acceptor.peeringBackend.DecodeToken([]byte(resp.PeeringToken)) + require.NoError(t, err) + + // we will update the token with bad addresses to assert it doesn't clobber existing ones + token.ServerAddresses = []string{"1.2.3.4:1234"} + + badToken, err := acceptor.peeringBackend.EncodeToken(token) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + t.Cleanup(cancel) + + // Try establishing. + // This call will only succeed if the bad address was not used in the calls to exchange the peering secret. + establishReq := pbpeering.EstablishRequest{ + PeerName: "my-peer-acceptor", + PeeringToken: string(badToken), + } + _, err = dialerClient.Establish(ctx, &establishReq) + require.NoError(t, err) + + p, err := dialerClient.PeeringRead(ctx, &pbpeering.PeeringReadRequest{Name: "my-peer-acceptor"}) + require.NoError(t, err) + require.NotContains(t, p.Peering.PeerServerAddresses, "1.2.3.4:1234") + }) + + testutil.RunStep(t, "updated server addresses are picked up by the leader", func(t *testing.T) { + // force close the acceptor's gRPC server so the dialier retries with a new address. + acceptor.externalGRPCServer.Stop() + + clone := proto.Clone(p.Peering) + updated := clone.(*pbpeering.Peering) + // start with a bad address so we can assert for a specific error + updated.PeerServerAddresses = append([]string{ + "bad", + }, p.Peering.PeerServerAddresses...) + + // this write will wake up the watch on the leader to refetch server addresses + require.NoError(t, dialer.fsm.State().PeeringWrite(2000, &pbpeering.PeeringWriteRequest{Peering: updated})) + + retry.Run(t, func(r *retry.R) { + status, found := dialer.peerStreamServer.StreamStatus(p.Peering.ID) + require.True(r, found) + // We assert for this error to be set which would indicate that we iterated + // through a bad address. + require.Contains(r, status.LastSendErrorMessage, "transport: Error while dialing dial tcp: address bad: missing port in address") + require.False(r, status.Connected) + }) + }) +} diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index b041d2f92..1edaa88a3 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strconv" "strings" "testing" "time" @@ -19,6 +20,7 @@ import ( "github.com/hashicorp/consul/agent/structs" tokenStore "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/freeport" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/testrpc" @@ -355,8 +357,10 @@ func TestLeader_CheckServersMeta(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") } - t.Parallel() + + ports := freeport.GetN(t, 2) // s3 grpc, s3 grpc_tls + dir1, s1 := testServerWithConfig(t, func(c *Config) { c.PrimaryDatacenter = "dc1" c.ACLsEnabled = true @@ -383,6 +387,8 @@ func TestLeader_CheckServersMeta(t *testing.T) { c.ACLInitialManagementToken = "root" c.ACLResolverSettings.ACLDefaultPolicy = "allow" c.Bootstrap = false + c.GRPCPort = ports[0] + c.GRPCTLSPort = ports[1] }) defer os.RemoveAll(dir3) defer s3.Shutdown() @@ -456,6 +462,14 @@ func TestLeader_CheckServersMeta(t *testing.T) { if newVersion != versionToExpect { r.Fatalf("Expected version to be updated to %s, was %s", versionToExpect, newVersion) } + grpcPort := service.Meta["grpc_port"] + if grpcPort != strconv.Itoa(ports[0]) { + r.Fatalf("Expected grpc port to be %d, was %s", ports[0], grpcPort) + } + grpcTLSPort := service.Meta["grpc_tls_port"] + if grpcTLSPort != strconv.Itoa(ports[1]) { + r.Fatalf("Expected grpc tls port to be %d, was %s", ports[1], grpcTLSPort) + } }) } diff --git a/agent/consul/merge_service_config.go b/agent/consul/merge_service_config.go index 027a2d3f5..706e24f4a 100644 --- a/agent/consul/merge_service_config.go +++ b/agent/consul/merge_service_config.go @@ -3,13 +3,14 @@ package consul import ( "fmt" - "github.com/hashicorp/consul/agent/configentry" - "github.com/hashicorp/consul/agent/consul/state" - "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/go-hclog" memdb "github.com/hashicorp/go-memdb" "github.com/imdario/mergo" "github.com/mitchellh/copystructure" + + "github.com/hashicorp/consul/agent/configentry" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/structs" ) // mergeNodeServiceWithCentralConfig merges a service instance (NodeService) with the @@ -66,7 +67,7 @@ func mergeNodeServiceWithCentralConfig( ns.ID, err) } - defaults, err := computeResolvedServiceConfig( + defaults, err := configentry.ComputeResolvedServiceConfig( configReq, upstreams, false, @@ -87,218 +88,6 @@ func mergeNodeServiceWithCentralConfig( return cfgIndex, mergedns, nil } -func computeResolvedServiceConfig( - args *structs.ServiceConfigRequest, - upstreamIDs []structs.ServiceID, - legacyUpstreams bool, - entries *configentry.ResolvedServiceConfigSet, - logger hclog.Logger, -) (*structs.ServiceConfigResponse, error) { - var thisReply structs.ServiceConfigResponse - - thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault - - // TODO(freddy) Refactor this into smaller set of state store functions - // Pass the WatchSet to both the service and proxy config lookups. If either is updated during the - // blocking query, this function will be rerun and these state store lookups will both be current. - // We use the default enterprise meta to look up the global proxy defaults because they are not namespaced. - var proxyConfGlobalProtocol string - proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault()) - if proxyConf != nil { - // Apply the proxy defaults to the sidecar's proxy config - mapCopy, err := copystructure.Copy(proxyConf.Config) - if err != nil { - return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err) - } - thisReply.ProxyConfig = mapCopy.(map[string]interface{}) - thisReply.Mode = proxyConf.Mode - thisReply.TransparentProxy = proxyConf.TransparentProxy - thisReply.MeshGateway = proxyConf.MeshGateway - thisReply.Expose = proxyConf.Expose - - // Extract the global protocol from proxyConf for upstream configs. - rawProtocol := proxyConf.Config["protocol"] - if rawProtocol != nil { - var ok bool - proxyConfGlobalProtocol, ok = rawProtocol.(string) - if !ok { - return nil, fmt.Errorf("invalid protocol type %T", rawProtocol) - } - } - } - - serviceConf := entries.GetServiceDefaults( - structs.NewServiceID(args.Name, &args.EnterpriseMeta), - ) - if serviceConf != nil { - if serviceConf.Expose.Checks { - thisReply.Expose.Checks = true - } - if len(serviceConf.Expose.Paths) >= 1 { - thisReply.Expose.Paths = serviceConf.Expose.Paths - } - if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault { - thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode - } - if serviceConf.Protocol != "" { - if thisReply.ProxyConfig == nil { - thisReply.ProxyConfig = make(map[string]interface{}) - } - thisReply.ProxyConfig["protocol"] = serviceConf.Protocol - } - if serviceConf.TransparentProxy.OutboundListenerPort != 0 { - thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort - } - if serviceConf.TransparentProxy.DialedDirectly { - thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly - } - if serviceConf.Mode != structs.ProxyModeDefault { - thisReply.Mode = serviceConf.Mode - } - if serviceConf.Destination != nil { - thisReply.Destination = *serviceConf.Destination - } - - thisReply.Meta = serviceConf.Meta - } - - // First collect all upstreams into a set of seen upstreams. - // Upstreams can come from: - // - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint - // - Implicitly from centralized upstream config in service-defaults - seenUpstreams := map[structs.ServiceID]struct{}{} - - var ( - noUpstreamArgs = len(upstreamIDs) == 0 && len(args.Upstreams) == 0 - - // Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode - // will never be transparent because the service config request does not use the resolved value. - tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent - ) - - // The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration. - // If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode. - // Otherwise we would return a resolved upstream config to a proxy with no configured upstreams. - if noUpstreamArgs && !tproxy { - return &thisReply, nil - } - - // First store all upstreams that were provided in the request - for _, sid := range upstreamIDs { - if _, ok := seenUpstreams[sid]; !ok { - seenUpstreams[sid] = struct{}{} - } - } - - // Then store upstreams inferred from service-defaults and mapify the overrides. - var ( - upstreamConfigs = make(map[structs.ServiceID]*structs.UpstreamConfig) - upstreamDefaults *structs.UpstreamConfig - // usConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID. - usConfigs = make(map[structs.ServiceID]map[string]interface{}) - ) - if serviceConf != nil && serviceConf.UpstreamConfig != nil { - for i, override := range serviceConf.UpstreamConfig.Overrides { - if override.Name == "" { - logger.Warn( - "Skipping UpstreamConfig.Overrides entry without a required name field", - "entryIndex", i, - "kind", serviceConf.GetKind(), - "name", serviceConf.GetName(), - "namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(), - ) - continue // skip this impossible condition - } - seenUpstreams[override.ServiceID()] = struct{}{} - upstreamConfigs[override.ServiceID()] = override - } - if serviceConf.UpstreamConfig.Defaults != nil { - upstreamDefaults = serviceConf.UpstreamConfig.Defaults - - // Store the upstream defaults under a wildcard key so that they can be applied to - // upstreams that are inferred from intentions and do not have explicit upstream configuration. - cfgMap := make(map[string]interface{}) - upstreamDefaults.MergeInto(cfgMap) - - wildcard := structs.NewServiceID(structs.WildcardSpecifier, args.WithWildcardNamespace()) - usConfigs[wildcard] = cfgMap - } - } - - for upstream := range seenUpstreams { - resolvedCfg := make(map[string]interface{}) - - // The protocol of an upstream is resolved in this order: - // 1. Default protocol from proxy-defaults (how all services should be addressed) - // 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed) - // 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream - // (how the downstream wants to address it) - protocol := proxyConfGlobalProtocol - - upstreamSvcDefaults := entries.GetServiceDefaults( - structs.NewServiceID(upstream.ID, &upstream.EnterpriseMeta), - ) - if upstreamSvcDefaults != nil { - if upstreamSvcDefaults.Protocol != "" { - protocol = upstreamSvcDefaults.Protocol - } - } - - if protocol != "" { - resolvedCfg["protocol"] = protocol - } - - // Merge centralized defaults for all upstreams before configuration for specific upstreams - if upstreamDefaults != nil { - upstreamDefaults.MergeInto(resolvedCfg) - } - - // The MeshGateway value from the proxy registration overrides the one from upstream_defaults - // because it is specific to the proxy instance. - // - // The goal is to flatten the mesh gateway mode in this order: - // 0. Value from centralized upstream_defaults - // 1. Value from local proxy registration - // 2. Value from centralized upstream_config - // 3. Value from local upstream definition. This last step is done in the client's service manager. - if !args.MeshGateway.IsZero() { - resolvedCfg["mesh_gateway"] = args.MeshGateway - } - - if upstreamConfigs[upstream] != nil { - upstreamConfigs[upstream].MergeInto(resolvedCfg) - } - - if len(resolvedCfg) > 0 { - usConfigs[upstream] = resolvedCfg - } - } - - // don't allocate the slices just to not fill them - if len(usConfigs) == 0 { - return &thisReply, nil - } - - if legacyUpstreams { - // For legacy upstreams we return a map that is only keyed on the string ID, since they precede namespaces - thisReply.UpstreamConfigs = make(map[string]map[string]interface{}) - - for us, conf := range usConfigs { - thisReply.UpstreamConfigs[us.ID] = conf - } - - } else { - thisReply.UpstreamIDConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(usConfigs)) - - for us, conf := range usConfigs { - thisReply.UpstreamIDConfigs = append(thisReply.UpstreamIDConfigs, - structs.OpaqueUpstreamConfig{Upstream: us, Config: conf}) - } - } - - return &thisReply, nil -} - // MergeServiceConfig merges the service into defaults to produce the final effective // config for the specified service. func MergeServiceConfig(defaults *structs.ServiceConfigResponse, service *structs.NodeService) (*structs.NodeService, error) { diff --git a/agent/consul/merge_service_config_test.go b/agent/consul/merge_service_config_test.go index 5a866dce2..a4b88308e 100644 --- a/agent/consul/merge_service_config_test.go +++ b/agent/consul/merge_service_config_test.go @@ -3,10 +3,11 @@ package consul import ( "testing" - "github.com/hashicorp/consul/agent/structs" "github.com/mitchellh/copystructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent/structs" ) func Test_MergeServiceConfig_TransparentProxy(t *testing.T) { @@ -153,6 +154,12 @@ func Test_MergeServiceConfig_UpstreamOverrides(t *testing.T) { DestinationNamespace: "default", DestinationPartition: "default", DestinationName: "zap", + Config: map[string]interface{}{ + "passive_health_check": map[string]interface{}{ + "Interval": int64(20), + "MaxFailures": int64(4), + }, + }, }, }, }, @@ -171,8 +178,8 @@ func Test_MergeServiceConfig_UpstreamOverrides(t *testing.T) { DestinationName: "zap", Config: map[string]interface{}{ "passive_health_check": map[string]interface{}{ - "Interval": int64(10), - "MaxFailures": int64(2), + "Interval": int64(20), + "MaxFailures": int64(4), }, "protocol": "grpc", }, diff --git a/agent/consul/options.go b/agent/consul/options.go index 576009aa0..afc69b51d 100644 --- a/agent/consul/options.go +++ b/agent/consul/options.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/consul-net-rpc/net/rpc" "github.com/hashicorp/consul/agent/consul/stream" + "github.com/hashicorp/consul/agent/grpc-external/limiter" "github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/agent/router" "github.com/hashicorp/consul/agent/rpc/middleware" @@ -15,14 +16,15 @@ import ( ) type Deps struct { - EventPublisher *stream.EventPublisher - Logger hclog.InterceptLogger - TLSConfigurator *tlsutil.Configurator - Tokens *token.Store - Router *router.Router - ConnPool *pool.ConnPool - GRPCConnPool GRPCClientConner - LeaderForwarder LeaderForwarder + EventPublisher *stream.EventPublisher + Logger hclog.InterceptLogger + TLSConfigurator *tlsutil.Configurator + Tokens *token.Store + Router *router.Router + ConnPool *pool.ConnPool + GRPCConnPool GRPCClientConner + LeaderForwarder LeaderForwarder + XDSStreamLimiter *limiter.SessionLimiter // GetNetRPCInterceptorFunc, if not nil, sets the net/rpc rpc.ServerServiceCallInterceptor on // the server side to record metrics around the RPC requests. If nil, no interceptor is added to // the rpc server. diff --git a/agent/consul/peering_backend.go b/agent/consul/peering_backend.go index 0f8b009e9..5b01b9d04 100644 --- a/agent/consul/peering_backend.go +++ b/agent/consul/peering_backend.go @@ -66,11 +66,19 @@ func (b *PeeringBackend) GetServerAddresses() ([]string, error) { } var addrs []string for _, node := range nodes { - grpcPortStr := node.ServiceMeta["grpc_port"] - if v, err := strconv.Atoi(grpcPortStr); err != nil || v < 1 { - continue // skip server that isn't exporting public gRPC properly + // Prefer the TLS port if it is defined. + grpcPortStr := node.ServiceMeta["grpc_tls_port"] + if v, err := strconv.Atoi(grpcPortStr); err == nil && v > 0 { + addrs = append(addrs, node.Address+":"+grpcPortStr) + continue } - addrs = append(addrs, node.Address+":"+grpcPortStr) + // Fallback to the standard port if TLS is not defined. + grpcPortStr = node.ServiceMeta["grpc_port"] + if v, err := strconv.Atoi(grpcPortStr); err == nil && v > 0 { + addrs = append(addrs, node.Address+":"+grpcPortStr) + continue + } + // Skip node if neither defined. } if len(addrs) == 0 { return nil, fmt.Errorf("a grpc bind port must be specified in the configuration for all servers") diff --git a/agent/consul/server.go b/agent/consul/server.go index 1afa74c91..991a4535b 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -39,6 +39,7 @@ import ( "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/consul/usagemetrics" "github.com/hashicorp/consul/agent/consul/wanfed" + "github.com/hashicorp/consul/agent/consul/xdscapacity" aclgrpc "github.com/hashicorp/consul/agent/grpc-external/services/acl" "github.com/hashicorp/consul/agent/grpc-external/services/connectca" "github.com/hashicorp/consul/agent/grpc-external/services/dataplane" @@ -253,7 +254,7 @@ type Server struct { // enable RPC forwarding. externalConnectCAServer *connectca.Server - // externalGRPCServer is the gRPC server exposed on the dedicated gRPC port, as + // externalGRPCServer has a gRPC server exposed on the dedicated gRPC ports, as // opposed to the multiplexed "server" port which is served by grpcHandler. externalGRPCServer *grpc.Server @@ -370,14 +371,17 @@ type Server struct { // peerStreamServer is a server used to handle peering streams from external clusters. peerStreamServer *peerstream.Server + // peeringServer handles peering RPC requests internal to this cluster, like generating peering tokens. - peeringServer *peering.Server - peerStreamTracker *peerstream.Tracker + peeringServer *peering.Server + + // xdsCapacityController controls the number of concurrent xDS streams the + // server is able to handle. + xdsCapacityController *xdscapacity.Controller // embedded struct to hold all the enterprise specific data EnterpriseServer } - type connHandler interface { Run() error Handle(conn net.Conn) @@ -724,11 +728,9 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server) (*Ser Logger: logger.Named("grpc-api.server-discovery"), }).Register(s.externalGRPCServer) - s.peerStreamTracker = peerstream.NewTracker() s.peeringBackend = NewPeeringBackend(s) s.peerStreamServer = peerstream.NewServer(peerstream.Config{ Backend: s.peeringBackend, - Tracker: s.peerStreamTracker, GetStore: func() peerstream.StateStore { return s.FSM().State() }, Logger: logger.Named("grpc-api.peerstream"), ACLResolver: s.ACLResolver, @@ -752,6 +754,13 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server) (*Ser s.grpcLeaderForwarder = flat.LeaderForwarder go s.trackLeaderChanges() + s.xdsCapacityController = xdscapacity.NewController(xdscapacity.Config{ + Logger: s.logger.Named(logging.XDSCapacityController), + GetStore: func() xdscapacity.Store { return s.fsm.State() }, + SessionLimiter: flat.XDSStreamLimiter, + }) + go s.xdsCapacityController.Run(&lib.StopChannelContext{StopCh: s.shutdownCh}) + // Initialize Autopilot. This must happen before starting leadership monitoring // as establishing leadership could attempt to use autopilot and cause a panic. s.initAutopilot(config) @@ -790,7 +799,7 @@ func newGRPCHandlerFromConfig(deps Deps, config *Config, s *Server) connHandler p := peering.NewServer(peering.Config{ Backend: s.peeringBackend, - Tracker: s.peerStreamTracker, + Tracker: s.peerStreamServer.Tracker, Logger: deps.Logger.Named("grpc-api.peering"), ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) { // Only forward the request if the dc in the request matches the server's datacenter. @@ -1361,6 +1370,11 @@ func (s *Server) WANMembers() []serf.Member { return s.serfWAN.Members() } +// GetPeeringBackend is a test helper. +func (s *Server) GetPeeringBackend() peering.Backend { + return s.peeringBackend +} + // RemoveFailedNode is used to remove a failed node from the cluster. func (s *Server) RemoveFailedNode(node string, prune bool, entMeta *acl.EnterpriseMeta) error { var removeFn func(*serf.Serf, string) error @@ -1574,12 +1588,12 @@ func (s *Server) Stats() map[string]map[string]string { // GetLANCoordinate returns the coordinate of the node in the LAN gossip // pool. // -// - Clients return a single coordinate for the single gossip pool they are -// in (default, segment, or partition). +// - Clients return a single coordinate for the single gossip pool they are +// in (default, segment, or partition). // -// - Servers return one coordinate for their canonical gossip pool (i.e. -// default partition/segment) and one per segment they are also ancillary -// members of. +// - Servers return one coordinate for their canonical gossip pool (i.e. +// default partition/segment) and one per segment they are also ancillary +// members of. // // NOTE: servers do not emit coordinates for partitioned gossip pools they // are ancillary members of. diff --git a/agent/consul/server_oss.go b/agent/consul/server_oss.go index 5ae2fc3ea..4ae524b65 100644 --- a/agent/consul/server_oss.go +++ b/agent/consul/server_oss.go @@ -159,3 +159,18 @@ func (s *Server) addEnterpriseStats(stats map[string]map[string]string) { func getSerfMemberEnterpriseMeta(member serf.Member) *acl.EnterpriseMeta { return structs.NodeEnterpriseMetaInDefaultPartition() } + +func addSerfMetricsLabels(conf *serf.Config, wan bool, segment string, partition string, areaID string) { + conf.MetricLabels = []metrics.Label{} + + networkMetric := metrics.Label{ + Name: "network", + } + if wan { + networkMetric.Value = "wan" + } else { + networkMetric.Value = "lan" + } + + conf.MetricLabels = append(conf.MetricLabels, networkMetric) +} diff --git a/agent/consul/server_serf.go b/agent/consul/server_serf.go index 5e29b47dd..80f44aedc 100644 --- a/agent/consul/server_serf.go +++ b/agent/consul/server_serf.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" "github.com/hashicorp/memberlist" "github.com/hashicorp/raft" @@ -106,6 +107,9 @@ func (s *Server) setupSerfConfig(opts setupSerfOptions) (*serf.Config, error) { if s.config.GRPCPort > 0 { conf.Tags["grpc_port"] = fmt.Sprintf("%d", s.config.GRPCPort) } + if s.config.GRPCTLSPort > 0 { + conf.Tags["grpc_tls_port"] = fmt.Sprintf("%d", s.config.GRPCTLSPort) + } if s.config.Bootstrap { conf.Tags["bootstrap"] = "1" } @@ -177,9 +181,10 @@ func (s *Server) setupSerfConfig(opts setupSerfOptions) (*serf.Config, error) { if opts.WAN { nt, err := memberlist.NewNetTransport(&memberlist.NetTransportConfig{ - BindAddrs: []string{conf.MemberlistConfig.BindAddr}, - BindPort: conf.MemberlistConfig.BindPort, - Logger: conf.MemberlistConfig.Logger, + BindAddrs: []string{conf.MemberlistConfig.BindAddr}, + BindPort: conf.MemberlistConfig.BindPort, + Logger: conf.MemberlistConfig.Logger, + MetricLabels: []metrics.Label{{Name: "network", Value: "wan"}}, }) if err != nil { return nil, err @@ -230,6 +235,8 @@ func (s *Server) setupSerfConfig(opts setupSerfOptions) (*serf.Config, error) { conf.ReconnectTimeoutOverride = libserf.NewReconnectOverride(s.logger) + addSerfMetricsLabels(conf, opts.WAN, opts.Segment, s.config.AgentEnterpriseMeta().PartitionOrDefault(), "") + addEnterpriseSerfTags(conf.Tags, s.config.AgentEnterpriseMeta()) if s.config.OverrideInitialSerfTags != nil { diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index 35bbe720e..5a3a2dea1 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/tls" "crypto/x509" "fmt" "net" @@ -218,9 +219,10 @@ func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (string, *S var dir string var srv *Server + var config *Config + var deps Deps // Retry added to avoid cases where bind addr is already in use retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) { - var config *Config dir, config = testServerConfig(t) for _, fn := range configOpts { fn(config) @@ -234,7 +236,8 @@ func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (string, *S config.ACLResolverSettings.EnterpriseMeta = *config.AgentEnterpriseMeta() var err error - srv, err = newServer(t, config) + deps = newDefaultDeps(t, config) + srv, err = newServerWithDeps(t, config, deps) if err != nil { r.Fatalf("err: %v", err) } @@ -245,9 +248,14 @@ func testServerWithConfig(t *testing.T, configOpts ...func(*Config)) (string, *S // Normally the gRPC server listener is created at the agent level and // passed down into the Server creation. externalGRPCAddr := fmt.Sprintf("127.0.0.1:%d", srv.config.GRPCPort) - ln, err := net.Listen("tcp", externalGRPCAddr) require.NoError(t, err) + + // Wrap the listener with TLS + if deps.TLSConfigurator.GRPCServerUseTLS() { + ln = tls.NewListener(ln, deps.TLSConfigurator.IncomingGRPCConfig()) + } + go func() { _ = srv.externalGRPCServer.Serve(ln) }() @@ -300,8 +308,8 @@ func newServerWithDeps(t *testing.T, c *Config, deps Deps) (*Server, error) { oldNotify() } } - - srv, err := NewServer(c, deps, external.NewServer(deps.Logger.Named("grpc.external"), deps.TLSConfigurator)) + grpcServer := external.NewServer(deps.Logger.Named("grpc.external")) + srv, err := NewServer(c, deps, grpcServer) if err != nil { return nil, err } diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 258519d5b..7dad6e36f 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -1134,7 +1134,7 @@ func terminatingGatewayVirtualIPsSupported(tx ReadTxn, ws memdb.WatchSet) (bool, } // Services returns all services along with a list of associated tags. -func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.Services, error) { +func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, []*structs.ServiceNode, error) { tx := s.db.Txn(false) defer tx.Abort() @@ -1148,30 +1148,11 @@ func (s *Store) Services(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerNam } ws.Add(services.WatchCh()) - // Rip through the services and enumerate them and their unique set of - // tags. - unique := make(map[string]map[string]struct{}) + var result []*structs.ServiceNode for service := services.Next(); service != nil; service = services.Next() { - svc := service.(*structs.ServiceNode) - tags, ok := unique[svc.ServiceName] - if !ok { - unique[svc.ServiceName] = make(map[string]struct{}) - tags = unique[svc.ServiceName] - } - for _, tag := range svc.ServiceTags { - tags[tag] = struct{}{} - } + result = append(result, service.(*structs.ServiceNode)) } - - // Generate the output structure. - var results = make(structs.Services) - for service, tags := range unique { - results[service] = make([]string, 0, len(tags)) - for tag := range tags { - results[service] = append(results[service], tag) - } - } - return idx, results, nil + return idx, result, nil } func (s *Store) ServiceList(ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.ServiceList, error) { @@ -1212,7 +1193,7 @@ func serviceListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *acl.EnterpriseMeta, } // ServicesByNodeMeta returns all services, filtered by the given node metadata. -func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.Services, error) { +func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, entMeta *acl.EnterpriseMeta, peerName string) (uint64, []*structs.ServiceNode, error) { tx := s.db.Txn(false) defer tx.Abort() @@ -1259,8 +1240,7 @@ func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, } allServicesCh := allServices.WatchCh() - // Populate the services map - unique := make(map[string]map[string]struct{}) + var result structs.ServiceNodes for node := nodes.Next(); node != nil; node = nodes.Next() { n := node.(*structs.Node) if len(filters) > 1 && !structs.SatisfiesMetaFilters(n.Meta, filters) { @@ -1274,30 +1254,11 @@ func (s *Store) ServicesByNodeMeta(ws memdb.WatchSet, filters map[string]string, } ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh) - // Rip through the services and enumerate them and their unique set of - // tags. for service := services.Next(); service != nil; service = services.Next() { - svc := service.(*structs.ServiceNode) - tags, ok := unique[svc.ServiceName] - if !ok { - unique[svc.ServiceName] = make(map[string]struct{}) - tags = unique[svc.ServiceName] - } - for _, tag := range svc.ServiceTags { - tags[tag] = struct{}{} - } + result = append(result, service.(*structs.ServiceNode)) } } - - // Generate the output structure. - var results = make(structs.Services) - for service, tags := range unique { - results[service] = make([]string, 0, len(tags)) - for tag := range tags { - results[service] = append(results[service], tag) - } - } - return idx, results, nil + return idx, result, nil } // maxIndexForService return the maximum Raft Index for a service @@ -1717,6 +1678,9 @@ func (s *Store) ServiceNode(nodeID, nodeName, serviceID string, entMeta *acl.Ent if err != nil { return 0, nil, fmt.Errorf("failed querying service for node %q: %w", node.Node, err) } + if service != nil { + service.ID = node.ID + } return idx, service, nil } diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index 10e7af6db..d354b9b09 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/consul/acl" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-uuid" "github.com/stretchr/testify/assert" @@ -270,17 +272,20 @@ func TestStateStore_EnsureRegistration(t *testing.T) { require.Equal(t, uint64(2), idx) require.Equal(t, svcmap["redis1"], r) + exp := svcmap["redis1"].ToServiceNode("node1") + exp.ID = nodeID + // lookup service by node name idx, sn, err := s.ServiceNode("", "node1", "redis1", nil, peerName) require.NoError(t, err) require.Equal(t, uint64(2), idx) - require.Equal(t, svcmap["redis1"].ToServiceNode("node1"), sn) + require.Equal(t, exp, sn) // lookup service by node ID idx, sn, err = s.ServiceNode(string(nodeID), "", "redis1", nil, peerName) require.NoError(t, err) require.Equal(t, uint64(2), idx) - require.Equal(t, svcmap["redis1"].ToServiceNode("node1"), sn) + require.Equal(t, exp, sn) // lookup service by invalid node _, _, err = s.ServiceNode("", "invalid-node", "redis1", nil, peerName) @@ -2102,10 +2107,13 @@ func TestStateStore_Services(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns1.EnterpriseMeta.Normalize() if err := s.EnsureService(2, "node1", ns1); err != nil { t.Fatalf("err: %s", err) } - testRegisterService(t, s, 3, "node1", "dogs") + ns1Dogs := testRegisterService(t, s, 3, "node1", "dogs") + ns1Dogs.EnterpriseMeta.Normalize() + testRegisterNode(t, s, 4, "node2") ns2 := &structs.NodeService{ ID: "service3", @@ -2114,6 +2122,7 @@ func TestStateStore_Services(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns2.EnterpriseMeta.Normalize() if err := s.EnsureService(5, "node2", ns2); err != nil { t.Fatalf("err: %s", err) } @@ -2131,19 +2140,13 @@ func TestStateStore_Services(t *testing.T) { t.Fatalf("bad index: %d", idx) } - // Verify the result. We sort the lists since the order is - // non-deterministic (it's built using a map internally). - expected := structs.Services{ - "redis": []string{"prod", "primary", "replica"}, - "dogs": []string{}, - } - sort.Strings(expected["redis"]) - for _, tags := range services { - sort.Strings(tags) - } - if !reflect.DeepEqual(expected, services) { - t.Fatalf("bad: %#v", services) + // Verify the result. + expected := []*structs.ServiceNode{ + ns1Dogs.ToServiceNode("node1"), + ns1.ToServiceNode("node1"), + ns2.ToServiceNode("node2"), } + assertDeepEqual(t, expected, services, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) // Deleting a node with a service should fire the watch. if err := s.DeleteNode(6, "node1", nil, ""); err != nil { @@ -2182,6 +2185,7 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns1.EnterpriseMeta.Normalize() if err := s.EnsureService(2, "node0", ns1); err != nil { t.Fatalf("err: %s", err) } @@ -2192,6 +2196,7 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { Address: "1.1.1.1", Port: 1111, } + ns2.EnterpriseMeta.Normalize() if err := s.EnsureService(3, "node1", ns2); err != nil { t.Fatalf("err: %s", err) } @@ -2206,11 +2211,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod"}, + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get all services using the common meta value", func(t *testing.T) { @@ -2218,11 +2222,12 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod", "replica"}, + require.Len(t, res, 2) + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), + ns2.ToServiceNode("node1"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get an empty list for an invalid meta value", func(t *testing.T) { @@ -2230,8 +2235,8 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{} - require.Equal(t, expected, res) + var expected []*structs.ServiceNode + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Get the first node's service instance using multiple meta filters", func(t *testing.T) { @@ -2239,11 +2244,10 @@ func TestStateStore_ServicesByNodeMeta(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - expected := structs.Services{ - "redis": []string{"primary", "prod"}, + expected := []*structs.ServiceNode{ + ns1.ToServiceNode("node0"), } - sort.Strings(res["redis"]) - require.Equal(t, expected, res) + assertDeepEqual(t, res, expected, cmpopts.IgnoreFields(structs.ServiceNode{}, "RaftIndex")) }) t.Run("Registering some unrelated node + service should not fire the watch.", func(t *testing.T) { @@ -8807,3 +8811,10 @@ func setVirtualIPFlags(t *testing.T, s *Store) { Value: "true", })) } + +func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) { + t.Helper() + if diff := cmp.Diff(x, y, opts...); diff != "" { + t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff) + } +} diff --git a/agent/consul/state/peering.go b/agent/consul/state/peering.go index f56fbe0e1..eef76aa72 100644 --- a/agent/consul/state/peering.go +++ b/agent/consul/state/peering.go @@ -7,12 +7,13 @@ import ( "strings" "github.com/golang/protobuf/proto" + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib/maps" "github.com/hashicorp/consul/proto/pbpeering" - "github.com/hashicorp/go-memdb" ) const ( @@ -534,6 +535,12 @@ func (s *Store) PeeringWrite(idx uint64, req *pbpeering.PeeringWriteRequest) err if req.Peering.Name == "" { return errors.New("Missing Peering Name") } + if req.Peering.State == pbpeering.PeeringState_DELETING && (req.Peering.DeletedAt == nil || structs.IsZeroProtoTime(req.Peering.DeletedAt)) { + return errors.New("Missing deletion time for peering in deleting state") + } + if req.Peering.DeletedAt != nil && !structs.IsZeroProtoTime(req.Peering.DeletedAt) && req.Peering.State != pbpeering.PeeringState_DELETING { + return fmt.Errorf("Unexpected state for peering with deletion time: %s", pbpeering.PeeringStateToAPI(req.Peering.State)) + } // Ensure the name is unique (cannot conflict with another peering with a different ID). _, existing, err := peeringReadTxn(tx, nil, Query{ @@ -545,11 +552,32 @@ func (s *Store) PeeringWrite(idx uint64, req *pbpeering.PeeringWriteRequest) err } if existing != nil { + if req.Peering.ShouldDial() != existing.ShouldDial() { + return fmt.Errorf("Cannot switch peering dialing mode from %t to %t", existing.ShouldDial(), req.Peering.ShouldDial()) + } + if req.Peering.ID != existing.ID { return fmt.Errorf("A peering already exists with the name %q and a different ID %q", req.Peering.Name, existing.ID) } + + // Nothing to do if our peer wants to terminate the peering but the peering is already marked for deletion. + if existing.State == pbpeering.PeeringState_DELETING && req.Peering.State == pbpeering.PeeringState_TERMINATED { + return nil + } + + // No-op deletion + if existing.State == pbpeering.PeeringState_DELETING && req.Peering.State == pbpeering.PeeringState_DELETING { + return nil + } + + // No-op termination + if existing.State == pbpeering.PeeringState_TERMINATED && req.Peering.State == pbpeering.PeeringState_TERMINATED { + return nil + } + // Prevent modifications to Peering marked for deletion. - if !existing.IsActive() { + // This blocks generating new peering tokens or re-establishing the peering until the peering is done deleting. + if existing.State == pbpeering.PeeringState_DELETING { return fmt.Errorf("cannot write to peering that is marked for deletion") } @@ -581,8 +609,8 @@ func (s *Store) PeeringWrite(idx uint64, req *pbpeering.PeeringWriteRequest) err req.Peering.ModifyIndex = idx } - // Ensure associated secrets are cleaned up when a peering is marked for deletion. - if req.Peering.State == pbpeering.PeeringState_DELETING { + // Ensure associated secrets are cleaned up when a peering is marked for deletion or terminated. + if !req.Peering.IsActive() { if err := peeringSecretsDeleteTxn(tx, req.Peering.ID, req.Peering.ShouldDial()); err != nil { return fmt.Errorf("failed to delete peering secrets: %w", err) } @@ -981,7 +1009,7 @@ func peeringsForServiceTxn(tx ReadTxn, ws memdb.WatchSet, serviceName string, en if idx > maxIdx { maxIdx = idx } - if peering == nil || !peering.IsActive() { + if !peering.IsActive() { continue } peerings = append(peerings, peering) diff --git a/agent/consul/state/peering_test.go b/agent/consul/state/peering_test.go index b48e4f80d..a90727f0e 100644 --- a/agent/consul/state/peering_test.go +++ b/agent/consul/state/peering_test.go @@ -950,6 +950,7 @@ func TestStore_Peering_Watch(t *testing.T) { Peering: &pbpeering.Peering{ ID: testFooPeerID, Name: "foo", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, }) @@ -976,6 +977,7 @@ func TestStore_Peering_Watch(t *testing.T) { err := s.PeeringWrite(lastIdx, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{ ID: testBarPeerID, Name: "bar", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, }) @@ -1077,6 +1079,7 @@ func TestStore_PeeringList_Watch(t *testing.T) { Peering: &pbpeering.Peering{ ID: testFooPeerID, Name: "foo", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, @@ -1112,16 +1115,22 @@ func TestStore_PeeringWrite(t *testing.T) { // Each case depends on the previous. s := NewStateStore(nil) + testTime := time.Now() + + type expectations struct { + peering *pbpeering.Peering + secrets *pbpeering.PeeringSecrets + err string + } type testcase struct { - name string - input *pbpeering.PeeringWriteRequest - expectSecrets *pbpeering.PeeringSecrets - expectErr string + name string + input *pbpeering.PeeringWriteRequest + expect expectations } run := func(t *testing.T, tc testcase) { err := s.PeeringWrite(10, tc.input) - if tc.expectErr != "" { - testutil.RequireErrorContains(t, err, tc.expectErr) + if tc.expect.err != "" { + testutil.RequireErrorContains(t, err, tc.expect.err) return } require.NoError(t, err) @@ -1133,52 +1142,176 @@ func TestStore_PeeringWrite(t *testing.T) { _, p, err := s.PeeringRead(nil, q) require.NoError(t, err) require.NotNil(t, p) - require.Equal(t, tc.input.Peering.State, p.State) - require.Equal(t, tc.input.Peering.Name, p.Name) + require.Equal(t, tc.expect.peering.State, p.State) + require.Equal(t, tc.expect.peering.Name, p.Name) + require.Equal(t, tc.expect.peering.Meta, p.Meta) + if tc.expect.peering.DeletedAt != nil { + require.Equal(t, tc.expect.peering.DeletedAt, p.DeletedAt) + } secrets, err := s.PeeringSecretsRead(nil, tc.input.Peering.ID) require.NoError(t, err) - prototest.AssertDeepEqual(t, tc.expectSecrets, secrets) + prototest.AssertDeepEqual(t, tc.expect.secrets, secrets) } tcs := []testcase{ { name: "create baz", input: &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ - ID: testBazPeerID, - Name: "baz", - Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_ESTABLISHING, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, SecretsRequest: &pbpeering.SecretsWriteRequest{ PeerID: testBazPeerID, - Request: &pbpeering.SecretsWriteRequest_GenerateToken{ - GenerateToken: &pbpeering.SecretsWriteRequest_GenerateTokenRequest{ - EstablishmentSecret: testBazSecretID, + Request: &pbpeering.SecretsWriteRequest_Establish{ + Establish: &pbpeering.SecretsWriteRequest_EstablishRequest{ + ActiveStreamSecret: testBazSecretID, }, }, }, }, - expectSecrets: &pbpeering.PeeringSecrets{ - PeerID: testBazPeerID, - Establishment: &pbpeering.PeeringSecrets_Establishment{ - SecretID: testBazSecretID, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_ESTABLISHING, }, + secrets: &pbpeering.PeeringSecrets{ + PeerID: testBazPeerID, + Stream: &pbpeering.PeeringSecrets_Stream{ + ActiveSecretID: testBazSecretID, + }, + }, + }, + }, + { + name: "cannot change ID for baz", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: "123", + Name: "baz", + State: pbpeering.PeeringState_FAILING, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + err: `A peering already exists with the name "baz" and a different ID`, + }, + }, + { + name: "cannot change dialer status for baz", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: "123", + Name: "baz", + State: pbpeering.PeeringState_FAILING, + // Excluding the peer server addresses leads to baz not being considered a dialer. + // PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + err: "Cannot switch peering dialing mode from true to false", }, }, { name: "update baz", input: &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ - ID: testBazPeerID, - Name: "baz", - State: pbpeering.PeeringState_FAILING, - Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_FAILING, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, }, - expectSecrets: &pbpeering.PeeringSecrets{ - PeerID: testBazPeerID, - Establishment: &pbpeering.PeeringSecrets_Establishment{ - SecretID: testBazSecretID, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_FAILING, + }, + secrets: &pbpeering.PeeringSecrets{ + PeerID: testBazPeerID, + Stream: &pbpeering.PeeringSecrets_Stream{ + ActiveSecretID: testBazSecretID, + }, + }, + }, + }, + { + name: "if no state was included in request it is inherited from existing", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + // Send undefined state. + // State: pbpeering.PeeringState_FAILING, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + // Previous failing state is picked up. + State: pbpeering.PeeringState_FAILING, + }, + secrets: &pbpeering.PeeringSecrets{ + PeerID: testBazPeerID, + Stream: &pbpeering.PeeringSecrets_Stream{ + ActiveSecretID: testBazSecretID, + }, + }, + }, + }, + { + name: "mark baz as terminated", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_TERMINATED, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_TERMINATED, + }, + // Secrets for baz should have been deleted + secrets: nil, + }, + }, + { + name: "cannot modify peering during no-op termination", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_TERMINATED, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + PeerServerAddresses: []string{"localhost:8502"}, + + // Attempt to add metadata + Meta: map[string]string{"foo": "bar"}, + }, + }, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_TERMINATED, + // Meta should be unchanged. + Meta: nil, }, }, }, @@ -1186,42 +1319,104 @@ func TestStore_PeeringWrite(t *testing.T) { name: "mark baz for deletion", input: &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_DELETING, + PeerServerAddresses: []string{"localhost:8502"}, + DeletedAt: structs.TimeToProto(testTime), + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + peering: &pbpeering.Peering{ ID: testBazPeerID, Name: "baz", State: pbpeering.PeeringState_DELETING, - DeletedAt: structs.TimeToProto(time.Now()), - Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + DeletedAt: structs.TimeToProto(testTime), + }, + secrets: nil, + }, + }, + { + name: "deleting a deleted peering is a no-op", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_DELETING, + PeerServerAddresses: []string{"localhost:8502"}, + DeletedAt: structs.TimeToProto(time.Now()), + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, }, - // Secrets for baz should have been deleted - expectSecrets: nil, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + // Still marked as deleting at the original testTime + State: pbpeering.PeeringState_DELETING, + DeletedAt: structs.TimeToProto(testTime), + }, + // Secrets for baz should have been deleted + secrets: nil, + }, + }, + { + name: "terminating a peering marked for deletion is a no-op", + input: &pbpeering.PeeringWriteRequest{ + Peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + State: pbpeering.PeeringState_TERMINATED, + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + }, + }, + expect: expectations{ + peering: &pbpeering.Peering{ + ID: testBazPeerID, + Name: "baz", + // Still marked as deleting + State: pbpeering.PeeringState_DELETING, + }, + // Secrets for baz should have been deleted + secrets: nil, + }, }, { name: "cannot update peering marked for deletion", input: &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ - ID: testBazPeerID, - Name: "baz", + ID: testBazPeerID, + Name: "baz", + PeerServerAddresses: []string{"localhost:8502"}, + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + // Attempt to add metadata Meta: map[string]string{ "source": "kubernetes", }, - Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, }, - expectErr: "cannot write to peering that is marked for deletion", + expect: expectations{ + err: "cannot write to peering that is marked for deletion", + }, }, { name: "cannot create peering marked for deletion", input: &pbpeering.PeeringWriteRequest{ Peering: &pbpeering.Peering{ - ID: testFooPeerID, - Name: "foo", - DeletedAt: structs.TimeToProto(time.Now()), - Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), + ID: testFooPeerID, + Name: "foo", + PeerServerAddresses: []string{"localhost:8502"}, + State: pbpeering.PeeringState_DELETING, + DeletedAt: structs.TimeToProto(time.Now()), + Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(), }, }, - expectErr: "cannot create a new peering marked for deletion", + expect: expectations{ + err: "cannot create a new peering marked for deletion", + }, }, } for _, tc := range tcs { @@ -1246,6 +1441,7 @@ func TestStore_PeeringDelete(t *testing.T) { Peering: &pbpeering.Peering{ ID: testFooPeerID, Name: "foo", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, })) @@ -1461,7 +1657,13 @@ func TestStateStore_ExportedServicesForPeer(t *testing.T) { } newTarget := func(service, serviceSubset, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, "default", "default", datacenter) + t := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: service, + ServiceSubset: serviceSubset, + Partition: "default", + Namespace: "default", + Datacenter: datacenter, + }) t.SNI = connect.TargetSNI(t, connect.TestTrustDomain) t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default @@ -1753,6 +1955,7 @@ func TestStateStore_PeeringsForService(t *testing.T) { copied := pbpeering.Peering{ ID: tp.peering.ID, Name: tp.peering.Name, + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), } require.NoError(t, s.PeeringWrite(lastIdx, &pbpeering.PeeringWriteRequest{Peering: &copied})) @@ -2195,6 +2398,7 @@ func TestStore_TrustBundleListByService(t *testing.T) { Peering: &pbpeering.Peering{ ID: peerID1, Name: "peer1", + State: pbpeering.PeeringState_DELETING, DeletedAt: structs.TimeToProto(time.Now()), }, })) diff --git a/agent/consul/state/state_store_test.go b/agent/consul/state/state_store_test.go index c8460ca82..88e5418c8 100644 --- a/agent/consul/state/state_store_test.go +++ b/agent/consul/state/state_store_test.go @@ -146,13 +146,13 @@ func testRegisterServiceOpts(t *testing.T, s *Store, idx uint64, nodeID, service // testRegisterServiceWithChange registers a service and allow ensuring the consul index is updated // even if service already exists if using `modifyAccordingIndex`. // This is done by setting the transaction ID in "version" meta so service will be updated if it already exists -func testRegisterServiceWithChange(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool) { - testRegisterServiceWithChangeOpts(t, s, idx, nodeID, serviceID, modifyAccordingIndex) +func testRegisterServiceWithChange(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool) *structs.NodeService { + return testRegisterServiceWithChangeOpts(t, s, idx, nodeID, serviceID, modifyAccordingIndex) } // testRegisterServiceWithChangeOpts is the same as testRegisterServiceWithChange with the addition of opts that can // modify the service prior to writing. -func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool, opts ...func(service *structs.NodeService)) { +func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, modifyAccordingIndex bool, opts ...func(service *structs.NodeService)) *structs.NodeService { meta := make(map[string]string) if modifyAccordingIndex { meta["version"] = fmt.Sprint(idx) @@ -183,14 +183,15 @@ func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeI result.ServiceID != serviceID { t.Fatalf("bad service: %#v", result) } + return svc } // testRegisterService register a service with given transaction idx // If the service already exists, transaction number might not be increased // Use `testRegisterServiceWithChange()` if you want perform a registration that // ensures the transaction is updated by setting idx in Meta of Service -func testRegisterService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) { - testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false) +func testRegisterService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) *structs.NodeService { + return testRegisterServiceWithChange(t, s, idx, nodeID, serviceID, false) } func testRegisterConnectService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) { diff --git a/agent/consul/state/txn.go b/agent/consul/state/txn.go index 087bb4fe8..af6e98995 100644 --- a/agent/consul/state/txn.go +++ b/agent/consul/state/txn.go @@ -60,6 +60,13 @@ func (s *Store) txnKVS(tx WriteTxn, idx uint64, op *structs.TxnKVOp) (structs.Tx err = fmt.Errorf("key %q doesn't exist", op.DirEnt.Key) } + case api.KVGetOrEmpty: + _, entry, err = kvsGetTxn(tx, nil, op.DirEnt.Key, op.DirEnt.EnterpriseMeta) + if entry == nil && err == nil { + entry = &op.DirEnt + entry.Value = nil + } + case api.KVGetTree: var entries structs.DirEntries _, entries, err = s.kvsListTxn(tx, nil, op.DirEnt.Key, op.DirEnt.EnterpriseMeta) @@ -95,7 +102,7 @@ func (s *Store) txnKVS(tx WriteTxn, idx uint64, op *structs.TxnKVOp) (structs.Tx // value (we have to clone so we don't modify the entry being used by // the state store). if entry != nil { - if op.Verb == api.KVGet { + if op.Verb == api.KVGet || op.Verb == api.KVGetOrEmpty { result := structs.TxnResult{KV: entry} return structs.TxnResults{&result}, nil } diff --git a/agent/consul/state/txn_test.go b/agent/consul/state/txn_test.go index f98325df3..a7694089b 100644 --- a/agent/consul/state/txn_test.go +++ b/agent/consul/state/txn_test.go @@ -577,6 +577,22 @@ func TestStateStore_Txn_KVS(t *testing.T) { }, }, }, + &structs.TxnOp{ + KV: &structs.TxnKVOp{ + Verb: api.KVGetOrEmpty, + DirEnt: structs.DirEntry{ + Key: "foo/update", + }, + }, + }, + &structs.TxnOp{ + KV: &structs.TxnKVOp{ + Verb: api.KVGetOrEmpty, + DirEnt: structs.DirEntry{ + Key: "foo/not-exists", + }, + }, + }, &structs.TxnOp{ KV: &structs.TxnKVOp{ Verb: api.KVCheckIndex, @@ -702,6 +718,22 @@ func TestStateStore_Txn_KVS(t *testing.T) { }, }, }, + &structs.TxnResult{ + KV: &structs.DirEntry{ + Key: "foo/update", + Value: []byte("stale"), + RaftIndex: structs.RaftIndex{ + CreateIndex: 5, + ModifyIndex: 5, + }, + }, + }, + &structs.TxnResult{ + KV: &structs.DirEntry{ + Key: "foo/not-exists", + Value: nil, + }, + }, &structs.TxnResult{ KV: &structs.DirEntry{ diff --git a/agent/consul/state/usage.go b/agent/consul/state/usage.go index 86b0d80ee..cfb38232a 100644 --- a/agent/consul/state/usage.go +++ b/agent/consul/state/usage.go @@ -325,7 +325,7 @@ func (s *Store) NodeUsage() (uint64, NodeUsage, error) { tx := s.db.ReadTxn() defer tx.Abort() - nodes, err := firstUsageEntry(tx, tableNodes) + nodes, err := firstUsageEntry(nil, tx, tableNodes) if err != nil { return 0, NodeUsage{}, fmt.Errorf("failed nodes lookup: %s", err) } @@ -347,7 +347,7 @@ func (s *Store) PeeringUsage() (uint64, PeeringUsage, error) { tx := s.db.ReadTxn() defer tx.Abort() - peerings, err := firstUsageEntry(tx, tablePeering) + peerings, err := firstUsageEntry(nil, tx, tablePeering) if err != nil { return 0, PeeringUsage{}, fmt.Errorf("failed peerings lookup: %s", err) } @@ -365,23 +365,23 @@ func (s *Store) PeeringUsage() (uint64, PeeringUsage, error) { // ServiceUsage returns the latest seen Raft index, a compiled set of service // usage data, and any errors. -func (s *Store) ServiceUsage() (uint64, ServiceUsage, error) { +func (s *Store) ServiceUsage(ws memdb.WatchSet) (uint64, ServiceUsage, error) { tx := s.db.ReadTxn() defer tx.Abort() - serviceInstances, err := firstUsageEntry(tx, tableServices) + serviceInstances, err := firstUsageEntry(ws, tx, tableServices) if err != nil { return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err) } - services, err := firstUsageEntry(tx, serviceNamesUsageTable) + services, err := firstUsageEntry(ws, tx, serviceNamesUsageTable) if err != nil { return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err) } serviceKindInstances := make(map[string]int) for _, kind := range allConnectKind { - usage, err := firstUsageEntry(tx, connectUsageTableName(kind)) + usage, err := firstUsageEntry(ws, tx, connectUsageTableName(kind)) if err != nil { return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err) } @@ -393,7 +393,7 @@ func (s *Store) ServiceUsage() (uint64, ServiceUsage, error) { Services: services.Count, ConnectServiceInstances: serviceKindInstances, } - results, err := compileEnterpriseServiceUsage(tx, usage) + results, err := compileEnterpriseServiceUsage(ws, tx, usage) if err != nil { return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err) } @@ -405,7 +405,7 @@ func (s *Store) KVUsage() (uint64, KVUsage, error) { tx := s.db.ReadTxn() defer tx.Abort() - kvs, err := firstUsageEntry(tx, "kvs") + kvs, err := firstUsageEntry(nil, tx, "kvs") if err != nil { return 0, KVUsage{}, fmt.Errorf("failed kvs lookup: %s", err) } @@ -428,7 +428,7 @@ func (s *Store) ConfigEntryUsage() (uint64, ConfigEntryUsage, error) { configEntries := make(map[string]int) var maxIdx uint64 for _, kind := range structs.AllConfigEntryKinds { - configEntry, err := firstUsageEntry(tx, configEntryUsageTableName(kind)) + configEntry, err := firstUsageEntry(nil, tx, configEntryUsageTableName(kind)) if configEntry.Index > maxIdx { maxIdx = configEntry.Index } @@ -448,11 +448,12 @@ func (s *Store) ConfigEntryUsage() (uint64, ConfigEntryUsage, error) { return maxIdx, results, nil } -func firstUsageEntry(tx ReadTxn, id string) (*UsageEntry, error) { - usage, err := tx.First(tableUsage, indexID, id) +func firstUsageEntry(ws memdb.WatchSet, tx ReadTxn, id string) (*UsageEntry, error) { + watch, usage, err := tx.FirstWatch(tableUsage, indexID, id) if err != nil { return nil, err } + ws.Add(watch) // If no elements have been inserted, the usage entry will not exist. We // return a valid value so that can be certain the return value is not nil diff --git a/agent/consul/state/usage_oss.go b/agent/consul/state/usage_oss.go index edea0e81f..a9b4d1c2f 100644 --- a/agent/consul/state/usage_oss.go +++ b/agent/consul/state/usage_oss.go @@ -29,7 +29,7 @@ func addEnterpriseKVUsage(map[string]int, memdb.Change) {} func addEnterpriseConfigEntryUsage(map[string]int, memdb.Change) {} -func compileEnterpriseServiceUsage(tx ReadTxn, usage ServiceUsage) (ServiceUsage, error) { +func compileEnterpriseServiceUsage(ws memdb.WatchSet, tx ReadTxn, usage ServiceUsage) (ServiceUsage, error) { return usage, nil } diff --git a/agent/consul/state/usage_test.go b/agent/consul/state/usage_test.go index 223e1b062..5c69cc2d5 100644 --- a/agent/consul/state/usage_test.go +++ b/agent/consul/state/usage_test.go @@ -1,7 +1,9 @@ package state import ( + "context" "testing" + "time" memdb "github.com/hashicorp/go-memdb" "github.com/stretchr/testify/require" @@ -150,7 +152,7 @@ func TestStateStore_Usage_ServiceUsageEmpty(t *testing.T) { s := testStateStore(t) // No services have been registered, and thus no usage entry exists - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(0)) require.Equal(t, usage.Services, 0) @@ -174,13 +176,22 @@ func TestStateStore_Usage_ServiceUsage(t *testing.T) { testRegisterConnectNativeService(t, s, 14, "node2", "service-native") testRegisterConnectNativeService(t, s, 15, "node2", "service-native-1") - idx, usage, err := s.ServiceUsage() + ws := memdb.NewWatchSet() + idx, usage, err := s.ServiceUsage(ws) require.NoError(t, err) require.Equal(t, idx, uint64(15)) require.Equal(t, 5, usage.Services) require.Equal(t, 8, usage.ServiceInstances) require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)]) require.Equal(t, 3, usage.ConnectServiceInstances[connectNativeInstancesTable]) + + testRegisterSidecarProxy(t, s, 16, "node2", "service2") + + select { + case <-ws.WatchCh(context.Background()): + case <-time.After(100 * time.Millisecond): + t.Fatal("timeout waiting on WatchSet") + } } func TestStateStore_Usage_ServiceUsage_DeleteNode(t *testing.T) { @@ -207,7 +218,7 @@ func TestStateStore_Usage_ServiceUsage_DeleteNode(t *testing.T) { testRegisterSidecarProxy(t, s, 3, "node1", "service2") testRegisterConnectNativeService(t, s, 4, "node1", "service-connect") - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(4)) require.Equal(t, 3, usage.Services) @@ -217,7 +228,7 @@ func TestStateStore_Usage_ServiceUsage_DeleteNode(t *testing.T) { require.NoError(t, s.DeleteNode(4, "node1", nil, "")) - idx, usage, err = s.ServiceUsage() + idx, usage, err = s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(4)) require.Equal(t, usage.Services, 0) @@ -245,7 +256,7 @@ func TestStateStore_Usage_ServiceUsagePeering(t *testing.T) { testRegisterConnectNativeService(t, s, 7, "node2", "service-native") testutil.RunStep(t, "writes", func(t *testing.T) { - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, uint64(7), idx) require.Equal(t, 3, usage.Services) @@ -257,7 +268,7 @@ func TestStateStore_Usage_ServiceUsagePeering(t *testing.T) { testutil.RunStep(t, "deletes", func(t *testing.T) { require.NoError(t, s.DeleteNode(7, "node1", nil, peerName)) require.NoError(t, s.DeleteNode(8, "node2", nil, "")) - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, uint64(8), idx) require.Equal(t, 0, usage.Services) @@ -295,7 +306,7 @@ func TestStateStore_Usage_Restore(t *testing.T) { require.Equal(t, idx, uint64(9)) require.Equal(t, nodeUsage.Nodes, 1) - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(9)) require.Equal(t, usage.Services, 1) @@ -395,7 +406,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) { require.NoError(t, s.EnsureService(2, "node1", svc)) // We renamed a service with a single instance, so we maintain 1 service. - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(2)) require.Equal(t, usage.Services, 1) @@ -415,7 +426,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) { require.NoError(t, s.EnsureService(3, "node1", svc)) // We renamed a service with a single instance, so we maintain 1 service. - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(3)) require.Equal(t, usage.Services, 1) @@ -436,7 +447,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) { require.NoError(t, s.EnsureService(4, "node1", svc)) // We renamed a service with a single instance, so we maintain 1 service. - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(4)) require.Equal(t, usage.Services, 1) @@ -467,7 +478,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) { } require.NoError(t, s.EnsureService(6, "node1", svc3)) - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(6)) require.Equal(t, usage.Services, 2) @@ -485,7 +496,7 @@ func TestStateStore_Usage_ServiceUsage_updatingService(t *testing.T) { } require.NoError(t, s.EnsureService(7, "node1", update)) - idx, usage, err = s.ServiceUsage() + idx, usage, err = s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(7)) require.Equal(t, usage.Services, 3) @@ -511,7 +522,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) { require.NoError(t, s.EnsureService(2, "node1", svc)) // We renamed a service with a single instance, so we maintain 1 service. - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(2)) require.Equal(t, usage.Services, 1) @@ -537,7 +548,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) { } require.NoError(t, s.EnsureService(4, "node1", svc3)) - idx, usage, err := s.ServiceUsage() + idx, usage, err := s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(4)) require.Equal(t, usage.Services, 2) @@ -552,7 +563,7 @@ func TestStateStore_Usage_ServiceUsage_updatingConnectProxy(t *testing.T) { } require.NoError(t, s.EnsureService(5, "node1", update)) - idx, usage, err = s.ServiceUsage() + idx, usage, err = s.ServiceUsage(nil) require.NoError(t, err) require.Equal(t, idx, uint64(5)) require.Equal(t, usage.Services, 3) diff --git a/agent/consul/usagemetrics/usagemetrics.go b/agent/consul/usagemetrics/usagemetrics.go index 0983cecd8..de15f497b 100644 --- a/agent/consul/usagemetrics/usagemetrics.go +++ b/agent/consul/usagemetrics/usagemetrics.go @@ -18,38 +18,74 @@ import ( var Gauges = []prometheus.GaugeDefinition{ { Name: []string{"consul", "state", "nodes"}, + Help: "Deprecated - please use state_nodes instead.", + }, + { + Name: []string{"state", "nodes"}, Help: "Measures the current number of nodes registered with Consul. It is only emitted by Consul servers. Added in v1.9.0.", }, { Name: []string{"consul", "state", "peerings"}, + Help: "Deprecated - please use state_peerings instead.", + }, + { + Name: []string{"state", "peerings"}, Help: "Measures the current number of peerings registered with Consul. It is only emitted by Consul servers. Added in v1.13.0.", }, { Name: []string{"consul", "state", "services"}, + Help: "Deprecated - please use state_services instead.", + }, + { + Name: []string{"state", "services"}, Help: "Measures the current number of unique services registered with Consul, based on service name. It is only emitted by Consul servers. Added in v1.9.0.", }, { Name: []string{"consul", "state", "service_instances"}, + Help: "Deprecated - please use state_service_instances instead.", + }, + { + Name: []string{"state", "service_instances"}, Help: "Measures the current number of unique services registered with Consul, based on service name. It is only emitted by Consul servers. Added in v1.9.0.", }, { Name: []string{"consul", "members", "clients"}, + Help: "Deprecated - please use members_clients instead.", + }, + { + Name: []string{"members", "clients"}, Help: "Measures the current number of client agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6.", }, { Name: []string{"consul", "members", "servers"}, + Help: "Deprecated - please use members_servers instead.", + }, + { + Name: []string{"members", "servers"}, Help: "Measures the current number of server agents registered with Consul. It is only emitted by Consul servers. Added in v1.9.6.", }, { - Name: []string{"consul", "kv", "entries"}, - Help: "Measures the current number of server agents registered with Consul. It is only emitted by Consul servers. Added in v1.10.3.", + Name: []string{"consul", "state", "kv_entries"}, + Help: "Deprecated - please use kv_entries instead.", + }, + { + Name: []string{"state", "kv_entries"}, + Help: "Measures the current number of entries in the Consul KV store. It is only emitted by Consul servers. Added in v1.10.3.", }, { Name: []string{"consul", "state", "connect_instances"}, + Help: "Deprecated - please use state_connect_instances instead.", + }, + { + Name: []string{"state", "connect_instances"}, Help: "Measures the current number of unique connect service instances registered with Consul, labeled by Kind. It is only emitted by Consul servers. Added in v1.10.4.", }, { Name: []string{"consul", "state", "config_entries"}, + Help: "Deprecated - please use state_config_entries instead.", + }, + { + Name: []string{"state", "config_entries"}, Help: "Measures the current number of unique configuration entries registered with Consul, labeled by Kind. It is only emitted by Consul servers. Added in v1.10.4.", }, } @@ -178,7 +214,7 @@ func (u *UsageMetricsReporter) runOnce() { u.emitPeeringUsage(peeringUsage) - _, serviceUsage, err := state.ServiceUsage() + _, serviceUsage, err := state.ServiceUsage(nil) if err != nil { u.logger.Warn("failed to retrieve services from state store", "error", err) } diff --git a/agent/consul/usagemetrics/usagemetrics_oss.go b/agent/consul/usagemetrics/usagemetrics_oss.go index 2aa35870c..6330707c1 100644 --- a/agent/consul/usagemetrics/usagemetrics_oss.go +++ b/agent/consul/usagemetrics/usagemetrics_oss.go @@ -17,6 +17,11 @@ func (u *UsageMetricsReporter) emitNodeUsage(nodeUsage state.NodeUsage) { float32(nodeUsage.Nodes), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"state", "nodes"}, + float32(nodeUsage.Nodes), + u.metricLabels, + ) } func (u *UsageMetricsReporter) emitPeeringUsage(peeringUsage state.PeeringUsage) { @@ -25,6 +30,11 @@ func (u *UsageMetricsReporter) emitPeeringUsage(peeringUsage state.PeeringUsage) float32(peeringUsage.Peerings), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"state", "peerings"}, + float32(peeringUsage.Peerings), + u.metricLabels, + ) } func (u *UsageMetricsReporter) emitMemberUsage(members []serf.Member) { @@ -46,12 +56,22 @@ func (u *UsageMetricsReporter) emitMemberUsage(members []serf.Member) { float32(clients), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"members", "clients"}, + float32(clients), + u.metricLabels, + ) metrics.SetGaugeWithLabels( []string{"consul", "members", "servers"}, float32(servers), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"members", "servers"}, + float32(servers), + u.metricLabels, + ) } func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage state.ServiceUsage) { @@ -60,12 +80,22 @@ func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage state.ServiceUsage) float32(serviceUsage.Services), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"state", "services"}, + float32(serviceUsage.Services), + u.metricLabels, + ) metrics.SetGaugeWithLabels( []string{"consul", "state", "service_instances"}, float32(serviceUsage.ServiceInstances), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"state", "service_instances"}, + float32(serviceUsage.ServiceInstances), + u.metricLabels, + ) for k, i := range serviceUsage.ConnectServiceInstances { metrics.SetGaugeWithLabels( @@ -73,6 +103,11 @@ func (u *UsageMetricsReporter) emitServiceUsage(serviceUsage state.ServiceUsage) float32(i), append(u.metricLabels, metrics.Label{Name: "kind", Value: k}), ) + metrics.SetGaugeWithLabels( + []string{"state", "connect_instances"}, + float32(i), + append(u.metricLabels, metrics.Label{Name: "kind", Value: k}), + ) } } @@ -82,6 +117,11 @@ func (u *UsageMetricsReporter) emitKVUsage(kvUsage state.KVUsage) { float32(kvUsage.KVCount), u.metricLabels, ) + metrics.SetGaugeWithLabels( + []string{"state", "kv_entries"}, + float32(kvUsage.KVCount), + u.metricLabels, + ) } func (u *UsageMetricsReporter) emitConfigEntryUsage(configUsage state.ConfigEntryUsage) { @@ -91,5 +131,10 @@ func (u *UsageMetricsReporter) emitConfigEntryUsage(configUsage state.ConfigEntr float32(i), append(u.metricLabels, metrics.Label{Name: "kind", Value: k}), ) + metrics.SetGaugeWithLabels( + []string{"state", "config_entries"}, + float32(i), + append(u.metricLabels, metrics.Label{Name: "kind", Value: k}), + ) } } diff --git a/agent/consul/usagemetrics/usagemetrics_oss_test.go b/agent/consul/usagemetrics/usagemetrics_oss_test.go index c860e5b74..add26bca6 100644 --- a/agent/consul/usagemetrics/usagemetrics_oss_test.go +++ b/agent/consul/usagemetrics/usagemetrics_oss_test.go @@ -8,10 +8,11 @@ import ( "time" "github.com/armon/go-metrics" - uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/serf/serf" "github.com/stretchr/testify/require" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" @@ -23,371 +24,678 @@ func newStateStore() (*state.Store, error) { return state.NewStateStore(nil), nil } +type testCase struct { + modfiyStateStore func(t *testing.T, s *state.Store) + getMembersFunc getMembersFunc + expectedGauges map[string]metrics.GaugeValue +} + +var baseCases = map[string]testCase{ + "empty-state": { + expectedGauges: map[string]metrics.GaugeValue{ + // --- node --- + "consul.usage.test.consul.state.nodes;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.nodes;datacenter=dc1": { + Name: "consul.usage.test.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- peering --- + "consul.usage.test.consul.state.peerings;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.peerings;datacenter=dc1": { + Name: "consul.usage.test.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- member --- + "consul.usage.test.consul.members.clients;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.members.clients;datacenter=dc1": { + Name: "consul.usage.test.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.members.servers;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.members.servers", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.members.servers;datacenter=dc1": { + Name: "consul.usage.test.members.servers", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service --- + "consul.usage.test.consul.state.services;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.services;datacenter=dc1": { + Name: "consul.usage.test.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.state.service_instances;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.service_instances;datacenter=dc1": { + Name: "consul.usage.test.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service mesh --- + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-proxy": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-native": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + // --- kv --- + "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.kv_entries;datacenter=dc1": { + Name: "consul.usage.test.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- config entries --- + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-intentions": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-resolver": { // Legacy + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-router": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-defaults": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-splitter": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=mesh": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=proxy-defaults": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=exported-services": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + }, + getMembersFunc: func() []serf.Member { return []serf.Member{} }, + }, + "nodes": { + modfiyStateStore: func(t *testing.T, s *state.Store) { + require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) + require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) + }, + getMembersFunc: func() []serf.Member { + return []serf.Member{ + { + Name: "foo", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + { + Name: "bar", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + } + }, + expectedGauges: map[string]metrics.GaugeValue{ + // --- node --- + "consul.usage.test.consul.state.nodes;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.nodes", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.nodes;datacenter=dc1": { + Name: "consul.usage.test.state.nodes", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- peering --- + "consul.usage.test.consul.state.peerings;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.peerings;datacenter=dc1": { + Name: "consul.usage.test.state.peerings", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- member --- + "consul.usage.test.consul.members.servers;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.members.servers", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.members.servers;datacenter=dc1": { + Name: "consul.usage.test.members.servers", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.members.clients;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.members.clients;datacenter=dc1": { + Name: "consul.usage.test.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service --- + "consul.usage.test.consul.state.services;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.services;datacenter=dc1": { + Name: "consul.usage.test.state.services", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.consul.state.service_instances;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.service_instances;datacenter=dc1": { + Name: "consul.usage.test.state.service_instances", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- service mesh --- + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-proxy": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + }, + "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + "consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-native": { + Name: "consul.usage.test.state.connect_instances", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + }, + // --- kv --- + "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { // Legacy + Name: "consul.usage.test.consul.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + "consul.usage.test.state.kv_entries;datacenter=dc1": { + Name: "consul.usage.test.state.kv_entries", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + }, + // --- config entries --- + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-intentions": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-intentions"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-resolver": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-resolver"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-router": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-router"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-defaults": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=ingress-gateway": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=service-splitter": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "service-splitter"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=mesh": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=proxy-defaults": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "proxy-defaults"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=terminating-gateway": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + }, + "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + "consul.usage.test.state.config_entries;datacenter=dc1;kind=exported-services": { + Name: "consul.usage.test.state.config_entries", + Value: 0, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "exported-services"}, + }, + }, + }, + }, +} + func TestUsageReporter_emitNodeUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue - } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 1, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, - } + cases := baseCases for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -426,371 +734,72 @@ func TestUsageReporter_emitNodeUsage_OSS(t *testing.T) { } func TestUsageReporter_emitPeeringUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "peerings": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - id, err := uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(1, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "foo", ID: id}})) - id, err = uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(2, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "bar", ID: id}})) - id, err = uuid.GenerateUUID() - require.NoError(t, err) - require.NoError(t, s.PeeringWrite(3, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "baz", ID: id}})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, + peeringsCase := cases["nodes"] + peeringsCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(1, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "foo", ID: id}})) + id, err = uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(2, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "bar", ID: id}})) + id, err = uuid.GenerateUUID() + require.NoError(t, err) + require.NoError(t, s.PeeringWrite(3, &pbpeering.PeeringWriteRequest{Peering: &pbpeering.Peering{Name: "baz", ID: id}})) } + peeringsCase.getMembersFunc = func() []serf.Member { + return []serf.Member{ + { + Name: "foo", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + { + Name: "bar", + Tags: map[string]string{"role": "consul"}, + Status: serf.StatusAlive, + }, + } + } + peeringsCase.expectedGauges["consul.usage.test.consul.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.nodes", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.consul.state.peerings;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.peerings", + Value: 3, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.state.peerings;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.peerings", + Value: 3, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.consul.members.clients;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + peeringsCase.expectedGauges["consul.usage.test.members.clients;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.members.clients", + Value: 0, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + cases["peerings"] = peeringsCase + delete(cases, "nodes") for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -829,420 +838,202 @@ func TestUsageReporter_emitPeeringUsage_OSS(t *testing.T) { } func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes-and-services": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(4, &structs.Node{Node: "qux", Address: "127.0.0.3"})) - mgw := structs.TestNodeServiceMeshGateway(t) - mgw.ID = "mesh-gateway" + nodesAndSvcsCase := cases["nodes"] + nodesAndSvcsCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) + require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) + require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) + require.NoError(t, s.EnsureNode(4, &structs.Node{Node: "qux", Address: "127.0.0.3"})) - tgw := structs.TestNodeServiceTerminatingGateway(t, "1.1.1.1") - tgw.ID = "terminating-gateway" - // Typical services and some consul services spread across two nodes - require.NoError(t, s.EnsureService(5, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: nil, Address: "", Port: 5000})) - require.NoError(t, s.EnsureService(6, "bar", &structs.NodeService{ID: "api", Service: "api", Tags: nil, Address: "", Port: 5000})) - require.NoError(t, s.EnsureService(7, "foo", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) - require.NoError(t, s.EnsureService(8, "bar", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) - require.NoError(t, s.EnsureService(9, "foo", &structs.NodeService{ID: "db-connect-proxy", Service: "db-connect-proxy", Tags: nil, Address: "", Port: 5000, Kind: structs.ServiceKindConnectProxy})) - require.NoError(t, s.EnsureRegistration(10, structs.TestRegisterIngressGateway(t))) - require.NoError(t, s.EnsureService(11, "foo", mgw)) - require.NoError(t, s.EnsureService(12, "foo", tgw)) - require.NoError(t, s.EnsureService(13, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}})) - require.NoError(t, s.EnsureConfigEntry(14, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "foo", - })) - require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "bar", - })) - require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{ - Kind: structs.IngressGateway, - Name: "baz", - })) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node", "segment": "a"}, - Status: serf.StatusAlive, - }, - { - Name: "qux", - Tags: map[string]string{"role": "node", "segment": "b"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 4, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 2, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 7, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 9, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - }, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 1, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 3, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, + mgw := structs.TestNodeServiceMeshGateway(t) + mgw.ID = "mesh-gateway" + + tgw := structs.TestNodeServiceTerminatingGateway(t, "1.1.1.1") + tgw.ID = "terminating-gateway" + // Typical services and some consul services spread across two nodes + require.NoError(t, s.EnsureService(5, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: nil, Address: "", Port: 5000})) + require.NoError(t, s.EnsureService(6, "bar", &structs.NodeService{ID: "api", Service: "api", Tags: nil, Address: "", Port: 5000})) + require.NoError(t, s.EnsureService(7, "foo", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) + require.NoError(t, s.EnsureService(8, "bar", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil})) + require.NoError(t, s.EnsureService(9, "foo", &structs.NodeService{ID: "db-connect-proxy", Service: "db-connect-proxy", Tags: nil, Address: "", Port: 5000, Kind: structs.ServiceKindConnectProxy})) + require.NoError(t, s.EnsureRegistration(10, structs.TestRegisterIngressGateway(t))) + require.NoError(t, s.EnsureService(11, "foo", mgw)) + require.NoError(t, s.EnsureService(12, "foo", tgw)) + require.NoError(t, s.EnsureService(13, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}})) + require.NoError(t, s.EnsureConfigEntry(14, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "foo", + })) + require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "bar", + })) + require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{ + Kind: structs.IngressGateway, + Name: "baz", + })) + } + baseCaseMembers := nodesAndSvcsCase.getMembersFunc() + nodesAndSvcsCase.getMembersFunc = func() []serf.Member { + baseCaseMembers = append(baseCaseMembers, serf.Member{ + Name: "baz", + Tags: map[string]string{"role": "node", "segment": "a"}, + Status: serf.StatusAlive, + }) + baseCaseMembers = append(baseCaseMembers, serf.Member{ + Name: "qux", + Tags: map[string]string{"role": "node", "segment": "b"}, + Status: serf.StatusAlive, + }) + return baseCaseMembers + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.nodes", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.nodes;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.nodes", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.members.clients;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.members.clients", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.members.clients;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.members.clients", + Value: 2, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.services;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.services", + Value: 7, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.services;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.services", + Value: 7, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.service_instances;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.service_instances", + Value: 9, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.service_instances;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.service_instances", + Value: 9, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, }, } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-proxy"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-proxy"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=terminating-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "terminating-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=mesh-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "mesh-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=connect-native"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.connect_instances", + Value: 1, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "connect-native"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.config_entries", + Value: 3, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + nodesAndSvcsCase.expectedGauges["consul.usage.test.state.config_entries;datacenter=dc1;kind=ingress-gateway"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.config_entries", + Value: 3, + Labels: []metrics.Label{ + {Name: "datacenter", Value: "dc1"}, + {Name: "kind", Value: "ingress-gateway"}, + }, + } + cases["nodes-and-services"] = nodesAndSvcsCase + delete(cases, "nodes") for name, tcase := range cases { t.Run(name, func(t *testing.T) { @@ -1280,379 +1071,39 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) { } func TestUsageReporter_emitKVUsage_OSS(t *testing.T) { - type testCase struct { - modfiyStateStore func(t *testing.T, s *state.Store) - getMembersFunc getMembersFunc - expectedGauges map[string]metrics.GaugeValue + cases := make(map[string]testCase) + for k, v := range baseCases { + eg := make(map[string]metrics.GaugeValue) + for k, v := range v.expectedGauges { + eg[k] = v + } + cases[k] = testCase{v.modfiyStateStore, v.getMembersFunc, eg} } - cases := map[string]testCase{ - "empty-state": { - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - getMembersFunc: func() []serf.Member { return []serf.Member{} }, - }, - "nodes": { - modfiyStateStore: func(t *testing.T, s *state.Store) { - require.NoError(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"})) - require.NoError(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"})) - require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"})) - require.NoError(t, s.KVSSet(4, &structs.DirEntry{Key: "a", Value: []byte{1}})) - require.NoError(t, s.KVSSet(5, &structs.DirEntry{Key: "b", Value: []byte{1}})) - require.NoError(t, s.KVSSet(6, &structs.DirEntry{Key: "c", Value: []byte{1}})) - require.NoError(t, s.KVSSet(7, &structs.DirEntry{Key: "d", Value: []byte{1}})) - require.NoError(t, s.KVSDelete(8, "d", &acl.EnterpriseMeta{})) - require.NoError(t, s.KVSDelete(9, "c", &acl.EnterpriseMeta{})) - require.NoError(t, s.KVSSet(10, &structs.DirEntry{Key: "e", Value: []byte{1}})) - require.NoError(t, s.KVSSet(11, &structs.DirEntry{Key: "f", Value: []byte{1}})) - }, - getMembersFunc: func() []serf.Member { - return []serf.Member{ - { - Name: "foo", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "bar", - Tags: map[string]string{"role": "consul"}, - Status: serf.StatusAlive, - }, - { - Name: "baz", - Tags: map[string]string{"role": "node"}, - Status: serf.StatusAlive, - }, - } - }, - expectedGauges: map[string]metrics.GaugeValue{ - // --- node --- - "consul.usage.test.consul.state.nodes;datacenter=dc1": { - Name: "consul.usage.test.consul.state.nodes", - Value: 3, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- peering --- - "consul.usage.test.consul.state.peerings;datacenter=dc1": { - Name: "consul.usage.test.consul.state.peerings", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- member --- - "consul.usage.test.consul.members.servers;datacenter=dc1": { - Name: "consul.usage.test.consul.members.servers", - Value: 2, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.members.clients;datacenter=dc1": { - Name: "consul.usage.test.consul.members.clients", - Value: 1, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service --- - "consul.usage.test.consul.state.services;datacenter=dc1": { - Name: "consul.usage.test.consul.state.services", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - "consul.usage.test.consul.state.service_instances;datacenter=dc1": { - Name: "consul.usage.test.consul.state.service_instances", - Value: 0, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- service mesh --- - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-proxy"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh-gateway"}, - }, - }, - "consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-native": { - Name: "consul.usage.test.consul.state.connect_instances", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "connect-native"}, - }, - }, - // --- kv --- - "consul.usage.test.consul.state.kv_entries;datacenter=dc1": { - Name: "consul.usage.test.consul.state.kv_entries", - Value: 4, - Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, - }, - // --- config entries --- - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-intentions": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-intentions"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-resolver": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-resolver"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-router": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-router"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=ingress-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "ingress-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=service-splitter": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "service-splitter"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=mesh": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "mesh"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=proxy-defaults": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "proxy-defaults"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=terminating-gateway": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "terminating-gateway"}, - }, - }, - "consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=exported-services": { - Name: "consul.usage.test.consul.state.config_entries", - Value: 0, - Labels: []metrics.Label{ - {Name: "datacenter", Value: "dc1"}, - {Name: "kind", Value: "exported-services"}, - }, - }, - }, - }, + nodesCase := cases["nodes"] + mss := nodesCase.modfiyStateStore + nodesCase.modfiyStateStore = func(t *testing.T, s *state.Store) { + mss(t, s) + require.NoError(t, s.KVSSet(4, &structs.DirEntry{Key: "a", Value: []byte{1}})) + require.NoError(t, s.KVSSet(5, &structs.DirEntry{Key: "b", Value: []byte{1}})) + require.NoError(t, s.KVSSet(6, &structs.DirEntry{Key: "c", Value: []byte{1}})) + require.NoError(t, s.KVSSet(7, &structs.DirEntry{Key: "d", Value: []byte{1}})) + require.NoError(t, s.KVSDelete(8, "d", &acl.EnterpriseMeta{})) + require.NoError(t, s.KVSDelete(9, "c", &acl.EnterpriseMeta{})) + require.NoError(t, s.KVSSet(10, &structs.DirEntry{Key: "e", Value: []byte{1}})) + require.NoError(t, s.KVSSet(11, &structs.DirEntry{Key: "f", Value: []byte{1}})) } + nodesCase.expectedGauges["consul.usage.test.consul.state.kv_entries;datacenter=dc1"] = metrics.GaugeValue{ // Legacy + Name: "consul.usage.test.consul.state.kv_entries", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + nodesCase.expectedGauges["consul.usage.test.state.kv_entries;datacenter=dc1"] = metrics.GaugeValue{ + Name: "consul.usage.test.state.kv_entries", + Value: 4, + Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}}, + } + cases["nodes"] = nodesCase for name, tcase := range cases { t.Run(name, func(t *testing.T) { diff --git a/agent/consul/xdscapacity/capacity.go b/agent/consul/xdscapacity/capacity.go new file mode 100644 index 000000000..2e24a09e0 --- /dev/null +++ b/agent/consul/xdscapacity/capacity.go @@ -0,0 +1,210 @@ +package xdscapacity + +import ( + "context" + "math" + "time" + + "github.com/armon/go-metrics" + "github.com/armon/go-metrics/prometheus" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/lib/retry" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-memdb" + "golang.org/x/time/rate" +) + +var StatsGauges = []prometheus.GaugeDefinition{ + { + Name: []string{"xds", "server", "idealStreamsMax"}, + Help: "The maximum number of xDS streams per server, chosen to achieve a roughly even spread of load across servers.", + }, +} + +// errorMargin is amount to which we allow a server to be over-occupied, +// expressed as a percentage (between 0 and 1). +// +// We allow 10% more than the ideal number of streams per server. +const errorMargin = 0.1 + +// Controller determines the ideal number of xDS streams for the server to +// handle and enforces it using the given SessionLimiter. +// +// We aim for a roughly even spread of streams between servers in the cluster +// and, to that end, limit the number of streams each server can handle to: +// +// ( / ) + +// +// Controller receives changes to the number of healthy servers from the +// autopilot delegate. It queries the state store's catalog tables to discover +// the number of registered proxy (sidecar and gateway) services. +type Controller struct { + cfg Config + + serverCh chan uint32 + doneCh chan struct{} + + prevMaxSessions uint32 + prevRateLimit rate.Limit +} + +// Config contains the dependencies for Controller. +type Config struct { + Logger hclog.Logger + GetStore func() Store + SessionLimiter SessionLimiter +} + +// SessionLimiter is used to enforce the session limit to achieve the ideal +// spread of xDS streams between servers. +type SessionLimiter interface { + SetMaxSessions(maxSessions uint32) + SetDrainRateLimit(rateLimit rate.Limit) +} + +// NewController creates a new capacity controller with the given config. +// +// Call Run to start the control-loop. +func NewController(cfg Config) *Controller { + return &Controller{ + cfg: cfg, + serverCh: make(chan uint32), + doneCh: make(chan struct{}), + } +} + +// Run the control-loop until the given context is canceled or reaches its +// deadline. +func (c *Controller) Run(ctx context.Context) { + defer close(c.doneCh) + + ws, numProxies, err := c.countProxies(ctx) + if err != nil { + return + } + + var numServers uint32 + for { + select { + case s := <-c.serverCh: + numServers = s + c.updateMaxSessions(numServers, numProxies) + case <-ws.WatchCh(ctx): + ws, numProxies, err = c.countProxies(ctx) + if err != nil { + return + } + c.updateDrainRateLimit(numProxies) + c.updateMaxSessions(numServers, numProxies) + case <-ctx.Done(): + return + } + } +} + +// SetServerCount updates the number of healthy servers that is used when +// determining capacity. It is called by the autopilot delegate. +func (c *Controller) SetServerCount(count uint32) { + select { + case c.serverCh <- count: + case <-c.doneCh: + } +} + +func (c *Controller) updateDrainRateLimit(numProxies uint32) { + rateLimit := calcRateLimit(numProxies) + if rateLimit == c.prevRateLimit { + return + } + + c.cfg.Logger.Debug("updating drain rate limit", "rate_limit", rateLimit) + c.cfg.SessionLimiter.SetDrainRateLimit(rateLimit) + c.prevRateLimit = rateLimit +} + +// We dynamically scale the rate at which excess sessions will be drained +// according to the number of proxies in the catalog. +// +// The numbers here are pretty arbitrary (change them if you find better ones!) +// but the logic is: +// +// 0-512 proxies: drain 1 per second +// 513-2815 proxies: linearly scaled by 1/s for every additional 256 proxies +// 2816+ proxies: drain 10 per second +// +func calcRateLimit(numProxies uint32) rate.Limit { + perSecond := math.Floor((float64(numProxies) - 256) / 256) + + if perSecond < 1 { + return 1 + } + + if perSecond > 10 { + return 10 + } + + return rate.Limit(perSecond) +} + +func (c *Controller) updateMaxSessions(numServers, numProxies uint32) { + if numServers == 0 || numProxies == 0 { + return + } + + maxSessions := uint32(math.Ceil((float64(numProxies) / float64(numServers)) * (1 + errorMargin))) + if maxSessions == c.prevMaxSessions { + return + } + + c.cfg.Logger.Debug( + "updating max sessions", + "max_sessions", maxSessions, + "num_servers", numServers, + "num_proxies", numProxies, + ) + metrics.SetGauge([]string{"xds", "server", "idealStreamsMax"}, float32(maxSessions)) + c.cfg.SessionLimiter.SetMaxSessions(maxSessions) + c.prevMaxSessions = maxSessions +} + +// countProxies counts the number of registered proxy services, retrying on +// error until the given context is cancelled. +func (c *Controller) countProxies(ctx context.Context) (memdb.WatchSet, uint32, error) { + retryWaiter := &retry.Waiter{ + MinFailures: 1, + MinWait: 1 * time.Second, + MaxWait: 1 * time.Minute, + } + + for { + store := c.cfg.GetStore() + + ws := memdb.NewWatchSet() + ws.Add(store.AbandonCh()) + + var count uint32 + _, usage, err := store.ServiceUsage(ws) + + // Query failed? Wait for a while, and then go to the top of the loop to + // retry (unless the context is cancelled). + if err != nil { + if err := retryWaiter.Wait(ctx); err != nil { + return nil, 0, err + } + continue + } + + for kind, kindCount := range usage.ConnectServiceInstances { + if structs.ServiceKind(kind).IsProxy() { + count += uint32(kindCount) + } + } + return ws, count, nil + } +} + +type Store interface { + AbandonCh() <-chan struct{} + ServiceUsage(ws memdb.WatchSet) (uint64, state.ServiceUsage, error) +} diff --git a/agent/consul/xdscapacity/capacity_test.go b/agent/consul/xdscapacity/capacity_test.go new file mode 100644 index 000000000..1b564949c --- /dev/null +++ b/agent/consul/xdscapacity/capacity_test.go @@ -0,0 +1,136 @@ +package xdscapacity + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/armon/go-metrics" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/sdk/testutil" +) + +func TestController(t *testing.T) { + const index = 123 + + store := state.NewStateStore(nil) + + // This loop generates: + // + // 4 (service kind) * 5 (service) * 5 * (node) = 100 proxy services. And 25 non-proxy services. + for _, kind := range []structs.ServiceKind{ + // These will be included in the count. + structs.ServiceKindConnectProxy, + structs.ServiceKindIngressGateway, + structs.ServiceKindTerminatingGateway, + structs.ServiceKindMeshGateway, + + // This one will not. + structs.ServiceKindTypical, + } { + for i := 0; i < 5; i++ { + serviceName := fmt.Sprintf("%s-%d", kind, i) + + for j := 0; j < 5; j++ { + nodeName := fmt.Sprintf("%s-node-%d", serviceName, j) + + require.NoError(t, store.EnsureRegistration(index, &structs.RegisterRequest{ + Node: nodeName, + Service: &structs.NodeService{ + ID: serviceName, + Service: serviceName, + Kind: kind, + }, + })) + } + } + } + + limiter := newTestLimiter() + + sink := metrics.NewInmemSink(1*time.Minute, 1*time.Minute) + cfg := metrics.DefaultConfig("consul") + cfg.EnableHostname = false + metrics.NewGlobal(cfg, sink) + + t.Cleanup(func() { + sink := &metrics.BlackholeSink{} + metrics.NewGlobal(cfg, sink) + }) + + adj := NewController(Config{ + Logger: testutil.Logger(t), + GetStore: func() Store { return store }, + SessionLimiter: limiter, + }) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + go adj.Run(ctx) + + // Keen readers will notice the numbers here are off by one. This is due to + // floating point math (because we multiply by 1.1). + testutil.RunStep(t, "load split between 2 servers", func(t *testing.T) { + adj.SetServerCount(2) + require.Equal(t, 56, limiter.receive(t)) + }) + + testutil.RunStep(t, "all load on 1 server", func(t *testing.T) { + adj.SetServerCount(1) + require.Equal(t, 111, limiter.receive(t)) + }) + + testutil.RunStep(t, "delete proxy service", func(t *testing.T) { + require.NoError(t, store.DeleteService(index+1, "ingress-gateway-0-node-0", "ingress-gateway-0", acl.DefaultEnterpriseMeta(), structs.DefaultPeerKeyword)) + require.Equal(t, 109, limiter.receive(t)) + }) + + testutil.RunStep(t, "check we're emitting gauge", func(t *testing.T) { + data := sink.Data() + require.Len(t, data, 1) + + gauge, ok := data[0].Gauges["consul.xds.server.idealStreamsMax"] + require.True(t, ok) + require.Equal(t, float32(109), gauge.Value) + }) +} + +func newTestLimiter() *testLimiter { + return &testLimiter{ch: make(chan uint32, 1)} +} + +type testLimiter struct{ ch chan uint32 } + +func (tl *testLimiter) SetMaxSessions(max uint32) { tl.ch <- max } + +func (tl *testLimiter) receive(t *testing.T) int { + select { + case v := <-tl.ch: + return int(v) + case <-time.After(1 * time.Second): + t.Fatal("timeout waiting for SetMaxSessions") + } + panic("this should never be reached") +} + +func (tl *testLimiter) SetDrainRateLimit(rateLimit rate.Limit) {} + +func TestCalcRateLimit(t *testing.T) { + for in, out := range map[uint32]rate.Limit{ + 0: rate.Limit(1), + 1: rate.Limit(1), + 512: rate.Limit(1), + 768: rate.Limit(2), + 1024: rate.Limit(3), + 2816: rate.Limit(10), + 1000000000: rate.Limit(10), + } { + require.Equalf(t, out, calcRateLimit(in), "calcRateLimit(%d)", in) + } +} diff --git a/agent/discovery_chain_endpoint_test.go b/agent/discovery_chain_endpoint_test.go index 8b4a7e272..42c082591 100644 --- a/agent/discovery_chain_endpoint_test.go +++ b/agent/discovery_chain_endpoint_test.go @@ -27,8 +27,17 @@ func TestDiscoveryChainRead(t *testing.T) { defer a.Shutdown() testrpc.WaitForTestAgent(t, a.RPC, "dc1") - newTarget := func(service, serviceSubset, namespace, partition, datacenter string) *structs.DiscoveryTarget { - t := structs.NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter) + newTarget := func(opts structs.DiscoveryTargetOpts) *structs.DiscoveryTarget { + if opts.Namespace == "" { + opts.Namespace = "default" + } + if opts.Partition == "" { + opts.Partition = "default" + } + if opts.Datacenter == "" { + opts.Datacenter = "dc1" + } + t := structs.NewDiscoveryTarget(opts) t.SNI = connect.TargetSNI(t, connect.TestClusterID+".consul") t.Name = t.SNI t.ConnectTimeout = 5 * time.Second // default @@ -99,7 +108,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, } require.Equal(t, expect, value.Chain) @@ -144,7 +153,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc2": newTarget("web", "", "default", "default", "dc2"), + "web.default.default.dc2": newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), }, } require.Equal(t, expect, value.Chain) @@ -198,7 +207,7 @@ func TestDiscoveryChainRead(t *testing.T) { }, }, Targets: map[string]*structs.DiscoveryTarget{ - "web.default.default.dc1": newTarget("web", "", "default", "default", "dc1"), + "web.default.default.dc1": newTarget(structs.DiscoveryTargetOpts{Service: "web"}), }, } require.Equal(t, expect, value.Chain) @@ -264,11 +273,11 @@ func TestDiscoveryChainRead(t *testing.T) { }, Targets: map[string]*structs.DiscoveryTarget{ "web.default.default.dc1": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 33*time.Second, ), "web.default.default.dc2": targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc2"), + newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), 33*time.Second, ), }, @@ -280,7 +289,7 @@ func TestDiscoveryChainRead(t *testing.T) { })) expectTarget_DC1 := targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc1"), + newTarget(structs.DiscoveryTargetOpts{Service: "web"}), 22*time.Second, ) expectTarget_DC1.MeshGateway = structs.MeshGatewayConfig{ @@ -288,7 +297,7 @@ func TestDiscoveryChainRead(t *testing.T) { } expectTarget_DC2 := targetWithConnectTimeout( - newTarget("web", "", "default", "default", "dc2"), + newTarget(structs.DiscoveryTargetOpts{Service: "web", Datacenter: "dc2"}), 22*time.Second, ) expectTarget_DC2.MeshGateway = structs.MeshGatewayConfig{ diff --git a/agent/grpc-external/limiter/limiter.go b/agent/grpc-external/limiter/limiter.go new file mode 100644 index 000000000..26e013b16 --- /dev/null +++ b/agent/grpc-external/limiter/limiter.go @@ -0,0 +1,245 @@ +// package limiter provides primatives for limiting the number of concurrent +// operations in-flight. +package limiter + +import ( + "context" + "errors" + "math/rand" + "sort" + "sync" + "sync/atomic" + + "golang.org/x/time/rate" +) + +// Unlimited can be used to allow an unlimited number of concurrent sessions. +const Unlimited uint32 = 0 + +// ErrCapacityReached is returned when there is no capacity for additional sessions. +var ErrCapacityReached = errors.New("active session limit reached") + +// SessionLimiter is a session-based concurrency limiter, it provides the basis +// of gRPC/xDS load balancing. +// +// Stream handlers obtain a session with BeginSession before they begin serving +// resources - if the server has reached capacity ErrCapacityReached is returned, +// otherwise a Session is returned. +// +// It is the session-holder's responsibility to: +// +// 1. Call End on the session when finished. +// 2. Receive on the session's Terminated channel and exit (e.g. close the gRPC +// stream) when it is closed. +// +// The maximum number of concurrent sessions is controlled with SetMaxSessions. +// If there are more than the given maximum sessions already in-flight, +// SessionLimiter will drain randomly-selected sessions at a rate controlled +// by SetDrainRateLimit. +type SessionLimiter struct { + drainLimiter *rate.Limiter + + // max and inFlight are read/written using atomic operations. + max, inFlight uint32 + + // wakeCh is used to trigger the Run loop to start draining excess sessions. + wakeCh chan struct{} + + // Everything below here is guarded by mu. + mu sync.Mutex + maxSessionID uint64 + sessionIDs []uint64 // sessionIDs must be sorted so we can binary search it. + sessions map[uint64]*session +} + +// NewSessionLimiter creates a new SessionLimiter. +func NewSessionLimiter() *SessionLimiter { + return &SessionLimiter{ + drainLimiter: rate.NewLimiter(rate.Inf, 1), + max: Unlimited, + wakeCh: make(chan struct{}, 1), + sessionIDs: make([]uint64, 0), + sessions: make(map[uint64]*session), + } +} + +// Run the SessionLimiter's drain loop, which terminates excess sessions if the +// limit is lowered. It will exit when the given context is canceled or reaches +// its deadline. +func (l *SessionLimiter) Run(ctx context.Context) { + for { + select { + case <-l.wakeCh: + for { + if !l.overCapacity() { + break + } + + if err := l.drainLimiter.Wait(ctx); err != nil { + break + } + + if !l.overCapacity() { + break + } + + l.terminateSession() + } + case <-ctx.Done(): + return + } + } +} + +// SetMaxSessions controls the maximum number of concurrent sessions. If it is +// lower, randomly-selected sessions will be drained. +func (l *SessionLimiter) SetMaxSessions(max uint32) { + atomic.StoreUint32(&l.max, max) + + // Send on wakeCh without blocking if the Run loop is busy. wakeCh has a + // buffer of 1, so no triggers will be missed. + select { + case l.wakeCh <- struct{}{}: + default: + } +} + +// SetDrainRateLimit controls the rate at which excess sessions will be drained. +func (l *SessionLimiter) SetDrainRateLimit(limit rate.Limit) { + l.drainLimiter.SetLimit(limit) +} + +// BeginSession begins a new session, or returns ErrCapacityReached if the +// concurrent session limit has been reached. +// +// It is the session-holder's responsibility to: +// +// 1. Call End on the session when finished. +// 2. Receive on the session's Terminated channel and exit (e.g. close the gRPC +// stream) when it is closed. +func (l *SessionLimiter) BeginSession() (Session, error) { + if !l.hasCapacity() { + return nil, ErrCapacityReached + } + + l.mu.Lock() + defer l.mu.Unlock() + return l.createSessionLocked(), nil +} + +// Note: hasCapacity is *best effort*. As we do not hold l.mu it's possible that: +// +// - max has changed by the time we compare it to inFlight. +// - inFlight < max now, but increases before we create a new session. +// +// This is acceptable for our uses, especially because excess sessions will +// eventually be drained. +func (l *SessionLimiter) hasCapacity() bool { + max := atomic.LoadUint32(&l.max) + if max == Unlimited { + return true + } + + cur := atomic.LoadUint32(&l.inFlight) + return max > cur +} + +// Note: overCapacity is *best effort*. As we do not hold l.mu it's possible that: +// +// - max has changed by the time we compare it to inFlight. +// - inFlight > max now, but decreases before we terminate a session. +func (l *SessionLimiter) overCapacity() bool { + max := atomic.LoadUint32(&l.max) + if max == Unlimited { + return false + } + + cur := atomic.LoadUint32(&l.inFlight) + return cur > max +} + +func (l *SessionLimiter) terminateSession() { + l.mu.Lock() + defer l.mu.Unlock() + + idx := rand.Intn(len(l.sessionIDs)) + id := l.sessionIDs[idx] + l.sessions[id].terminate() + l.deleteSessionLocked(idx, id) +} + +func (l *SessionLimiter) createSessionLocked() *session { + session := &session{ + l: l, + id: l.maxSessionID, + termCh: make(chan struct{}), + } + + l.maxSessionID++ + l.sessionIDs = append(l.sessionIDs, session.id) + l.sessions[session.id] = session + + atomic.AddUint32(&l.inFlight, 1) + + return session +} + +func (l *SessionLimiter) deleteSessionLocked(idx int, id uint64) { + delete(l.sessions, id) + + // Note: it's important that we preserve the order here (which most allocation + // free deletion tricks don't) because we binary search the slice. + l.sessionIDs = append(l.sessionIDs[:idx], l.sessionIDs[idx+1:]...) + + atomic.AddUint32(&l.inFlight, ^uint32(0)) +} + +func (l *SessionLimiter) deleteSessionWithID(id uint64) { + l.mu.Lock() + defer l.mu.Unlock() + + idx := sort.Search(len(l.sessionIDs), func(i int) bool { + return l.sessionIDs[i] >= id + }) + + if idx == len(l.sessionIDs) || l.sessionIDs[idx] != id { + // It's possible that we weren't able to find the id because the session has + // already been deleted. This could be because the session-holder called End + // more than once, or because the session was drained. In either case there's + // nothing more to do. + return + } + + l.deleteSessionLocked(idx, id) +} + +// Session allows its holder to perform an operation (e.g. serve a gRPC stream) +// concurrenly with other session-holders. Sessions may be terminated abruptly +// by the SessionLimiter, so it is the responsibility of the holder to receive +// on the Terminated channel and halt the operation when it is closed. +type Session interface { + // End the session. + // + // This MUST be called when the session-holder is done (e.g. the gRPC stream + // is closed). + End() + + // Terminated is a channel that is closed when the session is terminated. + // + // The session-holder MUST receive on it and exit (e.g. close the gRPC stream) + // when it is closed. + Terminated() <-chan struct{} +} + +type session struct { + l *SessionLimiter + + id uint64 + termCh chan struct{} +} + +func (s *session) End() { s.l.deleteSessionWithID(s.id) } + +func (s *session) Terminated() <-chan struct{} { return s.termCh } + +func (s *session) terminate() { close(s.termCh) } diff --git a/agent/grpc-external/limiter/limiter_test.go b/agent/grpc-external/limiter/limiter_test.go new file mode 100644 index 000000000..cef6a4d41 --- /dev/null +++ b/agent/grpc-external/limiter/limiter_test.go @@ -0,0 +1,81 @@ +package limiter + +import ( + "context" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/lib" +) + +func init() { lib.SeedMathRand() } + +func TestSessionLimiter(t *testing.T) { + lim := NewSessionLimiter() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + go lim.Run(ctx) + + // doneCh is used to shut the goroutines down at the end of the test. + doneCh := make(chan struct{}) + t.Cleanup(func() { close(doneCh) }) + + // Start 10 sessions, and increment the counter when they are terminated. + var ( + terminations uint32 + wg sync.WaitGroup + ) + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + sess, err := lim.BeginSession() + require.NoError(t, err) + defer sess.End() + + wg.Done() + + select { + case <-sess.Terminated(): + atomic.AddUint32(&terminations, 1) + case <-doneCh: + } + }() + } + + // Wait for all the sessions to begin. + wg.Wait() + + // Lowering max sessions to 5 should result in 5 sessions being terminated. + lim.SetMaxSessions(5) + require.Eventually(t, func() bool { + return atomic.LoadUint32(&terminations) == 5 + }, 2*time.Second, 50*time.Millisecond) + + // Attempting to start a new session should fail immediately. + _, err := lim.BeginSession() + require.Equal(t, ErrCapacityReached, err) + + // Raising MaxSessions should make room for a new session. + lim.SetMaxSessions(6) + sess, err := lim.BeginSession() + require.NoError(t, err) + + // ...but trying to start another new one should fail + _, err = lim.BeginSession() + require.Equal(t, ErrCapacityReached, err) + + // ...until another session ends. + sess.End() + _, err = lim.BeginSession() + require.NoError(t, err) + + // Calling End twice is a no-op. + sess.End() + _, err = lim.BeginSession() + require.Equal(t, ErrCapacityReached, err) +} diff --git a/agent/grpc-external/server.go b/agent/grpc-external/server.go index 751cca91c..7ab72d77c 100644 --- a/agent/grpc-external/server.go +++ b/agent/grpc-external/server.go @@ -1,20 +1,19 @@ package external import ( + "time" + middleware "github.com/grpc-ecosystem/go-grpc-middleware" recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" "google.golang.org/grpc" - "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" - "time" agentmiddleware "github.com/hashicorp/consul/agent/grpc-middleware" - "github.com/hashicorp/consul/tlsutil" ) // NewServer constructs a gRPC server for the external gRPC port, to which // handlers can be registered. -func NewServer(logger agentmiddleware.Logger, tls *tlsutil.Configurator) *grpc.Server { +func NewServer(logger agentmiddleware.Logger) *grpc.Server { recoveryOpts := agentmiddleware.PanicHandlerMiddlewareOpts(logger) opts := []grpc.ServerOption{ @@ -34,9 +33,5 @@ func NewServer(logger agentmiddleware.Logger, tls *tlsutil.Configurator) *grpc.S MinTime: 15 * time.Second, }), } - if tls != nil && tls.GRPCTLSConfigured() { - creds := credentials.NewTLS(tls.IncomingGRPCConfig()) - opts = append(opts, grpc.Creds(creds)) - } return grpc.NewServer(opts...) } diff --git a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go index bed302d12..b320559e9 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params.go @@ -52,13 +52,21 @@ func (s *Server) GetEnvoyBootstrapParams(ctx context.Context, req *pbdataplane.G } // Build out the response + var serviceName string + if svc.ServiceKind == structs.ServiceKindConnectProxy { + serviceName = svc.ServiceProxy.DestinationServiceName + } else { + serviceName = svc.ServiceName + } resp := &pbdataplane.GetEnvoyBootstrapParamsResponse{ - Service: svc.ServiceProxy.DestinationServiceName, + Service: serviceName, Partition: svc.EnterpriseMeta.PartitionOrDefault(), Namespace: svc.EnterpriseMeta.NamespaceOrDefault(), Datacenter: s.Datacenter, ServiceKind: convertToResponseServiceKind(svc.ServiceKind), + NodeName: svc.Node, + NodeId: string(svc.ID), } bootstrapConfig, err := structpb.NewStruct(svc.ServiceProxy.Config) diff --git a/agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go similarity index 96% rename from agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go rename to agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go index c3b4fd146..aa42b0bf1 100644 --- a/agent/grpc-external/services/dataplane/get_envoy_boostrap_params_test.go +++ b/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_test.go @@ -97,14 +97,20 @@ func TestGetEnvoyBootstrapParams_Success(t *testing.T) { resp, err := client.GetEnvoyBootstrapParams(ctx, req) require.NoError(t, err) - require.Equal(t, tc.registerReq.Service.Proxy.DestinationServiceName, resp.Service) + if tc.registerReq.Service.IsGateway() { + require.Equal(t, tc.registerReq.Service.Service, resp.Service) + } else { + require.Equal(t, tc.registerReq.Service.Proxy.DestinationServiceName, resp.Service) + } + require.Equal(t, serverDC, resp.Datacenter) require.Equal(t, tc.registerReq.EnterpriseMeta.PartitionOrDefault(), resp.Partition) require.Equal(t, tc.registerReq.EnterpriseMeta.NamespaceOrDefault(), resp.Namespace) require.Contains(t, resp.Config.Fields, proxyConfigKey) require.Equal(t, structpb.NewStringValue(proxyConfigValue), resp.Config.Fields[proxyConfigKey]) require.Equal(t, convertToResponseServiceKind(tc.registerReq.Service.Kind), resp.ServiceKind) - + require.Equal(t, tc.registerReq.Node, resp.NodeName) + require.Equal(t, string(tc.registerReq.ID), resp.NodeId) } testCases := []testCase{ diff --git a/agent/grpc-external/services/peerstream/replication.go b/agent/grpc-external/services/peerstream/replication.go index be79a23bd..20bc3b12a 100644 --- a/agent/grpc-external/services/peerstream/replication.go +++ b/agent/grpc-external/services/peerstream/replication.go @@ -5,12 +5,13 @@ import ( "fmt" "strings" - "github.com/hashicorp/go-hclog" + "github.com/golang/protobuf/proto" "google.golang.org/genproto/googleapis/rpc/code" newproto "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/consul/agent/cache" + "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/proto/pbpeering" "github.com/hashicorp/consul/proto/pbpeerstream" @@ -35,7 +36,6 @@ import ( // Each cache.UpdateEvent will contain all instances for a service name. // If there are no instances in the event, we consider that to be a de-registration. func makeServiceResponse( - logger hclog.Logger, mst *MutableStatus, update cache.UpdateEvent, ) (*pbpeerstream.ReplicationMessage_Response, error) { @@ -87,7 +87,6 @@ func makeServiceResponse( } func makeCARootsResponse( - logger hclog.Logger, update cache.UpdateEvent, ) (*pbpeerstream.ReplicationMessage_Response, error) { any, _, err := marshalToProtoAny[*pbpeering.PeeringTrustBundle](update.Result) @@ -105,6 +104,24 @@ func makeCARootsResponse( }, nil } +func makeServerAddrsResponse( + update cache.UpdateEvent, +) (*pbpeerstream.ReplicationMessage_Response, error) { + any, _, err := marshalToProtoAny[*pbpeering.PeeringServerAddresses](update.Result) + if err != nil { + return nil, fmt.Errorf("failed to marshal: %w", err) + } + + return &pbpeerstream.ReplicationMessage_Response{ + ResourceURL: pbpeerstream.TypeURLPeeringServerAddresses, + // TODO(peering): Nonce management + Nonce: "", + ResourceID: "server-addrs", + Operation: pbpeerstream.Operation_OPERATION_UPSERT, + Resource: any, + }, nil +} + // marshalToProtoAny takes any input and returns: // the protobuf.Any type, the asserted T type, and any errors // during marshalling or type assertion. @@ -127,7 +144,6 @@ func (s *Server) processResponse( partition string, mutableStatus *MutableStatus, resp *pbpeerstream.ReplicationMessage_Response, - logger hclog.Logger, ) (*pbpeerstream.ReplicationMessage, error) { if !pbpeerstream.KnownTypeURL(resp.ResourceURL) { err := fmt.Errorf("received response for unknown resource type %q", resp.ResourceURL) @@ -151,7 +167,7 @@ func (s *Server) processResponse( ), err } - if err := s.handleUpsert(peerName, partition, mutableStatus, resp.ResourceURL, resp.ResourceID, resp.Resource, logger); err != nil { + if err := s.handleUpsert(peerName, partition, mutableStatus, resp.ResourceURL, resp.ResourceID, resp.Resource); err != nil { return makeNACKReply( resp.ResourceURL, resp.Nonce, @@ -163,7 +179,7 @@ func (s *Server) processResponse( return makeACKReply(resp.ResourceURL, resp.Nonce), nil case pbpeerstream.Operation_OPERATION_DELETE: - if err := s.handleDelete(peerName, partition, mutableStatus, resp.ResourceURL, resp.ResourceID, logger); err != nil { + if err := s.handleDelete(peerName, partition, mutableStatus, resp.ResourceURL, resp.ResourceID); err != nil { return makeNACKReply( resp.ResourceURL, resp.Nonce, @@ -196,7 +212,6 @@ func (s *Server) handleUpsert( resourceURL string, resourceID string, resource *anypb.Any, - logger hclog.Logger, ) error { if resource.TypeUrl != resourceURL { return fmt.Errorf("mismatched resourceURL %q and Any typeUrl %q", resourceURL, resource.TypeUrl) @@ -229,15 +244,23 @@ func (s *Server) handleUpsert( return s.handleUpsertRoots(peerName, partition, roots) + case pbpeerstream.TypeURLPeeringServerAddresses: + addrs := &pbpeering.PeeringServerAddresses{} + if err := resource.UnmarshalTo(addrs); err != nil { + return fmt.Errorf("failed to unmarshal resource: %w", err) + } + + return s.handleUpsertServerAddrs(peerName, partition, addrs) default: return fmt.Errorf("unexpected resourceURL: %s", resourceURL) } } // handleUpdateService handles both deletion and upsert events for a service. -// On an UPSERT event: -// - All nodes, services, checks in the input pbNodes are re-applied through Raft. -// - Any nodes, services, or checks in the catalog that were not in the input pbNodes get deleted. +// +// On an UPSERT event: +// - All nodes, services, checks in the input pbNodes are re-applied through Raft. +// - Any nodes, services, or checks in the catalog that were not in the input pbNodes get deleted. // // On a DELETE event: // - A reconciliation against nil or empty input pbNodes leads to deleting all stored catalog resources @@ -449,13 +472,39 @@ func (s *Server) handleUpsertRoots( return s.Backend.PeeringTrustBundleWrite(req) } +func (s *Server) handleUpsertServerAddrs( + peerName string, + partition string, + addrs *pbpeering.PeeringServerAddresses, +) error { + q := state.Query{ + Value: peerName, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInPartition(partition), + } + _, existing, err := s.GetStore().PeeringRead(nil, q) + if err != nil { + return fmt.Errorf("failed to read peering: %w", err) + } + if existing == nil || !existing.IsActive() { + return fmt.Errorf("peering does not exist or has been marked for deletion") + } + + // Clone to avoid mutating the existing data + p := proto.Clone(existing).(*pbpeering.Peering) + p.PeerServerAddresses = addrs.GetAddresses() + + req := &pbpeering.PeeringWriteRequest{ + Peering: p, + } + return s.Backend.PeeringWrite(req) +} + func (s *Server) handleDelete( peerName string, partition string, mutableStatus *MutableStatus, resourceURL string, resourceID string, - logger hclog.Logger, ) error { switch resourceURL { case pbpeerstream.TypeURLExportedService: diff --git a/agent/grpc-external/services/peerstream/server.go b/agent/grpc-external/services/peerstream/server.go index 7254c60c7..0f0627cb5 100644 --- a/agent/grpc-external/services/peerstream/server.go +++ b/agent/grpc-external/services/peerstream/server.go @@ -26,11 +26,12 @@ const ( type Server struct { Config + + Tracker *Tracker } type Config struct { Backend Backend - Tracker *Tracker GetStore func() StateStore Logger hclog.Logger ForwardRPC func(structs.RPCInfo, func(*grpc.ClientConn) error) (bool, error) @@ -53,7 +54,6 @@ type ACLResolver interface { func NewServer(cfg Config) *Server { requireNotNil(cfg.Backend, "Backend") - requireNotNil(cfg.Tracker, "Tracker") requireNotNil(cfg.GetStore, "GetStore") requireNotNil(cfg.Logger, "Logger") // requireNotNil(cfg.ACLResolver, "ACLResolver") // TODO(peering): reenable check when ACLs are required @@ -67,7 +67,8 @@ func NewServer(cfg Config) *Server { cfg.incomingHeartbeatTimeout = defaultIncomingHeartbeatTimeout } return &Server{ - Config: cfg, + Config: cfg, + Tracker: NewTracker(cfg.incomingHeartbeatTimeout), } } @@ -104,6 +105,7 @@ type Backend interface { PeeringTrustBundleWrite(req *pbpeering.PeeringTrustBundleWriteRequest) error CatalogRegister(req *structs.RegisterRequest) error CatalogDeregister(req *structs.DeregisterRequest) error + PeeringWrite(req *pbpeering.PeeringWriteRequest) error } // StateStore provides a read-only interface for querying Peering data. diff --git a/agent/grpc-external/services/peerstream/stream_resources.go b/agent/grpc-external/services/peerstream/stream_resources.go index 657972b88..bdad21467 100644 --- a/agent/grpc-external/services/peerstream/stream_resources.go +++ b/agent/grpc-external/services/peerstream/stream_resources.go @@ -161,8 +161,22 @@ func (s *Server) StreamResources(stream pbpeerstream.PeerStreamService_StreamRes if p == nil { return grpcstatus.Error(codes.InvalidArgument, "initial subscription for unknown PeerID: "+req.PeerID) } + if !p.IsActive() { + // If peering is terminated, then our peer sent the termination message. + // For other non-active states, send the termination message. + if p.State != pbpeering.PeeringState_TERMINATED { + term := &pbpeerstream.ReplicationMessage{ + Payload: &pbpeerstream.ReplicationMessage_Terminated_{ + Terminated: &pbpeerstream.ReplicationMessage_Terminated{}, + }, + } + logTraceSend(logger, term) - // TODO(peering): If the peering is marked as deleted, send a Terminated message and return + // we don't care if send fails; stream will be killed by termination message or grpc error + _ = stream.Send(term) + } + return grpcstatus.Error(codes.Aborted, "peering is marked as deleted: "+req.PeerID) + } secrets, err := s.GetStore().PeeringSecretsRead(nil, req.PeerID) if err != nil { @@ -347,6 +361,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { for _, resourceURL := range []string{ pbpeerstream.TypeURLExportedService, pbpeerstream.TypeURLPeeringTrustBundle, + pbpeerstream.TypeURLPeeringServerAddresses, } { sub := makeReplicationRequest(&pbpeerstream.ReplicationMessage_Request{ ResourceURL: resourceURL, @@ -544,14 +559,11 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { // At this point we have a valid ResourceURL and we are subscribed to it. switch { - case req.ResponseNonce == "" && req.Error != nil: - return grpcstatus.Error(codes.InvalidArgument, "initial subscription request for a resource type must not contain an error") - - case req.ResponseNonce != "" && req.Error == nil: // ACK + case req.Error == nil: // ACK // TODO(peering): handle ACK fully status.TrackAck() - case req.ResponseNonce != "" && req.Error != nil: // NACK + case req.Error != nil: // NACK // TODO(peering): handle NACK fully logger.Warn("client peer was unable to apply resource", "code", req.Error.Code, "error", req.Error.Message) status.TrackNack(fmt.Sprintf("client peer was unable to apply resource: %s", req.Error.Message)) @@ -567,7 +579,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { if resp := msg.GetResponse(); resp != nil { // TODO(peering): Ensure there's a nonce - reply, err := s.processResponse(streamReq.PeerName, streamReq.Partition, status, resp, logger) + reply, err := s.processResponse(streamReq.PeerName, streamReq.Partition, status, resp) if err != nil { logger.Error("failed to persist resource", "resourceURL", resp.ResourceURL, "resourceID", resp.ResourceID) status.TrackRecvError(err.Error()) @@ -575,6 +587,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { status.TrackRecvResourceSuccess() } + // We are replying ACK or NACK depending on whether we successfully processed the response. if err := streamSend(reply); err != nil { return fmt.Errorf("failed to send to stream: %v", err) } @@ -612,7 +625,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { var resp *pbpeerstream.ReplicationMessage_Response switch { case strings.HasPrefix(update.CorrelationID, subExportedService): - resp, err = makeServiceResponse(logger, status, update) + resp, err = makeServiceResponse(status, update) if err != nil { // Log the error and skip this response to avoid locking up peering due to a bad update event. logger.Error("failed to create service response", "error", err) @@ -623,13 +636,20 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { // TODO(Peering): figure out how to sync this separately case update.CorrelationID == subCARoot: - resp, err = makeCARootsResponse(logger, update) + resp, err = makeCARootsResponse(update) if err != nil { // Log the error and skip this response to avoid locking up peering due to a bad update event. logger.Error("failed to create ca roots response", "error", err) continue } + case update.CorrelationID == subServerAddrs: + resp, err = makeServerAddrsResponse(update) + if err != nil { + logger.Error("failed to create server address response", "error", err) + continue + } + default: logger.Warn("unrecognized update type from subscription manager: " + update.CorrelationID) continue @@ -640,6 +660,7 @@ func (s *Server) realHandleStream(streamReq HandleStreamRequest) error { replResp := makeReplicationResponse(resp) if err := streamSend(replResp); err != nil { + // note: govet warns of context leak but it is cleaned up in a defer return fmt.Errorf("failed to push data for %q: %w", update.CorrelationID, err) } } diff --git a/agent/grpc-external/services/peerstream/stream_test.go b/agent/grpc-external/services/peerstream/stream_test.go index 49ba7be04..977f7d565 100644 --- a/agent/grpc-external/services/peerstream/stream_test.go +++ b/agent/grpc-external/services/peerstream/stream_test.go @@ -126,7 +126,7 @@ func TestStreamResources_Server_LeaderBecomesFollower(t *testing.T) { // Receive a subscription from a peer. This message arrives while the // server is a leader and should work. - testutil.RunStep(t, "send subscription request to leader and consume its two requests", func(t *testing.T) { + testutil.RunStep(t, "send subscription request to leader and consume its three requests", func(t *testing.T) { sub := &pbpeerstream.ReplicationMessage{ Payload: &pbpeerstream.ReplicationMessage_Open_{ Open: &pbpeerstream.ReplicationMessage_Open{ @@ -145,6 +145,10 @@ func TestStreamResources_Server_LeaderBecomesFollower(t *testing.T) { msg2, err := client.Recv() require.NoError(t, err) require.NotEmpty(t, msg2) + + msg3, err := client.Recv() + require.NoError(t, err) + require.NotEmpty(t, msg3) }) // The ACK will be a new request but at this point the server is not the @@ -499,9 +503,8 @@ func TestStreamResources_Server_Terminate(t *testing.T) { base: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), } - srv, store := newTestServer(t, func(c *Config) { - c.Tracker.SetClock(it.Now) - }) + srv, store := newTestServer(t, nil) + srv.Tracker.setClock(it.Now) p := writePeeringToBeDialed(t, store, 1, "my-peer") require.Empty(t, p.PeerID, "should be empty if being dialed") @@ -552,9 +555,8 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { base: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), } - srv, store := newTestServer(t, func(c *Config) { - c.Tracker.SetClock(it.Now) - }) + srv, store := newTestServer(t, nil) + srv.Tracker.setClock(it.Now) // Set the initial roots and CA configuration. _, rootA := writeInitialRootsAndCA(t, store) @@ -572,7 +574,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { }) }) - var lastSendSuccess time.Time + var lastSendAck time.Time testutil.RunStep(t, "ack tracked as success", func(t *testing.T) { ack := &pbpeerstream.ReplicationMessage{ @@ -587,19 +589,19 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { }, } - lastSendSuccess = it.FutureNow(1) + lastSendAck = it.FutureNow(1) err := client.Send(ack) require.NoError(t, err) expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, } retry.Run(t, func(r *retry.R) { - status, ok := srv.StreamStatus(testPeerID) + rStatus, ok := srv.StreamStatus(testPeerID) require.True(r, ok) - require.Equal(r, expect, status) + require.Equal(r, expect, rStatus) }) }) @@ -629,15 +631,15 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, } retry.Run(t, func(r *retry.R) { - status, ok := srv.StreamStatus(testPeerID) + rStatus, ok := srv.StreamStatus(testPeerID) require.True(r, ok) - require.Equal(r, expect, status) + require.Equal(r, expect, rStatus) }) }) @@ -694,7 +696,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, @@ -753,7 +755,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, @@ -785,7 +787,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: true, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, LastRecvResourceSuccess: lastRecvResourceSuccess, @@ -816,7 +818,7 @@ func TestStreamResources_Server_StreamTracker(t *testing.T) { expect := Status{ Connected: false, DisconnectErrorMessage: lastRecvErrorMsg, - LastAck: lastSendSuccess, + LastAck: lastSendAck, LastNack: lastNack, LastNackMessage: lastNackMsg, DisconnectTime: disconnectTime, @@ -1128,9 +1130,9 @@ func TestStreamResources_Server_DisconnectsOnHeartbeatTimeout(t *testing.T) { } srv, store := newTestServer(t, func(c *Config) { - c.Tracker.SetClock(it.Now) - c.incomingHeartbeatTimeout = 5 * time.Millisecond + c.incomingHeartbeatTimeout = 50 * time.Millisecond }) + srv.Tracker.setClock(it.Now) p := writePeeringToBeDialed(t, store, 1, "my-peer") require.Empty(t, p.PeerID, "should be empty if being dialed") @@ -1176,9 +1178,9 @@ func TestStreamResources_Server_SendsHeartbeats(t *testing.T) { outgoingHeartbeatInterval := 5 * time.Millisecond srv, store := newTestServer(t, func(c *Config) { - c.Tracker.SetClock(it.Now) c.outgoingHeartbeatInterval = outgoingHeartbeatInterval }) + srv.Tracker.setClock(it.Now) p := writePeeringToBeDialed(t, store, 1, "my-peer") require.Empty(t, p.PeerID, "should be empty if being dialed") @@ -1235,9 +1237,9 @@ func TestStreamResources_Server_KeepsConnectionOpenWithHeartbeat(t *testing.T) { incomingHeartbeatTimeout := 10 * time.Millisecond srv, store := newTestServer(t, func(c *Config) { - c.Tracker.SetClock(it.Now) c.incomingHeartbeatTimeout = incomingHeartbeatTimeout }) + srv.Tracker.setClock(it.Now) p := writePeeringToBeDialed(t, store, 1, "my-peer") require.Empty(t, p.PeerID, "should be empty if being dialed") @@ -1314,7 +1316,7 @@ func TestStreamResources_Server_KeepsConnectionOpenWithHeartbeat(t *testing.T) { // makeClient sets up a *MockClient with the initial subscription // message handshake. -func makeClient(t *testing.T, srv pbpeerstream.PeerStreamServiceServer, peerID string) *MockClient { +func makeClient(t *testing.T, srv *testServer, peerID string) *MockClient { t.Helper() client := NewMockClient(context.Background()) @@ -1326,7 +1328,7 @@ func makeClient(t *testing.T, srv pbpeerstream.PeerStreamServiceServer, peerID s // Pass errors from server handler into ErrCh so that they can be seen by the client on Recv(). // This matches gRPC's behavior when an error is returned by a server. if err := srv.StreamResources(client.ReplicationStream); err != nil { - errCh <- srv.StreamResources(client.ReplicationStream) + errCh <- err } }() @@ -1345,11 +1347,19 @@ func makeClient(t *testing.T, srv pbpeerstream.PeerStreamServiceServer, peerID s require.NoError(t, err) receivedSub2, err := client.Recv() require.NoError(t, err) + receivedSub3, err := client.Recv() + require.NoError(t, err) - // Issue a services and roots subscription pair to server + // This is required when the client subscribes to server address replication messages. + // We assert for the handler to be called at least once but the data doesn't matter. + srv.mockSnapshotHandler.expect("", 0, 0, nil) + + // Issue services, roots, and server address subscription to server. + // Note that server address may not come as an initial message for _, resourceURL := range []string{ pbpeerstream.TypeURLExportedService, pbpeerstream.TypeURLPeeringTrustBundle, + pbpeerstream.TypeURLPeeringServerAddresses, } { init := &pbpeerstream.ReplicationMessage{ Payload: &pbpeerstream.ReplicationMessage_Request_{ @@ -1385,10 +1395,22 @@ func makeClient(t *testing.T, srv pbpeerstream.PeerStreamServiceServer, peerID s }, }, }, + { + Payload: &pbpeerstream.ReplicationMessage_Request_{ + Request: &pbpeerstream.ReplicationMessage_Request{ + ResourceURL: pbpeerstream.TypeURLPeeringServerAddresses, + // The PeerID field is only set for the messages coming FROM + // the establishing side and are going to be empty from the + // other side. + PeerID: "", + }, + }, + }, } got := []*pbpeerstream.ReplicationMessage{ receivedSub1, receivedSub2, + receivedSub3, } prototest.AssertElementsMatch(t, expect, got) @@ -1445,6 +1467,10 @@ func (b *testStreamBackend) PeeringSecretsWrite(req *pbpeering.SecretsWriteReque return b.store.PeeringSecretsWrite(1, req) } +func (b *testStreamBackend) PeeringWrite(req *pbpeering.PeeringWriteRequest) error { + return b.store.PeeringWrite(1, req) +} + // CatalogRegister mocks catalog registrations through Raft by copying the logic of FSM.applyRegister. func (b *testStreamBackend) CatalogRegister(req *structs.RegisterRequest) error { return b.store.EnsureRegistration(1, req) @@ -1498,7 +1524,7 @@ func Test_makeServiceResponse_ExportedServicesCount(t *testing.T) { }, }, }} - _, err := makeServiceResponse(srv.Logger, mst, update) + _, err := makeServiceResponse(mst, update) require.NoError(t, err) require.Equal(t, 1, mst.GetExportedServicesCount()) @@ -1510,7 +1536,7 @@ func Test_makeServiceResponse_ExportedServicesCount(t *testing.T) { Result: &pbservice.IndexedCheckServiceNodes{ Nodes: []*pbservice.CheckServiceNode{}, }} - _, err := makeServiceResponse(srv.Logger, mst, update) + _, err := makeServiceResponse(mst, update) require.NoError(t, err) require.Equal(t, 0, mst.GetExportedServicesCount()) @@ -1541,7 +1567,7 @@ func Test_processResponse_Validation(t *testing.T) { require.NoError(t, err) run := func(t *testing.T, tc testCase) { - reply, err := srv.processResponse(peerName, "", mst, tc.in, srv.Logger) + reply, err := srv.processResponse(peerName, "", mst, tc.in) if tc.wantErr { require.Error(t, err) } else { @@ -1867,7 +1893,7 @@ func Test_processResponse_handleUpsert_handleDelete(t *testing.T) { } // Simulate an update arriving for billing/api. - _, err = srv.processResponse(peerName, acl.DefaultPartitionName, mst, in, srv.Logger) + _, err = srv.processResponse(peerName, acl.DefaultPartitionName, mst, in) require.NoError(t, err) for svc, expect := range tc.expect { @@ -2733,11 +2759,16 @@ func requireEqualInstances(t *testing.T, expect, got structs.CheckServiceNodes) type testServer struct { *Server + + // mockSnapshotHandler is solely used for handling autopilot events + // which don't come from the state store. + mockSnapshotHandler *mockSnapshotHandler } func newTestServer(t *testing.T, configFn func(c *Config)) (*testServer, *state.Store) { + t.Helper() publisher := stream.NewEventPublisher(10 * time.Second) - store := newStateStore(t, publisher) + store, handler := newStateStore(t, publisher) ports := freeport.GetN(t, 1) // {grpc} @@ -2746,7 +2777,6 @@ func newTestServer(t *testing.T, configFn func(c *Config)) (*testServer, *state. store: store, pub: publisher, }, - Tracker: NewTracker(), GetStore: func() StateStore { return store }, Logger: testutil.Logger(t), Datacenter: "dc1", @@ -2774,7 +2804,8 @@ func newTestServer(t *testing.T, configFn func(c *Config)) (*testServer, *state. t.Cleanup(grpcServer.Stop) return &testServer{ - Server: srv, + Server: srv, + mockSnapshotHandler: handler, }, store } diff --git a/agent/grpc-external/services/peerstream/stream_tracker.go b/agent/grpc-external/services/peerstream/stream_tracker.go index f7a451595..c3108e71e 100644 --- a/agent/grpc-external/services/peerstream/stream_tracker.go +++ b/agent/grpc-external/services/peerstream/stream_tracker.go @@ -14,18 +14,27 @@ type Tracker struct { mu sync.RWMutex streams map[string]*MutableStatus + // heartbeatTimeout is the max duration a connection is allowed to be + // disconnected before the stream health is reported as non-healthy + heartbeatTimeout time.Duration + // timeNow is a shim for testing. timeNow func() time.Time } -func NewTracker() *Tracker { +func NewTracker(heartbeatTimeout time.Duration) *Tracker { + if heartbeatTimeout == 0 { + heartbeatTimeout = defaultIncomingHeartbeatTimeout + } return &Tracker{ - streams: make(map[string]*MutableStatus), - timeNow: time.Now, + streams: make(map[string]*MutableStatus), + timeNow: time.Now, + heartbeatTimeout: heartbeatTimeout, } } -func (t *Tracker) SetClock(clock func() time.Time) { +// setClock is used for debugging purposes only. +func (t *Tracker) setClock(clock func() time.Time) { if clock == nil { t.timeNow = time.Now } else { @@ -101,7 +110,9 @@ func (t *Tracker) StreamStatus(id string) (resp Status, found bool) { s, ok := t.streams[id] if !ok { - return Status{}, false + return Status{ + NeverConnected: true, + }, false } return s.GetStatus(), true } @@ -126,6 +137,39 @@ func (t *Tracker) DeleteStatus(id string) { delete(t.streams, id) } +// IsHealthy is a calculates the health of a peering status. +// We define a peering as unhealthy if its status has been in the following +// states for longer than the configured incomingHeartbeatTimeout. +// - If it is disconnected +// - If the last received Nack is newer than last received Ack +// - If the last received error is newer than last received success +// +// If none of these conditions apply, we call the peering healthy. +func (t *Tracker) IsHealthy(s Status) bool { + // If stream is in a disconnected state for longer than the configured + // heartbeat timeout, report as unhealthy. + if !s.DisconnectTime.IsZero() && + t.timeNow().Sub(s.DisconnectTime) > t.heartbeatTimeout { + return false + } + + // If last Nack is after last Ack, it means the peer is unable to + // handle our replication message. + if s.LastNack.After(s.LastAck) && + t.timeNow().Sub(s.LastAck) > t.heartbeatTimeout { + return false + } + + // If last recv error is newer than last recv success, we were unable + // to handle the peer's replication message. + if s.LastRecvError.After(s.LastRecvResourceSuccess) && + t.timeNow().Sub(s.LastRecvError) > t.heartbeatTimeout { + return false + } + + return true +} + type MutableStatus struct { mu sync.RWMutex @@ -145,6 +189,9 @@ type Status struct { // Connected is true when there is an open stream for the peer. Connected bool + // NeverConnected is true for peerings that have never connected, false otherwise. + NeverConnected bool + // DisconnectErrorMessage tracks the error that caused the stream to disconnect non-gracefully. // If the stream is connected or it disconnected gracefully it will be empty. DisconnectErrorMessage string @@ -199,7 +246,8 @@ func (s *Status) GetExportedServicesCount() uint64 { func newMutableStatus(now func() time.Time, connected bool) *MutableStatus { return &MutableStatus{ Status: Status{ - Connected: connected, + Connected: connected, + NeverConnected: !connected, }, timeNow: now, doneCh: make(chan struct{}), diff --git a/agent/grpc-external/services/peerstream/stream_tracker_test.go b/agent/grpc-external/services/peerstream/stream_tracker_test.go index f7a9df321..bb018b4b4 100644 --- a/agent/grpc-external/services/peerstream/stream_tracker_test.go +++ b/agent/grpc-external/services/peerstream/stream_tracker_test.go @@ -5,13 +5,117 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/sdk/testutil" ) +const ( + aPeerID = "63b60245-c475-426b-b314-4588d210859d" +) + +func TestTracker_IsHealthy(t *testing.T) { + type testcase struct { + name string + tracker *Tracker + modifierFunc func(status *MutableStatus) + expectedVal bool + } + + tcs := []testcase{ + { + name: "disconnect time within timeout", + tracker: NewTracker(defaultIncomingHeartbeatTimeout), + expectedVal: true, + modifierFunc: func(status *MutableStatus) { + status.DisconnectTime = time.Now() + }, + }, + { + name: "disconnect time past timeout", + tracker: NewTracker(1 * time.Millisecond), + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + status.DisconnectTime = time.Now().Add(-1 * time.Minute) + }, + }, + { + name: "receive error before receive success within timeout", + tracker: NewTracker(defaultIncomingHeartbeatTimeout), + expectedVal: true, + modifierFunc: func(status *MutableStatus) { + now := time.Now() + status.LastRecvResourceSuccess = now + status.LastRecvError = now.Add(1 * time.Second) + }, + }, + { + name: "receive error before receive success within timeout", + tracker: NewTracker(defaultIncomingHeartbeatTimeout), + expectedVal: true, + modifierFunc: func(status *MutableStatus) { + now := time.Now() + status.LastRecvResourceSuccess = now + status.LastRecvError = now.Add(1 * time.Second) + }, + }, + { + name: "receive error before receive success past timeout", + tracker: NewTracker(1 * time.Millisecond), + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + now := time.Now().Add(-2 * time.Second) + status.LastRecvResourceSuccess = now + status.LastRecvError = now.Add(1 * time.Second) + }, + }, + { + name: "nack before ack within timeout", + tracker: NewTracker(defaultIncomingHeartbeatTimeout), + expectedVal: true, + modifierFunc: func(status *MutableStatus) { + now := time.Now() + status.LastAck = now + status.LastNack = now.Add(1 * time.Second) + }, + }, + { + name: "nack before ack past timeout", + tracker: NewTracker(1 * time.Millisecond), + expectedVal: false, + modifierFunc: func(status *MutableStatus) { + now := time.Now().Add(-2 * time.Second) + status.LastAck = now + status.LastNack = now.Add(1 * time.Second) + }, + }, + { + name: "healthy", + tracker: NewTracker(defaultIncomingHeartbeatTimeout), + expectedVal: true, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tracker := tc.tracker + + st, err := tracker.Connected(aPeerID) + require.NoError(t, err) + require.True(t, st.Connected) + + if tc.modifierFunc != nil { + tc.modifierFunc(st) + } + + assert.Equal(t, tc.expectedVal, tracker.IsHealthy(st.GetStatus())) + }) + } +} + func TestTracker_EnsureConnectedDisconnected(t *testing.T) { - tracker := NewTracker() + tracker := NewTracker(defaultIncomingHeartbeatTimeout) peerID := "63b60245-c475-426b-b314-4588d210859d" it := incrementalTime{ @@ -96,7 +200,7 @@ func TestTracker_EnsureConnectedDisconnected(t *testing.T) { status, ok := tracker.StreamStatus(peerID) require.False(t, ok) - require.Zero(t, status) + require.Equal(t, Status{NeverConnected: true}, status) }) } @@ -108,7 +212,7 @@ func TestTracker_connectedStreams(t *testing.T) { } run := func(t *testing.T, tc testCase) { - tracker := NewTracker() + tracker := NewTracker(defaultIncomingHeartbeatTimeout) if tc.setup != nil { tc.setup(t, tracker) } diff --git a/agent/grpc-external/services/peerstream/subscription_manager.go b/agent/grpc-external/services/peerstream/subscription_manager.go index 0c69b0338..138449e71 100644 --- a/agent/grpc-external/services/peerstream/subscription_manager.go +++ b/agent/grpc-external/services/peerstream/subscription_manager.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/golang/protobuf/proto" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/consul/autopilotevents" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/structs" @@ -42,6 +44,7 @@ type subscriptionManager struct { getStore func() StateStore serviceSubReady <-chan struct{} trustBundlesSubReady <-chan struct{} + serverAddrsSubReady <-chan struct{} } // TODO(peering): Maybe centralize so that there is a single manager per datacenter, rather than per peering. @@ -67,6 +70,7 @@ func newSubscriptionManager( getStore: getStore, serviceSubReady: remoteSubTracker.SubscribedChan(pbpeerstream.TypeURLExportedService), trustBundlesSubReady: remoteSubTracker.SubscribedChan(pbpeerstream.TypeURLPeeringTrustBundle), + serverAddrsSubReady: remoteSubTracker.SubscribedChan(pbpeerstream.TypeURLPeeringServerAddresses), } } @@ -83,6 +87,7 @@ func (m *subscriptionManager) subscribe(ctx context.Context, peerID, peerName, p // Wrap our bare state store queries in goroutines that emit events. go m.notifyExportedServicesForPeerID(ctx, state, peerID) + go m.notifyServerAddrUpdates(ctx, state.updateCh) if m.config.ConnectEnabled { go m.notifyMeshGatewaysForPartition(ctx, state, state.partition) // If connect is enabled, watch for updates to CA roots. @@ -262,6 +267,17 @@ func (m *subscriptionManager) handleEvent(ctx context.Context, state *subscripti state.sendPendingEvents(ctx, m.logger, pending) + case u.CorrelationID == subServerAddrs: + addrs, ok := u.Result.(*pbpeering.PeeringServerAddresses) + if !ok { + return fmt.Errorf("invalid type for response: %T", u.Result) + } + pending := &pendingPayload{} + if err := pending.Add(serverAddrsPayloadID, u.CorrelationID, addrs); err != nil { + return err + } + + state.sendPendingEvents(ctx, m.logger, pending) default: return fmt.Errorf("unknown correlation ID: %s", u.CorrelationID) } @@ -333,6 +349,8 @@ func (m *subscriptionManager) notifyRootCAUpdatesForPartition( } } +const subCARoot = "roots" + // subscribeCARoots subscribes to state.EventTopicCARoots for changes to CA roots. // Upon receiving an event it will send the payload in updateCh. func (m *subscriptionManager) subscribeCARoots( @@ -414,8 +432,6 @@ func (m *subscriptionManager) subscribeCARoots( } } -const subCARoot = "roots" - func (m *subscriptionManager) syncNormalServices( ctx context.Context, state *subscriptionState, @@ -721,3 +737,112 @@ const syntheticProxyNameSuffix = "-sidecar-proxy" func generateProxyNameForDiscoveryChain(sn structs.ServiceName) structs.ServiceName { return structs.NewServiceName(sn.Name+syntheticProxyNameSuffix, &sn.EnterpriseMeta) } + +const subServerAddrs = "server-addrs" + +func (m *subscriptionManager) notifyServerAddrUpdates( + ctx context.Context, + updateCh chan<- cache.UpdateEvent, +) { + // Wait until this is subscribed-to. + select { + case <-m.serverAddrsSubReady: + case <-ctx.Done(): + return + } + + var idx uint64 + // TODO(peering): retry logic; fail past a threshold + for { + var err error + // Typically, this function will block inside `m.subscribeServerAddrs` and only return on error. + // Errors are logged and the watch is retried. + idx, err = m.subscribeServerAddrs(ctx, idx, updateCh) + if errors.Is(err, stream.ErrSubForceClosed) { + m.logger.Trace("subscription force-closed due to an ACL change or snapshot restore, will attempt resume") + } else if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { + m.logger.Warn("failed to subscribe to server addresses, will attempt resume", "error", err.Error()) + } else { + m.logger.Trace(err.Error()) + } + + select { + case <-ctx.Done(): + return + default: + } + } +} + +func (m *subscriptionManager) subscribeServerAddrs( + ctx context.Context, + idx uint64, + updateCh chan<- cache.UpdateEvent, +) (uint64, error) { + // following code adapted from serverdiscovery/watch_servers.go + sub, err := m.backend.Subscribe(&stream.SubscribeRequest{ + Topic: autopilotevents.EventTopicReadyServers, + Subject: stream.SubjectNone, + Token: "", // using anonymous token for now + Index: idx, + }) + if err != nil { + return 0, fmt.Errorf("failed to subscribe to ReadyServers events: %w", err) + } + defer sub.Unsubscribe() + + for { + event, err := sub.Next(ctx) + switch { + case errors.Is(err, context.Canceled): + return 0, err + case err != nil: + return idx, err + } + + // We do not send framing events (e.g. EndOfSnapshot, NewSnapshotToFollow) + // because we send a full list of ready servers on every event, rather than expecting + // clients to maintain a state-machine in the way they do for service health. + if event.IsFramingEvent() { + continue + } + + // Note: this check isn't strictly necessary because the event publishing + // machinery will ensure the index increases monotonically, but it can be + // tricky to faithfully reproduce this in tests (e.g. the EventPublisher + // garbage collects topic buffers and snapshots aggressively when streams + // disconnect) so this avoids a bunch of confusing setup code. + if event.Index <= idx { + continue + } + + idx = event.Index + + payload, ok := event.Payload.(autopilotevents.EventPayloadReadyServers) + if !ok { + return 0, fmt.Errorf("unexpected event payload type: %T", payload) + } + + var serverAddrs = make([]string, 0, len(payload)) + + for _, srv := range payload { + if srv.ExtGRPCPort == 0 { + continue + } + grpcAddr := srv.Address + ":" + strconv.Itoa(srv.ExtGRPCPort) + serverAddrs = append(serverAddrs, grpcAddr) + } + + if len(serverAddrs) == 0 { + m.logger.Warn("did not find any server addresses with external gRPC ports to publish") + continue + } + + updateCh <- cache.UpdateEvent{ + CorrelationID: subServerAddrs, + Result: &pbpeering.PeeringServerAddresses{ + Addresses: serverAddrs, + }, + } + } +} diff --git a/agent/grpc-external/services/peerstream/subscription_manager_test.go b/agent/grpc-external/services/peerstream/subscription_manager_test.go index 03b89dbcc..d81568f0a 100644 --- a/agent/grpc-external/services/peerstream/subscription_manager_test.go +++ b/agent/grpc-external/services/peerstream/subscription_manager_test.go @@ -3,14 +3,17 @@ package peerstream import ( "context" "sort" + "sync" "testing" "time" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/consul/autopilotevents" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/structs" @@ -627,20 +630,100 @@ func TestSubscriptionManager_CARoots(t *testing.T) { }) } +func TestSubscriptionManager_ServerAddrs(t *testing.T) { + backend := newTestSubscriptionBackend(t) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + // Create a peering + _, id := backend.ensurePeering(t, "my-peering") + partition := acl.DefaultEnterpriseMeta().PartitionOrEmpty() + + payload := autopilotevents.EventPayloadReadyServers{ + autopilotevents.ReadyServerInfo{ + ID: "9aeb73f6-e83e-43c1-bdc9-ca5e43efe3e4", + Address: "198.18.0.1", + Version: "1.13.1", + ExtGRPCPort: 8502, + }, + } + // mock handler only gets called once during the initial subscription + backend.handler.expect("", 0, 1, payload) + + // Only configure a tracker for server address events. + tracker := newResourceSubscriptionTracker() + tracker.Subscribe(pbpeerstream.TypeURLPeeringServerAddresses) + + mgr := newSubscriptionManager(ctx, + testutil.Logger(t), + Config{ + Datacenter: "dc1", + ConnectEnabled: true, + }, + connect.TestTrustDomain, + backend, + func() StateStore { + return backend.store + }, + tracker) + subCh := mgr.subscribe(ctx, id, "my-peering", partition) + + testutil.RunStep(t, "initial events", func(t *testing.T) { + expectEvents(t, subCh, + func(t *testing.T, got cache.UpdateEvent) { + require.Equal(t, subServerAddrs, got.CorrelationID) + addrs, ok := got.Result.(*pbpeering.PeeringServerAddresses) + require.True(t, ok) + + require.Equal(t, []string{"198.18.0.1:8502"}, addrs.GetAddresses()) + }, + ) + }) + + testutil.RunStep(t, "added server", func(t *testing.T) { + payload = append(payload, autopilotevents.ReadyServerInfo{ + ID: "eec8721f-c42b-48da-a5a5-07565158015e", + Address: "198.18.0.2", + Version: "1.13.1", + ExtGRPCPort: 9502, + }) + backend.Publish([]stream.Event{ + { + Topic: autopilotevents.EventTopicReadyServers, + Index: 2, + Payload: payload, + }, + }) + + expectEvents(t, subCh, + func(t *testing.T, got cache.UpdateEvent) { + require.Equal(t, subServerAddrs, got.CorrelationID) + addrs, ok := got.Result.(*pbpeering.PeeringServerAddresses) + require.True(t, ok) + + require.Equal(t, []string{"198.18.0.1:8502", "198.18.0.2:9502"}, addrs.GetAddresses()) + }, + ) + }) +} + type testSubscriptionBackend struct { state.EventPublisher - store *state.Store + store *state.Store + handler *mockSnapshotHandler lastIdx uint64 } func newTestSubscriptionBackend(t *testing.T) *testSubscriptionBackend { publisher := stream.NewEventPublisher(10 * time.Second) - store := newStateStore(t, publisher) + store, handler := newStateStore(t, publisher) backend := &testSubscriptionBackend{ EventPublisher: publisher, store: store, + handler: handler, } backend.ensureCAConfig(t, &structs.CAConfiguration{ @@ -739,20 +822,35 @@ func setupTestPeering(t *testing.T, store *state.Store, name string, index uint6 return p.ID } -func newStateStore(t *testing.T, publisher *stream.EventPublisher) *state.Store { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - +func newStateStore(t *testing.T, publisher *stream.EventPublisher) (*state.Store, *mockSnapshotHandler) { gc, err := state.NewTombstoneGC(time.Second, time.Millisecond) require.NoError(t, err) + handler := newMockSnapshotHandler(t) + store := state.NewStateStoreWithEventPublisher(gc, publisher) require.NoError(t, publisher.RegisterHandler(state.EventTopicServiceHealth, store.ServiceHealthSnapshot, false)) require.NoError(t, publisher.RegisterHandler(state.EventTopicServiceHealthConnect, store.ServiceHealthSnapshot, false)) require.NoError(t, publisher.RegisterHandler(state.EventTopicCARoots, store.CARootsSnapshot, false)) - go publisher.Run(ctx) + require.NoError(t, publisher.RegisterHandler(autopilotevents.EventTopicReadyServers, handler.handle, false)) - return store + // WaitGroup used to make sure that the publisher returns + // before handler's t.Cleanup is called (otherwise an event + // might fire during an assertion and cause a data race). + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancel() + wg.Wait() + }) + + wg.Add(1) + go func() { + publisher.Run(ctx) + wg.Done() + }() + + return store, handler } func expectEvents( @@ -870,3 +968,39 @@ func pbCheck(node, svcID, svcName, status string, entMeta *pbcommon.EnterpriseMe EnterpriseMeta: entMeta, } } + +// mockSnapshotHandler is copied from server_discovery/server_test.go +type mockSnapshotHandler struct { + mock.Mock +} + +func newMockSnapshotHandler(t *testing.T) *mockSnapshotHandler { + handler := &mockSnapshotHandler{} + t.Cleanup(func() { + handler.AssertExpectations(t) + }) + return handler +} + +func (m *mockSnapshotHandler) handle(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) { + ret := m.Called(req, buf) + return ret.Get(0).(uint64), ret.Error(1) +} + +func (m *mockSnapshotHandler) expect(token string, requestIndex uint64, eventIndex uint64, payload autopilotevents.EventPayloadReadyServers) { + m.On("handle", stream.SubscribeRequest{ + Topic: autopilotevents.EventTopicReadyServers, + Subject: stream.SubjectNone, + Token: token, + Index: requestIndex, + }, mock.Anything).Run(func(args mock.Arguments) { + buf := args.Get(1).(stream.SnapshotAppender) + buf.Append([]stream.Event{ + { + Topic: autopilotevents.EventTopicReadyServers, + Index: eventIndex, + Payload: payload, + }, + }) + }).Return(eventIndex, nil) +} diff --git a/agent/grpc-external/services/peerstream/subscription_state.go b/agent/grpc-external/services/peerstream/subscription_state.go index 58e631f70..9e32be545 100644 --- a/agent/grpc-external/services/peerstream/subscription_state.go +++ b/agent/grpc-external/services/peerstream/subscription_state.go @@ -93,6 +93,9 @@ func (s *subscriptionState) cleanupEventVersions(logger hclog.Logger) { case id == caRootsPayloadID: keep = true + case id == serverAddrsPayloadID: + keep = true + case strings.HasPrefix(id, servicePayloadIDPrefix): name := strings.TrimPrefix(id, servicePayloadIDPrefix) sn := structs.ServiceNameFromString(name) @@ -129,6 +132,7 @@ type pendingEvent struct { } const ( + serverAddrsPayloadID = "server-addrs" caRootsPayloadID = "roots" meshGatewayPayloadID = "mesh-gateway" servicePayloadIDPrefix = "service:" diff --git a/agent/http.go b/agent/http.go index 98beb6feb..ff7764f0d 100644 --- a/agent/http.go +++ b/agent/http.go @@ -81,6 +81,10 @@ type HTTPHandlers struct { configReloaders []ConfigReloader h http.Handler metricsProxyCfg atomic.Value + + // proxyTransport is used by UIMetricsProxy to keep + // a managed pool of connections. + proxyTransport http.RoundTripper } // endpoint is a Consul-specific HTTP handler that takes the usual arguments in diff --git a/agent/http_test.go b/agent/http_test.go index a42a93230..60b0bd97f 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -148,7 +148,7 @@ func TestSetupHTTPServer_HTTP2(t *testing.T) { // Fire up an agent with TLS enabled. a := StartTestAgent(t, TestAgent{ - UseTLS: true, + UseHTTPS: true, HCL: ` key_file = "../test/client_certs/server.key" cert_file = "../test/client_certs/server.crt" @@ -1549,7 +1549,7 @@ func TestHTTPServer_HandshakeTimeout(t *testing.T) { // Fire up an agent with TLS enabled. a := StartTestAgent(t, TestAgent{ - UseTLS: true, + UseHTTPS: true, HCL: ` key_file = "../test/client_certs/server.key" cert_file = "../test/client_certs/server.crt" @@ -1621,7 +1621,7 @@ func TestRPC_HTTPSMaxConnsPerClient(t *testing.T) { // Fire up an agent with TLS enabled. a := StartTestAgent(t, TestAgent{ - UseTLS: tc.tlsEnabled, + UseHTTPS: tc.tlsEnabled, HCL: hclPrefix + ` limits { http_max_conns_per_client = 2 diff --git a/agent/metadata/server.go b/agent/metadata/server.go index 83997f7cd..bb0c26336 100644 --- a/agent/metadata/server.go +++ b/agent/metadata/server.go @@ -23,26 +23,27 @@ func (k *Key) Equal(x *Key) bool { // Server is used to return details of a consul server type Server struct { - Name string // . - ShortName string // - ID string - Datacenter string - Segment string - Port int - SegmentAddrs map[string]string - SegmentPorts map[string]int - WanJoinPort int - LanJoinPort int - ExternalGRPCPort int - Bootstrap bool - Expect int - Build version.Version - Version int - RaftVersion int - Addr net.Addr - Status serf.MemberStatus - ReadReplica bool - FeatureFlags map[string]int + Name string // . + ShortName string // + ID string + Datacenter string + Segment string + Port int + SegmentAddrs map[string]string + SegmentPorts map[string]int + WanJoinPort int + LanJoinPort int + ExternalGRPCPort int + ExternalGRPCTLSPort int + Bootstrap bool + Expect int + Build version.Version + Version int + RaftVersion int + Addr net.Addr + Status serf.MemberStatus + ReadReplica bool + FeatureFlags map[string]int // If true, use TLS when connecting to this server UseTLS bool @@ -137,14 +138,18 @@ func IsConsulServer(m serf.Member) (bool, *Server) { } } - externalGRPCPort := 0 - externalGRPCPortStr, ok := m.Tags["grpc_port"] - if ok { - externalGRPCPort, err = strconv.Atoi(externalGRPCPortStr) - if err != nil { - return false, nil - } - if externalGRPCPort < 1 { + var externalGRPCPort, externalGRPCTLSPort int + externalGRPCPortStr, foundGRPC := m.Tags["grpc_port"] + externalGRPCTLSPortStr, foundGRPCTLS := m.Tags["grpc_tls_port"] + if foundGRPC { + externalGRPCPort, _ = strconv.Atoi(externalGRPCPortStr) + } + if foundGRPCTLS { + externalGRPCTLSPort, _ = strconv.Atoi(externalGRPCTLSPortStr) + } + // If either port tag was found, check to ensure that at least one port was valid. + if foundGRPC || foundGRPCTLS { + if externalGRPCPort < 1 && externalGRPCTLSPort < 1 { return false, nil } } @@ -173,25 +178,26 @@ func IsConsulServer(m serf.Member) (bool, *Server) { addr := &net.TCPAddr{IP: m.Addr, Port: port} parts := &Server{ - Name: m.Name, - ShortName: strings.TrimSuffix(m.Name, "."+datacenter), - ID: m.Tags["id"], - Datacenter: datacenter, - Segment: segment, - Port: port, - SegmentAddrs: segmentAddrs, - SegmentPorts: segmentPorts, - WanJoinPort: wanJoinPort, - LanJoinPort: int(m.Port), - ExternalGRPCPort: externalGRPCPort, - Bootstrap: bootstrap, - Expect: expect, - Addr: addr, - Build: *buildVersion, - Version: vsn, - RaftVersion: raftVsn, - Status: m.Status, - UseTLS: useTLS, + Name: m.Name, + ShortName: strings.TrimSuffix(m.Name, "."+datacenter), + ID: m.Tags["id"], + Datacenter: datacenter, + Segment: segment, + Port: port, + SegmentAddrs: segmentAddrs, + SegmentPorts: segmentPorts, + WanJoinPort: wanJoinPort, + LanJoinPort: int(m.Port), + ExternalGRPCPort: externalGRPCPort, + ExternalGRPCTLSPort: externalGRPCTLSPort, + Bootstrap: bootstrap, + Expect: expect, + Addr: addr, + Build: *buildVersion, + Version: vsn, + RaftVersion: raftVsn, + Status: m.Status, + UseTLS: useTLS, // DEPRECATED - remove nonVoter check once support for that tag is removed ReadReplica: nonVoter || readReplica, FeatureFlags: featureFlags, diff --git a/agent/metadata/server_test.go b/agent/metadata/server_test.go index 2f56bd7fd..691302122 100644 --- a/agent/metadata/server_test.go +++ b/agent/metadata/server_test.go @@ -73,6 +73,7 @@ func TestIsConsulServer(t *testing.T) { "build": "0.8.0", "wan_join_port": "1234", "grpc_port": "9876", + "grpc_tls_port": "9877", "vsn": "1", "expect": "3", "raft_vsn": "3", @@ -82,19 +83,20 @@ func TestIsConsulServer(t *testing.T) { } expected := &metadata.Server{ - Name: "foo", - ShortName: "foo", - ID: "asdf", - Datacenter: "east-aws", - Segment: "", - Port: 10000, - SegmentAddrs: map[string]string{}, - SegmentPorts: map[string]int{}, - WanJoinPort: 1234, - LanJoinPort: 5454, - ExternalGRPCPort: 9876, - Bootstrap: false, - Expect: 3, + Name: "foo", + ShortName: "foo", + ID: "asdf", + Datacenter: "east-aws", + Segment: "", + Port: 10000, + SegmentAddrs: map[string]string{}, + SegmentPorts: map[string]int{}, + WanJoinPort: 1234, + LanJoinPort: 5454, + ExternalGRPCPort: 9876, + ExternalGRPCTLSPort: 9877, + Bootstrap: false, + Expect: 3, Addr: &net.TCPAddr{ IP: net.IP([]byte{127, 0, 0, 1}), Port: 10000, @@ -137,15 +139,41 @@ func TestIsConsulServer(t *testing.T) { case "feature-namespaces": m.Tags["ft_ns"] = "1" expected.FeatureFlags = map[string]int{"ns": 1} - // - case "bad-grpc-port": - m.Tags["grpc_port"] = "three" - case "negative-grpc-port": - m.Tags["grpc_port"] = "-1" - case "zero-grpc-port": - m.Tags["grpc_port"] = "0" case "no-role": delete(m.Tags, "role") + // + case "missing-grpc-port": + delete(m.Tags, "grpc_port") + expected.ExternalGRPCPort = 0 + case "missing-grpc-tls-port": + delete(m.Tags, "grpc_tls_port") + expected.ExternalGRPCTLSPort = 0 + case "missing-both-grpc-ports": + delete(m.Tags, "grpc_port") + delete(m.Tags, "grpc_tls_port") + expected.ExternalGRPCPort = 0 + expected.ExternalGRPCTLSPort = 0 + case "bad-both-grpc-ports": + m.Tags["grpc_port"] = "" + m.Tags["grpc_tls_port"] = "" + case "bad-grpc-port": + m.Tags["grpc_port"] = "three" + m.Tags["grpc_tls_port"] = "" + case "bad-grpc-tls-port": + m.Tags["grpc_port"] = "" + m.Tags["grpc_tls_port"] = "three" + case "negative-grpc-port": + m.Tags["grpc_port"] = "-1" + m.Tags["grpc_tls_port"] = "" + case "negative-grpc-tls-port": + m.Tags["grpc_port"] = "" + m.Tags["grpc_tls_port"] = "-1" + case "zero-grpc-port": + m.Tags["grpc_port"] = "0" + m.Tags["grpc_tls_port"] = "" + case "zero-grpc-tls-port": + m.Tags["grpc_port"] = "" + m.Tags["grpc_tls_port"] = "0" default: t.Fatalf("unhandled variant: %s", variant) } @@ -174,11 +202,18 @@ func TestIsConsulServer(t *testing.T) { "bootstrapped": true, "optionals": true, "feature-namespaces": true, - // "no-role": false, - "bad-grpc-port": false, - "negative-grpc-port": false, - "zero-grpc-port": false, + // + "missing-grpc-port": true, + "missing-grpc-tls-port": true, + "missing-both-grpc-ports": true, + "bad-both-grpc-ports": false, + "bad-grpc-port": false, + "negative-grpc-port": false, + "zero-grpc-port": false, + "bad-grpc-tls-port": false, + "negative-grpc-tls-port": false, + "zero-grpc-tls-port": false, } for variant, expectOK := range cases { diff --git a/agent/metrics_test.go b/agent/metrics_test.go index fde404713..4b80f9eda 100644 --- a/agent/metrics_test.go +++ b/agent/metrics_test.go @@ -47,7 +47,7 @@ func assertMetricExists(t *testing.T, respRec *httptest.ResponseRecorder, metric } } -// assertMetricExistsWithLabels looks in the prometheus metrics reponse for the metric name and all the labels. eg: +// assertMetricExistsWithLabels looks in the prometheus metrics response for the metric name and all the labels. eg: // new_rpc_metrics_rpc_server_call{errored="false",method="Status.Ping",request_type="unknown",rpc_type="net/rpc"} func assertMetricExistsWithLabels(t *testing.T, respRec *httptest.ResponseRecorder, metric string, labelNames []string) { if respRec.Body.String() == "" { @@ -136,6 +136,28 @@ func assertMetricExistsWithValue(t *testing.T, respRec *httptest.ResponseRecorde } } +func assertMetricsWithLabelIsNonZero(t *testing.T, respRec *httptest.ResponseRecorder, label, labelValue string) { + if respRec.Body.String() == "" { + t.Fatalf("Response body is empty.") + } + + metrics := respRec.Body.String() + labelWithValueTarget := label + "=" + "\"" + labelValue + "\"" + + for _, line := range strings.Split(metrics, "\n") { + if len(line) < 1 || line[0] == '#' { + continue + } + + if strings.Contains(line, labelWithValueTarget) { + s := strings.SplitN(line, " ", 2) + if s[1] == "0" { + t.Fatalf("Metric with label provided \"%s:%s\" has the value 0", label, labelValue) + } + } + } +} + func assertMetricNotExists(t *testing.T, respRec *httptest.ResponseRecorder, metric string) { if respRec.Body.String() == "" { t.Fatalf("Response body is empty.") @@ -205,6 +227,8 @@ func TestAgent_OneTwelveRPCMetrics(t *testing.T) { assertMetricExistsWithLabels(t, respRec, metricsPrefix+"_rpc_server_call", []string{"errored", "method", "request_type", "rpc_type", "leader"}) // make sure we see 3 Status.Ping metrics corresponding to the calls we made above assertLabelWithValueForMetricExistsNTime(t, respRec, metricsPrefix+"_rpc_server_call", "method", "Status.Ping", 3) + // make sure rpc calls with elapsed time below 1ms are reported as decimal + assertMetricsWithLabelIsNonZero(t, respRec, "method", "Status.Ping") }) } diff --git a/agent/peering_endpoint_test.go b/agent/peering_endpoint_test.go index 2c49ee479..5555fde10 100644 --- a/agent/peering_endpoint_test.go +++ b/agent/peering_endpoint_test.go @@ -267,8 +267,8 @@ func TestHTTP_Peering_Establish(t *testing.T) { }) t.Run("Success", func(t *testing.T) { - a2 := NewTestAgent(t, "") - testrpc.WaitForTestAgent(t, a2.RPC, "dc1") + a2 := NewTestAgent(t, `datacenter = "dc2"`) + testrpc.WaitForTestAgent(t, a2.RPC, "dc2") bodyBytes, err := json.Marshal(&pbpeering.GenerateTokenRequest{ PeerName: "foo", diff --git a/agent/proxycfg-glue/config_entry.go b/agent/proxycfg-glue/config_entry.go index 1f6fbf245..1321e352a 100644 --- a/agent/proxycfg-glue/config_entry.go +++ b/agent/proxycfg-glue/config_entry.go @@ -4,11 +4,9 @@ import ( "context" "fmt" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" - "github.com/hashicorp/consul/agent/consul/stream" + cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/submatview" @@ -17,15 +15,16 @@ import ( "github.com/hashicorp/consul/proto/pbsubscribe" ) -// ServerDataSourceDeps contains the dependencies needed for sourcing data from -// server-local sources (e.g. materialized views). -type ServerDataSourceDeps struct { - Datacenter string - ViewStore *submatview.Store - EventPublisher *stream.EventPublisher - Logger hclog.Logger - ACLResolver submatview.ACLResolver - GetStore func() Store +// CacheConfigEntry satisfies the proxycfg.ConfigEntry interface by sourcing +// data from the agent cache. +func CacheConfigEntry(c *cache.Cache) proxycfg.ConfigEntry { + return &cacheProxyDataSource[*structs.ConfigEntryQuery]{c, cachetype.ConfigEntryName} +} + +// CacheConfigEntryList satisfies the proxycfg.ConfigEntryList interface by +// sourcing data from the agent cache. +func CacheConfigEntryList(c *cache.Cache) proxycfg.ConfigEntryList { + return &cacheProxyDataSource[*structs.ConfigEntryQuery]{c, cachetype.ConfigEntryListName} } // ServerConfigEntry satisfies the proxycfg.ConfigEntry interface by sourcing diff --git a/agent/proxycfg-glue/glue.go b/agent/proxycfg-glue/glue.go index 86badf67e..6aef1da54 100644 --- a/agent/proxycfg-glue/glue.go +++ b/agent/proxycfg-glue/glue.go @@ -3,20 +3,35 @@ package proxycfgglue import ( "context" - "github.com/hashicorp/consul/proto/pbpeering" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" + "github.com/hashicorp/consul/proto/pbpeering" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/consul/discoverychain" "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/consul/watch" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/agent/submatview" ) +// ServerDataSourceDeps contains the dependencies needed for sourcing data from +// server-local sources (e.g. materialized views). +type ServerDataSourceDeps struct { + Datacenter string + ViewStore *submatview.Store + EventPublisher *stream.EventPublisher + Logger hclog.Logger + ACLResolver submatview.ACLResolver + GetStore func() Store +} + // Store is the state store interface required for server-local data sources. type Store interface { watch.StateStore @@ -25,7 +40,9 @@ type Store interface { FederationStateList(ws memdb.WatchSet) (uint64, []*structs.FederationState, error) GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl.EnterpriseMeta) (uint64, structs.GatewayServices, error) IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error) + ReadResolvedServiceConfigEntries(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, upstreamIDs []structs.ServiceID, proxyMode structs.ProxyMode) (uint64, *configentry.ResolvedServiceConfigSet, error) ServiceDiscoveryChain(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, req discoverychain.CompileRequest) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error) + ServiceDump(ws memdb.WatchSet, kind structs.ServiceKind, useKind bool, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.CheckServiceNodes, error) PeeringTrustBundleRead(ws memdb.WatchSet, q state.Query) (uint64, *pbpeering.PeeringTrustBundle, error) PeeringTrustBundleList(ws memdb.WatchSet, entMeta acl.EnterpriseMeta) (uint64, []*pbpeering.PeeringTrustBundle, error) TrustBundleListByService(ws memdb.WatchSet, service, dc string, entMeta acl.EnterpriseMeta) (uint64, []*pbpeering.PeeringTrustBundle, error) @@ -34,24 +51,18 @@ type Store interface { // CacheCARoots satisfies the proxycfg.CARoots interface by sourcing data from // the agent cache. +// +// Note: there isn't a server-local equivalent of this data source because +// "agentless" proxies obtain certificates via SDS served by consul-dataplane. func CacheCARoots(c *cache.Cache) proxycfg.CARoots { return &cacheProxyDataSource[*structs.DCSpecificRequest]{c, cachetype.ConnectCARootName} } -// CacheConfigEntry satisfies the proxycfg.ConfigEntry interface by sourcing -// data from the agent cache. -func CacheConfigEntry(c *cache.Cache) proxycfg.ConfigEntry { - return &cacheProxyDataSource[*structs.ConfigEntryQuery]{c, cachetype.ConfigEntryName} -} - -// CacheConfigEntryList satisfies the proxycfg.ConfigEntryList interface by -// sourcing data from the agent cache. -func CacheConfigEntryList(c *cache.Cache) proxycfg.ConfigEntryList { - return &cacheProxyDataSource[*structs.ConfigEntryQuery]{c, cachetype.ConfigEntryListName} -} - // CacheDatacenters satisfies the proxycfg.Datacenters interface by sourcing // data from the agent cache. +// +// Note: there isn't a server-local equivalent of this data source because it +// relies on polling (so a more efficient method isn't available). func CacheDatacenters(c *cache.Cache) proxycfg.Datacenters { return &cacheProxyDataSource[*structs.DatacentersRequest]{c, cachetype.CatalogDatacentersName} } @@ -64,46 +75,31 @@ func CacheServiceGateways(c *cache.Cache) proxycfg.GatewayServices { // CacheHTTPChecks satisifies the proxycfg.HTTPChecks interface by sourcing // data from the agent cache. +// +// Note: there isn't a server-local equivalent of this data source because only +// services registered to the local agent can be health checked by it. func CacheHTTPChecks(c *cache.Cache) proxycfg.HTTPChecks { return &cacheProxyDataSource[*cachetype.ServiceHTTPChecksRequest]{c, cachetype.ServiceHTTPChecksName} } -// CacheIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface -// by sourcing data from the agent cache. -func CacheIntentionUpstreams(c *cache.Cache) proxycfg.IntentionUpstreams { - return &cacheProxyDataSource[*structs.ServiceSpecificRequest]{c, cachetype.IntentionUpstreamsName} -} - -// CacheIntentionUpstreamsDestination satisfies the proxycfg.IntentionUpstreamsDestination interface -// by sourcing data from the agent cache. -func CacheIntentionUpstreamsDestination(c *cache.Cache) proxycfg.IntentionUpstreams { - return &cacheProxyDataSource[*structs.ServiceSpecificRequest]{c, cachetype.IntentionUpstreamsDestinationName} -} - -// CacheInternalServiceDump satisfies the proxycfg.InternalServiceDump -// interface by sourcing data from the agent cache. -func CacheInternalServiceDump(c *cache.Cache) proxycfg.InternalServiceDump { - return &cacheProxyDataSource[*structs.ServiceDumpRequest]{c, cachetype.InternalServiceDumpName} -} - // CacheLeafCertificate satisifies the proxycfg.LeafCertificate interface by // sourcing data from the agent cache. +// +// Note: there isn't a server-local equivalent of this data source because +// "agentless" proxies obtain certificates via SDS served by consul-dataplane. func CacheLeafCertificate(c *cache.Cache) proxycfg.LeafCertificate { return &cacheProxyDataSource[*cachetype.ConnectCALeafRequest]{c, cachetype.ConnectCALeafName} } // CachePrepraredQuery satisfies the proxycfg.PreparedQuery interface by // sourcing data from the agent cache. +// +// Note: there isn't a server-local equivalent of this data source because it +// relies on polling (so a more efficient method isn't available). func CachePrepraredQuery(c *cache.Cache) proxycfg.PreparedQuery { return &cacheProxyDataSource[*structs.PreparedQueryExecuteRequest]{c, cachetype.PreparedQueryName} } -// CacheResolvedServiceConfig satisfies the proxycfg.ResolvedServiceConfig -// interface by sourcing data from the agent cache. -func CacheResolvedServiceConfig(c *cache.Cache) proxycfg.ResolvedServiceConfig { - return &cacheProxyDataSource[*structs.ServiceConfigRequest]{c, cachetype.ResolvedServiceConfigName} -} - // cacheProxyDataSource implements a generic wrapper around the agent cache to // provide data to the proxycfg.Manager. type cacheProxyDataSource[ReqType cache.Request] struct { @@ -124,15 +120,30 @@ func (c *cacheProxyDataSource[ReqType]) Notify( func dispatchCacheUpdate(ch chan<- proxycfg.UpdateEvent) cache.Callback { return func(ctx context.Context, e cache.UpdateEvent) { - u := proxycfg.UpdateEvent{ - CorrelationID: e.CorrelationID, - Result: e.Result, - Err: e.Err, - } - select { - case ch <- u: + case ch <- newUpdateEvent(e.CorrelationID, e.Result, e.Err): case <-ctx.Done(): } } } + +func dispatchBlockingQueryUpdate[ResultType any](ch chan<- proxycfg.UpdateEvent) func(context.Context, string, ResultType, error) { + return func(ctx context.Context, correlationID string, result ResultType, err error) { + select { + case ch <- newUpdateEvent(correlationID, result, err): + case <-ctx.Done(): + } + } +} + +func newUpdateEvent(correlationID string, result any, err error) proxycfg.UpdateEvent { + // This roughly matches the logic in agent/submatview.LocalMaterializer.isTerminalError. + if acl.IsErrNotFound(err) { + err = proxycfg.TerminalError(err) + } + return proxycfg.UpdateEvent{ + CorrelationID: correlationID, + Result: result, + Err: err, + } +} diff --git a/agent/proxycfg-glue/intention_upstreams.go b/agent/proxycfg-glue/intention_upstreams.go index 186d91b35..e5d5e9959 100644 --- a/agent/proxycfg-glue/intention_upstreams.go +++ b/agent/proxycfg-glue/intention_upstreams.go @@ -5,20 +5,45 @@ import ( "github.com/hashicorp/go-memdb" + "github.com/hashicorp/consul/agent/cache" + cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/consul/watch" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs/aclfilter" ) +// CacheIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface +// by sourcing upstreams for the given service, inferred from intentions, from +// the agent cache. +func CacheIntentionUpstreams(c *cache.Cache) proxycfg.IntentionUpstreams { + return &cacheProxyDataSource[*structs.ServiceSpecificRequest]{c, cachetype.IntentionUpstreamsName} +} + +// CacheIntentionUpstreamsDestination satisfies the proxycfg.IntentionUpstreams +// interface by sourcing upstreams for the given destination, inferred from +// intentions, from the agent cache. +func CacheIntentionUpstreamsDestination(c *cache.Cache) proxycfg.IntentionUpstreams { + return &cacheProxyDataSource[*structs.ServiceSpecificRequest]{c, cachetype.IntentionUpstreamsDestinationName} +} + // ServerIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface -// by sourcing data from a blocking query against the server's state store. +// by sourcing upstreams for the given service, inferred from intentions, from +// the server's state store. func ServerIntentionUpstreams(deps ServerDataSourceDeps) proxycfg.IntentionUpstreams { - return serverIntentionUpstreams{deps} + return serverIntentionUpstreams{deps, structs.IntentionTargetService} +} + +// ServerIntentionUpstreamsDestination satisfies the proxycfg.IntentionUpstreams +// interface by sourcing upstreams for the given destination, inferred from +// intentions, from the server's state store. +func ServerIntentionUpstreamsDestination(deps ServerDataSourceDeps) proxycfg.IntentionUpstreams { + return serverIntentionUpstreams{deps, structs.IntentionTargetDestination} } type serverIntentionUpstreams struct { - deps ServerDataSourceDeps + deps ServerDataSourceDeps + target structs.IntentionTargetType } func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { @@ -32,7 +57,7 @@ func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.Servi } defaultDecision := authz.IntentionDefaultAllow(nil) - index, services, err := store.IntentionTopology(ws, target, false, defaultDecision, structs.IntentionTargetService) + index, services, err := store.IntentionTopology(ws, target, false, defaultDecision, s.target) if err != nil { return 0, nil, err } @@ -51,17 +76,3 @@ func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.Servi dispatchBlockingQueryUpdate[*structs.IndexedServiceList](ch), ) } - -func dispatchBlockingQueryUpdate[ResultType any](ch chan<- proxycfg.UpdateEvent) func(context.Context, string, ResultType, error) { - return func(ctx context.Context, correlationID string, result ResultType, err error) { - event := proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Result: result, - Err: err, - } - select { - case ch <- event: - case <-ctx.Done(): - } - } -} diff --git a/agent/proxycfg-glue/intentions.go b/agent/proxycfg-glue/intentions.go index 57f48bdae..69652d922 100644 --- a/agent/proxycfg-glue/intentions.go +++ b/agent/proxycfg-glue/intentions.go @@ -39,12 +39,8 @@ func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecifi QueryOptions: structs.QueryOptions{Token: req.QueryOptions.Token}, } return c.c.NotifyCallback(ctx, cachetype.IntentionMatchName, query, correlationID, func(ctx context.Context, event cache.UpdateEvent) { - e := proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Err: event.Err, - } - - if e.Err == nil { + var result any + if event.Err == nil { rsp, ok := event.Result.(*structs.IndexedIntentionMatches) if !ok { return @@ -54,11 +50,11 @@ func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecifi if len(rsp.Matches) != 0 { matches = rsp.Matches[0] } - e.Result = matches + result = matches } select { - case ch <- e: + case ch <- newUpdateEvent(correlationID, result, event.Err): case <-ctx.Done(): } }) @@ -110,10 +106,7 @@ func (s *serverIntentions) Notify(ctx context.Context, req *structs.ServiceSpeci sort.Sort(structs.IntentionPrecedenceSorter(intentions)) - return proxycfg.UpdateEvent{ - CorrelationID: correlationID, - Result: intentions, - }, true + return newUpdateEvent(correlationID, intentions, nil), true } for subjectIdx, subject := range subjects { diff --git a/agent/proxycfg-glue/internal_service_dump.go b/agent/proxycfg-glue/internal_service_dump.go new file mode 100644 index 000000000..2d94487f3 --- /dev/null +++ b/agent/proxycfg-glue/internal_service_dump.go @@ -0,0 +1,99 @@ +package proxycfgglue + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-bexpr" + "github.com/hashicorp/go-memdb" + + "github.com/hashicorp/consul/agent/cache" + cachetype "github.com/hashicorp/consul/agent/cache-types" + "github.com/hashicorp/consul/agent/consul/watch" + "github.com/hashicorp/consul/agent/proxycfg" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/agent/structs/aclfilter" +) + +// CacheInternalServiceDump satisfies the proxycfg.InternalServiceDump +// interface by sourcing data from the agent cache. +func CacheInternalServiceDump(c *cache.Cache) proxycfg.InternalServiceDump { + return &cacheInternalServiceDump{c} +} + +// cacheInternalServiceDump wraps the underlying cache-type to return a simpler +// subset of the response (as this is all we use in proxycfg). +type cacheInternalServiceDump struct { + c *cache.Cache +} + +func (c *cacheInternalServiceDump) Notify(ctx context.Context, req *structs.ServiceDumpRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { + dispatch := dispatchCacheUpdate(ch) + + return c.c.NotifyCallback(ctx, cachetype.InternalServiceDumpName, req, correlationID, + func(ctx context.Context, event cache.UpdateEvent) { + if r, _ := event.Result.(*structs.IndexedNodesWithGateways); r != nil { + event.Result = &structs.IndexedCheckServiceNodes{ + Nodes: r.Nodes, + QueryMeta: r.QueryMeta, + } + } + dispatch(ctx, event) + }) +} + +// ServerInternalServiceDump satisfies the proxycfg.InternalServiceDump +// interface by sourcing data from a blocking query against the server's +// state store. +func ServerInternalServiceDump(deps ServerDataSourceDeps, remoteSource proxycfg.InternalServiceDump) proxycfg.InternalServiceDump { + return &serverInternalServiceDump{deps, remoteSource} +} + +type serverInternalServiceDump struct { + deps ServerDataSourceDeps + remoteSource proxycfg.InternalServiceDump +} + +func (s *serverInternalServiceDump) Notify(ctx context.Context, req *structs.ServiceDumpRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { + if req.Datacenter != s.deps.Datacenter { + return s.remoteSource.Notify(ctx, req, correlationID, ch) + } + + filter, err := bexpr.CreateFilter(req.Filter, nil, structs.CheckServiceNodes{}) + if err != nil { + return err + } + + // This is just the small subset of the Internal.ServiceDump RPC handler used + // by proxycfg. + return watch.ServerLocalNotify(ctx, correlationID, s.deps.GetStore, + func(ws memdb.WatchSet, store Store) (uint64, *structs.IndexedCheckServiceNodes, error) { + authz, err := s.deps.ACLResolver.ResolveTokenAndDefaultMeta(req.Token, &req.EnterpriseMeta, nil) + if err != nil { + return 0, nil, err + } + + idx, nodes, err := store.ServiceDump(ws, req.ServiceKind, req.UseServiceKind, &req.EnterpriseMeta, structs.DefaultPeerKeyword) + if err != nil { + return 0, nil, err + } + + raw, err := filter.Execute(nodes) + if err != nil { + return 0, nil, fmt.Errorf("could not filter local service dump: %w", err) + } + nodes = raw.(structs.CheckServiceNodes) + + aclfilter.New(authz, s.deps.Logger).Filter(&nodes) + + return idx, &structs.IndexedCheckServiceNodes{ + Nodes: nodes, + QueryMeta: structs.QueryMeta{ + Index: idx, + Backend: structs.QueryBackendBlocking, + }, + }, nil + }, + dispatchBlockingQueryUpdate[*structs.IndexedCheckServiceNodes](ch), + ) +} diff --git a/agent/proxycfg-glue/internal_service_dump_test.go b/agent/proxycfg-glue/internal_service_dump_test.go new file mode 100644 index 000000000..66245c52a --- /dev/null +++ b/agent/proxycfg-glue/internal_service_dump_test.go @@ -0,0 +1,139 @@ +package proxycfgglue + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/proxycfg" + "github.com/hashicorp/consul/agent/structs" +) + +func TestServerInternalServiceDump(t *testing.T) { + t.Run("remote queries are delegated to the remote source", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + var ( + req = &structs.ServiceDumpRequest{Datacenter: "dc2"} + correlationID = "correlation-id" + ch = make(chan<- proxycfg.UpdateEvent) + result = errors.New("KABOOM") + ) + + remoteSource := newMockInternalServiceDump(t) + remoteSource.On("Notify", ctx, req, correlationID, ch).Return(result) + + dataSource := ServerInternalServiceDump(ServerDataSourceDeps{Datacenter: "dc1"}, remoteSource) + err := dataSource.Notify(ctx, req, correlationID, ch) + require.Equal(t, result, err) + }) + + t.Run("local queries are served from the state store", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + nextIndex := indexGenerator() + + store := state.NewStateStore(nil) + + services := []*structs.NodeService{ + { + Service: "mgw", + Kind: structs.ServiceKindMeshGateway, + }, + { + Service: "web", + Kind: structs.ServiceKindTypical, + }, + { + Service: "db", + Kind: structs.ServiceKindTypical, + }, + } + for idx, service := range services { + require.NoError(t, store.EnsureRegistration(nextIndex(), &structs.RegisterRequest{ + Node: fmt.Sprintf("node-%d", idx), + Service: service, + })) + } + + authz := newStaticResolver( + policyAuthorizer(t, ` + service "mgw" { policy = "read" } + service "web" { policy = "read" } + service "db" { policy = "read" } + node_prefix "node-" { policy = "read" } + `), + ) + + dataSource := ServerInternalServiceDump(ServerDataSourceDeps{ + GetStore: func() Store { return store }, + ACLResolver: authz, + }, nil) + + t.Run("filter by kind", func(t *testing.T) { + eventCh := make(chan proxycfg.UpdateEvent) + require.NoError(t, dataSource.Notify(ctx, &structs.ServiceDumpRequest{ + ServiceKind: structs.ServiceKindMeshGateway, + UseServiceKind: true, + }, "", eventCh)) + + result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) + require.Len(t, result.Nodes, 1) + require.Equal(t, "mgw", result.Nodes[0].Service.Service) + }) + + t.Run("bexpr filtering", func(t *testing.T) { + eventCh := make(chan proxycfg.UpdateEvent) + require.NoError(t, dataSource.Notify(ctx, &structs.ServiceDumpRequest{ + QueryOptions: structs.QueryOptions{Filter: `Service.Service == "web"`}, + }, "", eventCh)) + + result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) + require.Len(t, result.Nodes, 1) + require.Equal(t, "web", result.Nodes[0].Service.Service) + }) + + t.Run("all services", func(t *testing.T) { + eventCh := make(chan proxycfg.UpdateEvent) + require.NoError(t, dataSource.Notify(ctx, &structs.ServiceDumpRequest{}, "", eventCh)) + + result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) + require.Len(t, result.Nodes, 3) + }) + + t.Run("access denied", func(t *testing.T) { + authz.SwapAuthorizer(acl.DenyAll()) + + eventCh := make(chan proxycfg.UpdateEvent) + require.NoError(t, dataSource.Notify(ctx, &structs.ServiceDumpRequest{}, "", eventCh)) + + result := getEventResult[*structs.IndexedCheckServiceNodes](t, eventCh) + require.Empty(t, result.Nodes) + }) + }) +} + +func newMockInternalServiceDump(t *testing.T) *mockInternalServiceDump { + mock := &mockInternalServiceDump{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +type mockInternalServiceDump struct { + mock.Mock +} + +func (m *mockInternalServiceDump) Notify(ctx context.Context, req *structs.ServiceDumpRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { + return m.Called(ctx, req, correlationID, ch).Error(0) +} diff --git a/agent/proxycfg-glue/resolved_service_config.go b/agent/proxycfg-glue/resolved_service_config.go new file mode 100644 index 000000000..3cd952e90 --- /dev/null +++ b/agent/proxycfg-glue/resolved_service_config.go @@ -0,0 +1,70 @@ +package proxycfgglue + +import ( + "context" + "errors" + + "github.com/hashicorp/go-memdb" + + "github.com/hashicorp/consul/agent/cache" + cachetype "github.com/hashicorp/consul/agent/cache-types" + "github.com/hashicorp/consul/agent/configentry" + "github.com/hashicorp/consul/agent/consul/watch" + "github.com/hashicorp/consul/agent/proxycfg" + "github.com/hashicorp/consul/agent/structs" +) + +// CacheResolvedServiceConfig satisfies the proxycfg.ResolvedServiceConfig +// interface by sourcing data from the agent cache. +func CacheResolvedServiceConfig(c *cache.Cache) proxycfg.ResolvedServiceConfig { + return &cacheProxyDataSource[*structs.ServiceConfigRequest]{c, cachetype.ResolvedServiceConfigName} +} + +// ServerResolvedServiceConfig satisfies the proxycfg.ResolvedServiceConfig +// interface by sourcing data from a blocking query against the server's state +// store. +func ServerResolvedServiceConfig(deps ServerDataSourceDeps, remoteSource proxycfg.ResolvedServiceConfig) proxycfg.ResolvedServiceConfig { + return &serverResolvedServiceConfig{deps, remoteSource} +} + +type serverResolvedServiceConfig struct { + deps ServerDataSourceDeps + remoteSource proxycfg.ResolvedServiceConfig +} + +func (s *serverResolvedServiceConfig) Notify(ctx context.Context, req *structs.ServiceConfigRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { + if req.Datacenter != s.deps.Datacenter { + return s.remoteSource.Notify(ctx, req, correlationID, ch) + } + + if len(req.Upstreams) != 0 { + return errors.New("ServerResolvedServiceConfig does not support the legacy Upstreams parameter") + } + + return watch.ServerLocalNotify(ctx, correlationID, s.deps.GetStore, + func(ws memdb.WatchSet, store Store) (uint64, *structs.ServiceConfigResponse, error) { + authz, err := s.deps.ACLResolver.ResolveTokenAndDefaultMeta(req.Token, &req.EnterpriseMeta, nil) + if err != nil { + return 0, nil, err + } + + if err := authz.ToAllowAuthorizer().ServiceReadAllowed(req.Name, nil); err != nil { + return 0, nil, err + } + + idx, entries, err := store.ReadResolvedServiceConfigEntries(ws, req.Name, &req.EnterpriseMeta, req.UpstreamIDs, req.Mode) + if err != nil { + return 0, nil, err + } + + reply, err := configentry.ComputeResolvedServiceConfig(req, req.UpstreamIDs, false, entries, s.deps.Logger) + if err != nil { + return 0, nil, err + } + reply.Index = idx + + return idx, reply, nil + }, + dispatchBlockingQueryUpdate[*structs.ServiceConfigResponse](ch), + ) +} diff --git a/agent/proxycfg-glue/resolved_service_config_test.go b/agent/proxycfg-glue/resolved_service_config_test.go new file mode 100644 index 000000000..3f165aa55 --- /dev/null +++ b/agent/proxycfg-glue/resolved_service_config_test.go @@ -0,0 +1,116 @@ +package proxycfgglue + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/proxycfg" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/sdk/testutil" +) + +func TestServerResolvedServiceConfig(t *testing.T) { + t.Run("remote queries are delegated to the remote source", func(t *testing.T) { + var ( + ctx = context.Background() + req = &structs.ServiceConfigRequest{Datacenter: "dc2"} + correlationID = "correlation-id" + ch = make(chan<- proxycfg.UpdateEvent) + result = errors.New("KABOOM") + ) + + remoteSource := newMockResolvedServiceConfig(t) + remoteSource.On("Notify", ctx, req, correlationID, ch).Return(result) + + dataSource := ServerResolvedServiceConfig(ServerDataSourceDeps{Datacenter: "dc1"}, remoteSource) + err := dataSource.Notify(ctx, req, correlationID, ch) + require.Equal(t, result, err) + }) + + t.Run("local queries are served from the state store", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + const ( + serviceName = "web" + datacenter = "dc1" + ) + + store := state.NewStateStore(nil) + nextIndex := indexGenerator() + + require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ServiceConfigEntry{ + Name: serviceName, + Protocol: "http", + })) + + authz := newStaticResolver( + policyAuthorizer(t, fmt.Sprintf(`service "%s" { policy = "read" }`, serviceName)), + ) + + dataSource := ServerResolvedServiceConfig(ServerDataSourceDeps{ + Datacenter: datacenter, + ACLResolver: authz, + GetStore: func() Store { return store }, + }, nil) + + eventCh := make(chan proxycfg.UpdateEvent) + require.NoError(t, dataSource.Notify(ctx, &structs.ServiceConfigRequest{Datacenter: datacenter, Name: serviceName}, "", eventCh)) + + testutil.RunStep(t, "initial state", func(t *testing.T) { + result := getEventResult[*structs.ServiceConfigResponse](t, eventCh) + require.Equal(t, map[string]any{"protocol": "http"}, result.ProxyConfig) + }) + + testutil.RunStep(t, "write proxy defaults", func(t *testing.T) { + require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ProxyConfigEntry{ + Name: structs.ProxyConfigGlobal, + Mode: structs.ProxyModeDirect, + })) + result := getEventResult[*structs.ServiceConfigResponse](t, eventCh) + require.Equal(t, structs.ProxyModeDirect, result.Mode) + }) + + testutil.RunStep(t, "delete service config", func(t *testing.T) { + require.NoError(t, store.DeleteConfigEntry(nextIndex(), structs.ServiceDefaults, serviceName, nil)) + + result := getEventResult[*structs.ServiceConfigResponse](t, eventCh) + require.Empty(t, result.ProxyConfig) + }) + + testutil.RunStep(t, "revoke access", func(t *testing.T) { + authz.SwapAuthorizer(acl.DenyAll()) + + require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ServiceConfigEntry{ + Name: serviceName, + Protocol: "http", + })) + + expectNoEvent(t, eventCh) + }) + }) +} + +func newMockResolvedServiceConfig(t *testing.T) *mockResolvedServiceConfig { + mock := &mockResolvedServiceConfig{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +type mockResolvedServiceConfig struct { + mock.Mock +} + +func (m *mockResolvedServiceConfig) Notify(ctx context.Context, req *structs.ServiceConfigRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { + return m.Called(ctx, req, correlationID, ch).Error(0) +} diff --git a/agent/proxycfg/connect_proxy.go b/agent/proxycfg/connect_proxy.go index 15e3498f2..2ff1f9ca9 100644 --- a/agent/proxycfg/connect_proxy.go +++ b/agent/proxycfg/connect_proxy.go @@ -280,16 +280,6 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s } snap.Roots = roots - case strings.HasPrefix(u.CorrelationID, peerTrustBundleIDPrefix): - resp, ok := u.Result.(*pbpeering.TrustBundleReadResponse) - if !ok { - return fmt.Errorf("invalid type for response: %T", u.Result) - } - peer := strings.TrimPrefix(u.CorrelationID, peerTrustBundleIDPrefix) - if resp.Bundle != nil { - snap.ConnectProxy.UpstreamPeerTrustBundles.Set(peer, resp.Bundle) - } - case u.CorrelationID == peeringTrustBundlesWatchID: resp, ok := u.Result.(*pbpeering.TrustBundleListByServiceResponse) if !ok { @@ -369,6 +359,17 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s // Clean up data // + peeredChainTargets := make(map[UpstreamID]struct{}) + for _, discoChain := range snap.ConnectProxy.DiscoveryChain { + for _, target := range discoChain.Targets { + if target.Peer == "" { + continue + } + uid := NewUpstreamIDFromTargetID(target.ID) + peeredChainTargets[uid] = struct{}{} + } + } + validPeerNames := make(map[string]struct{}) // Iterate through all known endpoints and remove references to upstream IDs that weren't in the update @@ -383,6 +384,11 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s validPeerNames[uid.Peer] = struct{}{} return true } + // Peered upstream came from a discovery chain target + if _, ok := peeredChainTargets[uid]; ok { + validPeerNames[uid.Peer] = struct{}{} + return true + } snap.ConnectProxy.PeerUpstreamEndpoints.CancelWatch(uid) return true }) @@ -463,8 +469,14 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s continue } if _, ok := seenUpstreams[uid]; !ok { - for _, cancelFn := range targets { + for targetID, cancelFn := range targets { cancelFn() + + targetUID := NewUpstreamIDFromTargetID(targetID) + if targetUID.Peer != "" { + snap.ConnectProxy.PeerUpstreamEndpoints.CancelWatch(targetUID) + snap.ConnectProxy.UpstreamPeerTrustBundles.CancelWatch(targetUID.Peer) + } } delete(snap.ConnectProxy.WatchedUpstreams, uid) } diff --git a/agent/proxycfg/data_sources.go b/agent/proxycfg/data_sources.go index bda0226ff..c01261071 100644 --- a/agent/proxycfg/data_sources.go +++ b/agent/proxycfg/data_sources.go @@ -2,6 +2,7 @@ package proxycfg import ( "context" + "errors" cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/structs" @@ -15,6 +16,28 @@ type UpdateEvent struct { Err error } +// TerminalError wraps the given error to indicate that the data source is in +// an irrecoverably broken state (e.g. because the given ACL token has been +// deleted). +// +// Setting UpdateEvent.Err to a TerminalError causes all watches to be canceled +// which, in turn, terminates the xDS streams. +func TerminalError(err error) error { + return terminalError{err} +} + +// IsTerminalError returns whether the given error indicates that the data +// source is in an irrecoverably broken state so watches should be torn down +// and retried at a higher level. +func IsTerminalError(err error) bool { + return errors.As(err, &terminalError{}) +} + +type terminalError struct{ err error } + +func (e terminalError) Error() string { return e.err.Error() } +func (e terminalError) Unwrap() error { return e.err } + // DataSources contains the dependencies used to consume data used to configure // proxies. type DataSources struct { @@ -66,10 +89,10 @@ type DataSources struct { // IntentionUpstreamsDestination provides intention-inferred upstream updates on a // notification channel. - IntentionUpstreamsDestination IntentionUpstreamsDestination + IntentionUpstreamsDestination IntentionUpstreams - // InternalServiceDump provides updates about a (gateway) service on a - // notification channel. + // InternalServiceDump provides updates about services of a given kind (e.g. + // mesh gateways) on a notification channel. InternalServiceDump InternalServiceDump // LeafCertificate provides updates about the service's leaf certificate on a @@ -174,14 +197,8 @@ type IntentionUpstreams interface { Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- UpdateEvent) error } -// IntentionUpstreamsDestination is the interface used to consume updates about upstreams destination -// inferred from service intentions. -type IntentionUpstreamsDestination interface { - Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- UpdateEvent) error -} - -// InternalServiceDump is the interface used to consume updates about a (gateway) -// service via the internal ServiceDump RPC. +// InternalServiceDump is the interface used to consume updates about services +// of a given kind (e.g. mesh gateways). type InternalServiceDump interface { Notify(ctx context.Context, req *structs.ServiceDumpRequest, correlationID string, ch chan<- UpdateEvent) error } diff --git a/agent/proxycfg/ingress_gateway.go b/agent/proxycfg/ingress_gateway.go index 828229864..81a492836 100644 --- a/agent/proxycfg/ingress_gateway.go +++ b/agent/proxycfg/ingress_gateway.go @@ -5,7 +5,9 @@ import ( "fmt" cachetype "github.com/hashicorp/consul/agent/cache-types" + "github.com/hashicorp/consul/agent/proxycfg/internal/watch" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/proto/pbpeering" ) type handlerIngressGateway struct { @@ -66,6 +68,9 @@ func (s *handlerIngressGateway) initialize(ctx context.Context) (ConfigSnapshot, snap.IngressGateway.WatchedGateways = make(map[UpstreamID]map[string]context.CancelFunc) snap.IngressGateway.WatchedGatewayEndpoints = make(map[UpstreamID]map[string]structs.CheckServiceNodes) snap.IngressGateway.Listeners = make(map[IngressListenerKey]structs.IngressListener) + snap.IngressGateway.UpstreamPeerTrustBundles = watch.NewMap[string, *pbpeering.PeeringTrustBundle]() + snap.IngressGateway.PeerUpstreamEndpoints = watch.NewMap[UpstreamID, structs.CheckServiceNodes]() + snap.IngressGateway.PeerUpstreamEndpointsUseHostnames = make(map[UpstreamID]struct{}) return snap, nil } @@ -152,6 +157,12 @@ func (s *handlerIngressGateway) handleUpdate(ctx context.Context, u UpdateEvent, delete(snap.IngressGateway.WatchedUpstreams[uid], targetID) delete(snap.IngressGateway.WatchedUpstreamEndpoints[uid], targetID) cancelUpstreamFn() + + targetUID := NewUpstreamIDFromTargetID(targetID) + if targetUID.Peer != "" { + snap.IngressGateway.PeerUpstreamEndpoints.CancelWatch(targetUID) + snap.IngressGateway.UpstreamPeerTrustBundles.CancelWatch(targetUID.Peer) + } } cancelFn() diff --git a/agent/proxycfg/manager.go b/agent/proxycfg/manager.go index 3de11b3f8..efdfe4b72 100644 --- a/agent/proxycfg/manager.go +++ b/agent/proxycfg/manager.go @@ -127,7 +127,7 @@ func (m *Manager) Register(id ProxyID, ns *structs.NodeService, source ProxySour } // We are updating the proxy, close its old state - state.Close() + state.Close(false) } // TODO: move to a function that translates ManagerConfig->stateConfig @@ -148,14 +148,13 @@ func (m *Manager) Register(id ProxyID, ns *structs.NodeService, source ProxySour return err } - ch, err := state.Watch() - if err != nil { + if _, err = state.Watch(); err != nil { return err } m.proxies[id] = state // Start a goroutine that will wait for changes and broadcast them to watchers. - go m.notifyBroadcast(ch) + go m.notifyBroadcast(id, state) return nil } @@ -175,8 +174,8 @@ func (m *Manager) Deregister(id ProxyID, source ProxySource) { } // Closing state will let the goroutine we started in Register finish since - // watch chan is closed. - state.Close() + // watch chan is closed + state.Close(false) delete(m.proxies, id) // We intentionally leave potential watchers hanging here - there is no new @@ -186,11 +185,17 @@ func (m *Manager) Deregister(id ProxyID, source ProxySource) { // cleaned up naturally. } -func (m *Manager) notifyBroadcast(ch <-chan ConfigSnapshot) { - // Run until ch is closed - for snap := range ch { +func (m *Manager) notifyBroadcast(proxyID ProxyID, state *state) { + // Run until ch is closed (by a defer in state.run). + for snap := range state.snapCh { m.notify(&snap) } + + // If state.run exited because of an irrecoverable error, close all of the + // watchers so that the consumers reconnect/retry at a higher level. + if state.failed() { + m.closeAllWatchers(proxyID) + } } func (m *Manager) notify(snap *ConfigSnapshot) { @@ -281,6 +286,20 @@ func (m *Manager) Watch(id ProxyID) (<-chan *ConfigSnapshot, CancelFunc) { } } +func (m *Manager) closeAllWatchers(proxyID ProxyID) { + m.mu.Lock() + defer m.mu.Unlock() + + watchers, ok := m.watchers[proxyID] + if !ok { + return + } + + for watchID := range watchers { + m.closeWatchLocked(proxyID, watchID) + } +} + // closeWatchLocked cleans up state related to a single watcher. It assumes the // lock is held. func (m *Manager) closeWatchLocked(proxyID ProxyID, watchID uint64) { @@ -309,7 +328,7 @@ func (m *Manager) Close() error { // Then close all states for proxyID, state := range m.proxies { - state.Close() + state.Close(false) delete(m.proxies, proxyID) } return nil diff --git a/agent/proxycfg/mesh_gateway.go b/agent/proxycfg/mesh_gateway.go index f80ee537f..93fffdc31 100644 --- a/agent/proxycfg/mesh_gateway.go +++ b/agent/proxycfg/mesh_gateway.go @@ -491,7 +491,7 @@ func (s *handlerMeshGateway) handleUpdate(ctx context.Context, u UpdateEvent, sn } case strings.HasPrefix(u.CorrelationID, "mesh-gateway:"): - resp, ok := u.Result.(*structs.IndexedNodesWithGateways) + resp, ok := u.Result.(*structs.IndexedCheckServiceNodes) if !ok { return fmt.Errorf("invalid type for response: %T", u.Result) } diff --git a/agent/proxycfg/naming.go b/agent/proxycfg/naming.go index 3bb0854b0..08ff216ed 100644 --- a/agent/proxycfg/naming.go +++ b/agent/proxycfg/naming.go @@ -63,22 +63,29 @@ func NewUpstreamIDFromServiceID(sid structs.ServiceID) UpstreamID { return id } -// TODO(peering): confirm we don't need peername here func NewUpstreamIDFromTargetID(tid string) UpstreamID { - // Drop the leading subset if one is present in the target ID. - separators := strings.Count(tid, ".") - if separators > 3 { - prefix := tid[:strings.Index(tid, ".")+1] - tid = strings.TrimPrefix(tid, prefix) + var id UpstreamID + split := strings.Split(tid, ".") + + switch { + case split[len(split)-2] == "external": + id = UpstreamID{ + Name: split[0], + EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), + Peer: split[4], + } + case len(split) == 5: + // Drop the leading subset if one is present in the target ID. + split = split[1:] + fallthrough + default: + id = UpstreamID{ + Name: split[0], + EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), + Datacenter: split[3], + } } - split := strings.SplitN(tid, ".", 4) - - id := UpstreamID{ - Name: split[0], - EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(split[2], split[1]), - Datacenter: split[3], - } id.normalize() return id } diff --git a/agent/proxycfg/naming_test.go b/agent/proxycfg/naming_test.go index 23ff24165..2c4f5173a 100644 --- a/agent/proxycfg/naming_test.go +++ b/agent/proxycfg/naming_test.go @@ -35,6 +35,13 @@ func TestUpstreamIDFromTargetID(t *testing.T) { Datacenter: "dc2", }, }, + "peered": { + tid: "foo.default.default.external.cluster-01", + expect: UpstreamID{ + Name: "foo", + Peer: "cluster-01", + }, + }, } for name, tc := range cases { diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index 8d2d81bed..23cb8a955 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -814,6 +814,18 @@ func (s *ConfigSnapshot) MeshConfigTLSOutgoing() *structs.MeshDirectionalTLSConf return mesh.TLS.Outgoing } +func (s *ConfigSnapshot) ToConfigSnapshotUpstreams() (*ConfigSnapshotUpstreams, error) { + switch s.Kind { + case structs.ServiceKindConnectProxy: + return &s.ConnectProxy.ConfigSnapshotUpstreams, nil + case structs.ServiceKindIngressGateway: + return &s.IngressGateway.ConfigSnapshotUpstreams, nil + default: + // This is a coherence check and should never fail + return nil, fmt.Errorf("No upstream snapshot for gateway mode %q", s.Kind) + } +} + func (u *ConfigSnapshotUpstreams) UpstreamPeerMeta(uid UpstreamID) structs.PeeringServiceMeta { nodes, _ := u.PeerUpstreamEndpoints.Get(uid) if len(nodes) == 0 { diff --git a/agent/proxycfg/state.go b/agent/proxycfg/state.go index 13b22c4fd..34d336435 100644 --- a/agent/proxycfg/state.go +++ b/agent/proxycfg/state.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "reflect" + "sync/atomic" "time" "github.com/hashicorp/go-hclog" @@ -70,11 +71,21 @@ type state struct { // in Watch. cancel func() + // failedFlag is (atomically) set to 1 (by Close) when run exits because a data + // source is in an irrecoverable state. It can be read with failed. + failedFlag int32 + ch chan UpdateEvent snapCh chan ConfigSnapshot reqCh chan chan *ConfigSnapshot } +// failed returns whether run exited because a data source is in an +// irrecoverable state. +func (s *state) failed() bool { + return atomic.LoadInt32(&s.failedFlag) == 1 +} + type DNSConfig struct { Domain string AltDomain string @@ -250,10 +261,13 @@ func (s *state) Watch() (<-chan ConfigSnapshot, error) { } // Close discards the state and stops any long-running watches. -func (s *state) Close() error { +func (s *state) Close(failed bool) error { if s.cancel != nil { s.cancel() } + if failed { + atomic.StoreInt32(&s.failedFlag, 1) + } return nil } @@ -300,7 +314,13 @@ func (s *state) run(ctx context.Context, snap *ConfigSnapshot) { case <-ctx.Done(): return case u := <-s.ch: - s.logger.Trace("A blocking query returned; handling snapshot update", "correlationID", u.CorrelationID) + s.logger.Trace("Data source returned; handling snapshot update", "correlationID", u.CorrelationID) + + if IsTerminalError(u.Err) { + s.logger.Error("Data source in an irrecoverable state; exiting", "error", u.Err, "correlationID", u.CorrelationID) + s.Close(true) + return + } if err := s.handler.handleUpdate(ctx, u, snap); err != nil { s.logger.Error("Failed to handle update from watch", diff --git a/agent/proxycfg/state_test.go b/agent/proxycfg/state_test.go index 855ded03d..f8cf0834c 100644 --- a/agent/proxycfg/state_test.go +++ b/agent/proxycfg/state_test.go @@ -493,6 +493,11 @@ func TestState_WatchesAndUpdates(t *testing.T) { Mode: structs.MeshGatewayModeNone, }, }, + structs.Upstream{ + DestinationType: structs.UpstreamDestTypeService, + DestinationName: "api-failover-to-peer", + LocalBindPort: 10007, + }, structs.Upstream{ DestinationType: structs.UpstreamDestTypeService, DestinationName: "api-dc2", @@ -552,6 +557,16 @@ func TestState_WatchesAndUpdates(t *testing.T) { Mode: structs.MeshGatewayModeNone, }, }), + fmt.Sprintf("discovery-chain:%s-failover-to-peer", apiUID.String()): genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ + Name: "api-failover-to-peer", + EvaluateInDatacenter: "dc1", + EvaluateInNamespace: "default", + EvaluateInPartition: "default", + Datacenter: "dc1", + OverrideMeshGateway: structs.MeshGatewayConfig{ + Mode: meshGatewayProxyConfigValue, + }, + }), fmt.Sprintf("discovery-chain:%s-dc2", apiUID.String()): genVerifyDiscoveryChainWatch(&structs.DiscoveryChainRequest{ Name: "api-dc2", EvaluateInDatacenter: "dc1", @@ -639,6 +654,26 @@ func TestState_WatchesAndUpdates(t *testing.T) { }, Err: nil, }, + { + CorrelationID: fmt.Sprintf("discovery-chain:%s-failover-to-peer", apiUID.String()), + Result: &structs.DiscoveryChainResponse{ + Chain: discoverychain.TestCompileConfigEntries(t, "api-failover-to-peer", "default", "default", "dc1", "trustdomain.consul", + func(req *discoverychain.CompileRequest) { + req.OverrideMeshGateway.Mode = meshGatewayProxyConfigValue + }, &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "api-failover-to-peer", + Failover: map[string]structs.ServiceResolverFailover{ + "*": { + Targets: []structs.ServiceResolverFailoverTarget{ + {Peer: "cluster-01"}, + }, + }, + }, + }), + }, + Err: nil, + }, }, verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) { require.True(t, snap.Valid()) @@ -646,15 +681,18 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Equal(t, indexedRoots, snap.Roots) require.Equal(t, issuedCert, snap.ConnectProxy.Leaf) - require.Len(t, snap.ConnectProxy.DiscoveryChain, 5, "%+v", snap.ConnectProxy.DiscoveryChain) - require.Len(t, snap.ConnectProxy.WatchedUpstreams, 5, "%+v", snap.ConnectProxy.WatchedUpstreams) - require.Len(t, snap.ConnectProxy.WatchedUpstreamEndpoints, 5, "%+v", snap.ConnectProxy.WatchedUpstreamEndpoints) - require.Len(t, snap.ConnectProxy.WatchedGateways, 5, "%+v", snap.ConnectProxy.WatchedGateways) - require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 5, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) + require.Len(t, snap.ConnectProxy.DiscoveryChain, 6, "%+v", snap.ConnectProxy.DiscoveryChain) + require.Len(t, snap.ConnectProxy.WatchedUpstreams, 6, "%+v", snap.ConnectProxy.WatchedUpstreams) + require.Len(t, snap.ConnectProxy.WatchedUpstreamEndpoints, 6, "%+v", snap.ConnectProxy.WatchedUpstreamEndpoints) + require.Len(t, snap.ConnectProxy.WatchedGateways, 6, "%+v", snap.ConnectProxy.WatchedGateways) + require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 6, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) require.Len(t, snap.ConnectProxy.WatchedServiceChecks, 0, "%+v", snap.ConnectProxy.WatchedServiceChecks) require.Len(t, snap.ConnectProxy.PreparedQueryEndpoints, 0, "%+v", snap.ConnectProxy.PreparedQueryEndpoints) + require.Equal(t, 1, snap.ConnectProxy.ConfigSnapshotUpstreams.PeerUpstreamEndpoints.Len()) + require.Equal(t, 1, snap.ConnectProxy.ConfigSnapshotUpstreams.UpstreamPeerTrustBundles.Len()) + require.True(t, snap.ConnectProxy.IntentionsSet) require.Equal(t, ixnMatch, snap.ConnectProxy.Intentions) require.True(t, snap.ConnectProxy.MeshConfigSet) @@ -667,6 +705,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { fmt.Sprintf("upstream-target:api-failover-remote.default.default.dc2:%s-failover-remote?dc=dc2", apiUID.String()): genVerifyServiceSpecificRequest("api-failover-remote", "", "dc2", true), fmt.Sprintf("upstream-target:api-failover-local.default.default.dc2:%s-failover-local?dc=dc2", apiUID.String()): genVerifyServiceSpecificRequest("api-failover-local", "", "dc2", true), fmt.Sprintf("upstream-target:api-failover-direct.default.default.dc2:%s-failover-direct?dc=dc2", apiUID.String()): genVerifyServiceSpecificRequest("api-failover-direct", "", "dc2", true), + upstreamPeerWatchIDPrefix + fmt.Sprintf("%s-failover-to-peer?peer=cluster-01", apiUID.String()): genVerifyServiceSpecificPeeredRequest("api-failover-to-peer", "", "", "cluster-01", true), fmt.Sprintf("mesh-gateway:dc2:%s-failover-remote?dc=dc2", apiUID.String()): genVerifyGatewayWatch("dc2"), fmt.Sprintf("mesh-gateway:dc1:%s-failover-local?dc=dc2", apiUID.String()): genVerifyGatewayWatch("dc1"), }, @@ -676,15 +715,18 @@ func TestState_WatchesAndUpdates(t *testing.T) { require.Equal(t, indexedRoots, snap.Roots) require.Equal(t, issuedCert, snap.ConnectProxy.Leaf) - require.Len(t, snap.ConnectProxy.DiscoveryChain, 5, "%+v", snap.ConnectProxy.DiscoveryChain) - require.Len(t, snap.ConnectProxy.WatchedUpstreams, 5, "%+v", snap.ConnectProxy.WatchedUpstreams) - require.Len(t, snap.ConnectProxy.WatchedUpstreamEndpoints, 5, "%+v", snap.ConnectProxy.WatchedUpstreamEndpoints) - require.Len(t, snap.ConnectProxy.WatchedGateways, 5, "%+v", snap.ConnectProxy.WatchedGateways) - require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 5, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) + require.Len(t, snap.ConnectProxy.DiscoveryChain, 6, "%+v", snap.ConnectProxy.DiscoveryChain) + require.Len(t, snap.ConnectProxy.WatchedUpstreams, 6, "%+v", snap.ConnectProxy.WatchedUpstreams) + require.Len(t, snap.ConnectProxy.WatchedUpstreamEndpoints, 6, "%+v", snap.ConnectProxy.WatchedUpstreamEndpoints) + require.Len(t, snap.ConnectProxy.WatchedGateways, 6, "%+v", snap.ConnectProxy.WatchedGateways) + require.Len(t, snap.ConnectProxy.WatchedGatewayEndpoints, 6, "%+v", snap.ConnectProxy.WatchedGatewayEndpoints) require.Len(t, snap.ConnectProxy.WatchedServiceChecks, 0, "%+v", snap.ConnectProxy.WatchedServiceChecks) require.Len(t, snap.ConnectProxy.PreparedQueryEndpoints, 0, "%+v", snap.ConnectProxy.PreparedQueryEndpoints) + require.Equal(t, 1, snap.ConnectProxy.ConfigSnapshotUpstreams.PeerUpstreamEndpoints.Len()) + require.Equal(t, 1, snap.ConnectProxy.ConfigSnapshotUpstreams.UpstreamPeerTrustBundles.Len()) + require.True(t, snap.ConnectProxy.IntentionsSet) require.Equal(t, ixnMatch, snap.ConnectProxy.Intentions) }, @@ -885,7 +927,7 @@ func TestState_WatchesAndUpdates(t *testing.T) { events: []UpdateEvent{ { CorrelationID: "mesh-gateway:dc4", - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC4Hostname(t), }, Err: nil, diff --git a/agent/proxycfg/testing.go b/agent/proxycfg/testing.go index 0493e30da..d5a3d8224 100644 --- a/agent/proxycfg/testing.go +++ b/agent/proxycfg/testing.go @@ -280,6 +280,31 @@ func TestUpstreamNodesDC2(t testing.T) structs.CheckServiceNodes { } } +func TestUpstreamNodesPeerCluster01(t testing.T) structs.CheckServiceNodes { + peer := "cluster-01" + service := structs.TestNodeServiceWithNameInPeer(t, "web", peer) + return structs.CheckServiceNodes{ + structs.CheckServiceNode{ + Node: &structs.Node{ + ID: "test1", + Node: "test1", + Address: "10.40.1.1", + PeerName: peer, + }, + Service: service, + }, + structs.CheckServiceNode{ + Node: &structs.Node{ + ID: "test2", + Node: "test2", + Address: "10.40.1.2", + PeerName: peer, + }, + Service: service, + }, + } +} + func TestUpstreamNodesInStatusDC2(t testing.T, status string) structs.CheckServiceNodes { return structs.CheckServiceNodes{ structs.CheckServiceNode{ @@ -949,7 +974,7 @@ func NewTestDataSources() *TestDataSources { Intentions: NewTestDataSource[*structs.ServiceSpecificRequest, structs.Intentions](), IntentionUpstreams: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](), IntentionUpstreamsDestination: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](), - InternalServiceDump: NewTestDataSource[*structs.ServiceDumpRequest, *structs.IndexedNodesWithGateways](), + InternalServiceDump: NewTestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes](), LeafCertificate: NewTestDataSource[*cachetype.ConnectCALeafRequest, *structs.IssuedCert](), PreparedQuery: NewTestDataSource[*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryExecuteResponse](), ResolvedServiceConfig: NewTestDataSource[*structs.ServiceConfigRequest, *structs.ServiceConfigResponse](), @@ -975,7 +1000,7 @@ type TestDataSources struct { Intentions *TestDataSource[*structs.ServiceSpecificRequest, structs.Intentions] IntentionUpstreams *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList] IntentionUpstreamsDestination *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList] - InternalServiceDump *TestDataSource[*structs.ServiceDumpRequest, *structs.IndexedNodesWithGateways] + InternalServiceDump *TestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes] LeafCertificate *TestDataSource[*cachetype.ConnectCALeafRequest, *structs.IssuedCert] PeeredUpstreams *TestDataSource[*structs.PartitionSpecificRequest, *structs.IndexedPeeredServiceList] PreparedQuery *TestDataSource[*structs.PreparedQueryExecuteRequest, *structs.PreparedQueryExecuteResponse] diff --git a/agent/proxycfg/testing_mesh_gateway.go b/agent/proxycfg/testing_mesh_gateway.go index 388ac12b8..f8b6116a2 100644 --- a/agent/proxycfg/testing_mesh_gateway.go +++ b/agent/proxycfg/testing_mesh_gateway.go @@ -316,19 +316,19 @@ func TestConfigSnapshotMeshGateway(t testing.T, variant string, nsFn func(ns *st baseEvents = testSpliceEvents(baseEvents, []UpdateEvent{ { CorrelationID: "mesh-gateway:dc2", - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC2(t), }, }, { CorrelationID: "mesh-gateway:dc4", - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC4Hostname(t), }, }, { CorrelationID: "mesh-gateway:dc6", - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC6Hostname(t), }, }, @@ -376,7 +376,7 @@ func TestConfigSnapshotMeshGateway(t testing.T, variant string, nsFn func(ns *st // Have the cross-dc query mechanism not work for dc2 so // fedstates will infill. CorrelationID: "mesh-gateway:dc2", - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: nil, }, }, diff --git a/agent/proxycfg/testing_upstreams.go b/agent/proxycfg/testing_upstreams.go index 2d80c0968..afb310c75 100644 --- a/agent/proxycfg/testing_upstreams.go +++ b/agent/proxycfg/testing_upstreams.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/consul/discoverychain" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/proto/pbpeering" ) func setupTestVariationConfigEntriesAndSnapshot( @@ -68,10 +69,46 @@ func setupTestVariationConfigEntriesAndSnapshot( }) events = append(events, UpdateEvent{ CorrelationID: "mesh-gateway:dc2:" + dbUID.String(), - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC2(t), }, }) + case "failover-to-cluster-peer": + events = append(events, UpdateEvent{ + CorrelationID: "peer-trust-bundle:cluster-01", + Result: &pbpeering.TrustBundleReadResponse{ + Bundle: &pbpeering.PeeringTrustBundle{ + PeerName: "peer1", + TrustDomain: "peer1.domain", + ExportedPartition: "peer1ap", + RootPEMs: []string{"peer1-root-1"}, + }, + }, + }) + events = append(events, UpdateEvent{ + CorrelationID: "upstream-peer:db?peer=cluster-01", + Result: &structs.IndexedCheckServiceNodes{ + Nodes: TestUpstreamNodesPeerCluster01(t), + }, + }) + case "redirect-to-cluster-peer": + events = append(events, UpdateEvent{ + CorrelationID: "peer-trust-bundle:cluster-01", + Result: &pbpeering.TrustBundleReadResponse{ + Bundle: &pbpeering.PeeringTrustBundle{ + PeerName: "peer1", + TrustDomain: "peer1.domain", + ExportedPartition: "peer1ap", + RootPEMs: []string{"peer1-root-1"}, + }, + }, + }) + events = append(events, UpdateEvent{ + CorrelationID: "upstream-peer:db?peer=cluster-01", + Result: &structs.IndexedCheckServiceNodes{ + Nodes: TestUpstreamNodesPeerCluster01(t), + }, + }) case "failover-through-double-remote-gateway-triggered": events = append(events, UpdateEvent{ CorrelationID: "upstream-target:db.default.default.dc1:" + dbUID.String(), @@ -95,13 +132,13 @@ func setupTestVariationConfigEntriesAndSnapshot( }) events = append(events, UpdateEvent{ CorrelationID: "mesh-gateway:dc2:" + dbUID.String(), - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC2(t), }, }) events = append(events, UpdateEvent{ CorrelationID: "mesh-gateway:dc3:" + dbUID.String(), - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC3(t), }, }) @@ -122,7 +159,7 @@ func setupTestVariationConfigEntriesAndSnapshot( }) events = append(events, UpdateEvent{ CorrelationID: "mesh-gateway:dc1:" + dbUID.String(), - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC1(t), }, }) @@ -149,7 +186,7 @@ func setupTestVariationConfigEntriesAndSnapshot( }) events = append(events, UpdateEvent{ CorrelationID: "mesh-gateway:dc1:" + dbUID.String(), - Result: &structs.IndexedNodesWithGateways{ + Result: &structs.IndexedCheckServiceNodes{ Nodes: TestGatewayNodesDC1(t), }, }) @@ -255,6 +292,32 @@ func setupTestVariationDiscoveryChain( }, }, ) + case "failover-to-cluster-peer": + entries = append(entries, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "db", + ConnectTimeout: 33 * time.Second, + Failover: map[string]structs.ServiceResolverFailover{ + "*": { + Targets: []structs.ServiceResolverFailoverTarget{ + {Peer: "cluster-01"}, + }, + }, + }, + }, + ) + case "redirect-to-cluster-peer": + entries = append(entries, + &structs.ServiceResolverConfigEntry{ + Kind: structs.ServiceResolver, + Name: "db", + ConnectTimeout: 33 * time.Second, + Redirect: &structs.ServiceResolverRedirect{ + Peer: "cluster-01", + }, + }, + ) case "failover-through-double-remote-gateway-triggered": fallthrough case "failover-through-double-remote-gateway": diff --git a/agent/proxycfg/upstreams.go b/agent/proxycfg/upstreams.go index 600a89e09..94c872e39 100644 --- a/agent/proxycfg/upstreams.go +++ b/agent/proxycfg/upstreams.go @@ -9,7 +9,9 @@ import ( "github.com/mitchellh/mapstructure" "github.com/hashicorp/consul/acl" + cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/proto/pbpeering" ) type handlerUpstreams struct { @@ -21,9 +23,10 @@ func (s *handlerUpstreams) handleUpdateUpstreams(ctx context.Context, u UpdateEv return fmt.Errorf("error filling agent cache: %v", u.Err) } - upstreamsSnapshot := &snap.ConnectProxy.ConfigSnapshotUpstreams - if snap.Kind == structs.ServiceKindIngressGateway { - upstreamsSnapshot = &snap.IngressGateway.ConfigSnapshotUpstreams + upstreamsSnapshot, err := snap.ToConfigSnapshotUpstreams() + + if err != nil { + return err } switch { @@ -98,19 +101,16 @@ func (s *handlerUpstreams) handleUpdateUpstreams(ctx context.Context, u UpdateEv uid := UpstreamIDFromString(uidString) - filteredNodes := hostnameEndpoints( - s.logger, - GatewayKey{ /*empty so it never matches*/ }, - resp.Nodes, - ) - if len(filteredNodes) > 0 { - if set := upstreamsSnapshot.PeerUpstreamEndpoints.Set(uid, filteredNodes); set { - upstreamsSnapshot.PeerUpstreamEndpointsUseHostnames[uid] = struct{}{} - } - } else { - if set := upstreamsSnapshot.PeerUpstreamEndpoints.Set(uid, resp.Nodes); set { - delete(upstreamsSnapshot.PeerUpstreamEndpointsUseHostnames, uid) - } + s.setPeerEndpoints(upstreamsSnapshot, uid, resp.Nodes) + + case strings.HasPrefix(u.CorrelationID, peerTrustBundleIDPrefix): + resp, ok := u.Result.(*pbpeering.TrustBundleReadResponse) + if !ok { + return fmt.Errorf("invalid type for response: %T", u.Result) + } + peer := strings.TrimPrefix(u.CorrelationID, peerTrustBundleIDPrefix) + if resp.Bundle != nil { + upstreamsSnapshot.UpstreamPeerTrustBundles.Set(peer, resp.Bundle) } case strings.HasPrefix(u.CorrelationID, "upstream-target:"): @@ -186,7 +186,7 @@ func (s *handlerUpstreams) handleUpdateUpstreams(ctx context.Context, u UpdateEv } case strings.HasPrefix(u.CorrelationID, "mesh-gateway:"): - resp, ok := u.Result.(*structs.IndexedNodesWithGateways) + resp, ok := u.Result.(*structs.IndexedCheckServiceNodes) if !ok { return fmt.Errorf("invalid type for response: %T", u.Result) } @@ -216,6 +216,23 @@ func removeColonPrefix(s string) (string, string, bool) { return s[0:idx], s[idx+1:], true } +func (s *handlerUpstreams) setPeerEndpoints(upstreamsSnapshot *ConfigSnapshotUpstreams, uid UpstreamID, nodes structs.CheckServiceNodes) { + filteredNodes := hostnameEndpoints( + s.logger, + GatewayKey{ /*empty so it never matches*/ }, + nodes, + ) + if len(filteredNodes) > 0 { + if set := upstreamsSnapshot.PeerUpstreamEndpoints.Set(uid, filteredNodes); set { + upstreamsSnapshot.PeerUpstreamEndpointsUseHostnames[uid] = struct{}{} + } + } else { + if set := upstreamsSnapshot.PeerUpstreamEndpoints.Set(uid, nodes); set { + delete(upstreamsSnapshot.PeerUpstreamEndpointsUseHostnames, uid) + } + } +} + func (s *handlerUpstreams) resetWatchesFromChain( ctx context.Context, uid UpstreamID, @@ -255,6 +272,12 @@ func (s *handlerUpstreams) resetWatchesFromChain( delete(snap.WatchedUpstreams[uid], targetID) delete(snap.WatchedUpstreamEndpoints[uid], targetID) cancelFn() + + targetUID := NewUpstreamIDFromTargetID(targetID) + if targetUID.Peer != "" { + snap.PeerUpstreamEndpoints.CancelWatch(targetUID) + snap.UpstreamPeerTrustBundles.CancelWatch(targetUID.Peer) + } } var ( @@ -274,6 +297,7 @@ func (s *handlerUpstreams) resetWatchesFromChain( service: target.Service, filter: target.Subset.Filter, datacenter: target.Datacenter, + peer: target.Peer, entMeta: target.GetEnterpriseMetadata(), } err := s.watchUpstreamTarget(ctx, snap, opts) @@ -384,6 +408,7 @@ type targetWatchOpts struct { service string filter string datacenter string + peer string entMeta *acl.EnterpriseMeta } @@ -397,11 +422,17 @@ func (s *handlerUpstreams) watchUpstreamTarget(ctx context.Context, snap *Config var finalMeta acl.EnterpriseMeta finalMeta.Merge(opts.entMeta) - correlationID := "upstream-target:" + opts.chainID + ":" + opts.upstreamID.String() + uid := opts.upstreamID + correlationID := "upstream-target:" + opts.chainID + ":" + uid.String() + + if opts.peer != "" { + uid = NewUpstreamIDFromTargetID(opts.chainID) + correlationID = upstreamPeerWatchIDPrefix + uid.String() + } ctx, cancel := context.WithCancel(ctx) err := s.dataSources.Health.Notify(ctx, &structs.ServiceSpecificRequest{ - PeerName: opts.upstreamID.Peer, + PeerName: opts.peer, Datacenter: opts.datacenter, QueryOptions: structs.QueryOptions{ Token: s.token, @@ -422,6 +453,31 @@ func (s *handlerUpstreams) watchUpstreamTarget(ctx context.Context, snap *Config } snap.WatchedUpstreams[opts.upstreamID][opts.chainID] = cancel + if uid.Peer == "" { + return nil + } + + if ok := snap.PeerUpstreamEndpoints.IsWatched(uid); !ok { + snap.PeerUpstreamEndpoints.InitWatch(uid, cancel) + } + + // Check whether a watch for this peer exists to avoid duplicates. + if ok := snap.UpstreamPeerTrustBundles.IsWatched(uid.Peer); !ok { + peerCtx, cancel := context.WithCancel(ctx) + if err := s.dataSources.TrustBundle.Notify(peerCtx, &cachetype.TrustBundleReadRequest{ + Request: &pbpeering.TrustBundleReadRequest{ + Name: uid.Peer, + Partition: uid.PartitionOrDefault(), + }, + QueryOptions: structs.QueryOptions{Token: s.token}, + }, peerTrustBundleIDPrefix+uid.Peer, s.ch); err != nil { + cancel() + return fmt.Errorf("error while watching trust bundle for peer %q: %w", uid.Peer, err) + } + + snap.UpstreamPeerTrustBundles.InitWatch(uid.Peer, cancel) + } + return nil } diff --git a/agent/rpc/middleware/interceptors.go b/agent/rpc/middleware/interceptors.go index 049283ac2..6abcf0a44 100644 --- a/agent/rpc/middleware/interceptors.go +++ b/agent/rpc/middleware/interceptors.go @@ -49,7 +49,8 @@ func NewRequestRecorder(logger hclog.Logger, isLeader func() bool, localDC strin } func (r *RequestRecorder) Record(requestName string, rpcType string, start time.Time, request interface{}, respErrored bool) { - elapsed := time.Since(start).Milliseconds() + elapsed := time.Since(start).Microseconds() + elapsedMs := float32(elapsed) / 1000 reqType := requestType(request) isLeader := r.getServerLeadership() @@ -64,7 +65,7 @@ func (r *RequestRecorder) Record(requestName string, rpcType string, start time. labels = r.addOptionalLabels(request, labels) // math.MaxInt64 < math.MaxFloat32 is true so we should be good! - r.RecorderFunc(metricRPCRequest, float32(elapsed), labels) + r.RecorderFunc(metricRPCRequest, elapsedMs, labels) labelsArr := flattenLabels(labels) r.Logger.Trace(requestLogName, labelsArr...) diff --git a/agent/rpc/peering/service.go b/agent/rpc/peering/service.go index ed9cd9e4f..17b862ffa 100644 --- a/agent/rpc/peering/service.go +++ b/agent/rpc/peering/service.go @@ -8,7 +8,6 @@ import ( "time" "github.com/armon/go-metrics" - "github.com/hashicorp/consul/proto/pbpeerstream" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-multierror" @@ -27,6 +26,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/pbpeering" + "github.com/hashicorp/consul/proto/pbpeerstream" ) var ( @@ -194,8 +194,6 @@ func (s *Server) GenerateToken( return nil, fmt.Errorf("meta tags failed validation: %w", err) } - defer metrics.MeasureSince([]string{"peering", "generate_token"}, time.Now()) - resp := &pbpeering.GenerateTokenResponse{} handled, err := s.ForwardRPC(&writeRequest, func(conn *grpc.ClientConn) error { ctx := external.ForwardMetadataContext(ctx) @@ -207,6 +205,8 @@ func (s *Server) GenerateToken( return resp, err } + defer metrics.MeasureSince([]string{"peering", "generate_token"}, time.Now()) + var authzCtx acl.AuthorizerContext entMeta := structs.DefaultEnterpriseMetaInPartition(req.Partition) authz, err := s.Backend.ResolveTokenAndDefaultMeta(external.TokenFromContext(ctx), entMeta, &authzCtx) @@ -374,11 +374,12 @@ func (s *Server) Establish( return nil, err } - if err := s.validatePeeringInPartition(tok.PeerID, entMeta.PartitionOrEmpty()); err != nil { + if err := s.validatePeeringLocality(tok, entMeta.PartitionOrEmpty()); err != nil { return nil, err } var id string + serverAddrs := tok.ServerAddresses if existing == nil { id, err = lib.GenerateUUID(s.Backend.CheckPeeringUUID) if err != nil { @@ -386,6 +387,11 @@ func (s *Server) Establish( } } else { id = existing.ID + // If there is a connected stream, assume that the existing ServerAddresses + // are up to date and do not try to overwrite them with the token's addresses. + if status, ok := s.Tracker.StreamStatus(id); ok && status.Connected { + serverAddrs = existing.PeerServerAddresses + } } // validate that this peer name is not being used as an acceptor already @@ -397,7 +403,7 @@ func (s *Server) Establish( ID: id, Name: req.PeerName, PeerCAPems: tok.CA, - PeerServerAddresses: tok.ServerAddresses, + PeerServerAddresses: serverAddrs, PeerServerName: tok.ServerName, PeerID: tok.PeerID, Meta: req.Meta, @@ -418,9 +424,9 @@ func (s *Server) Establish( } var exchangeResp *pbpeerstream.ExchangeSecretResponse - // Loop through the token's addresses once, attempting to fetch the long-lived stream secret. + // Loop through the known server addresses once, attempting to fetch the long-lived stream secret. var dialErrors error - for _, addr := range peering.PeerServerAddresses { + for _, addr := range serverAddrs { exchangeResp, err = exchangeSecret(ctx, addr, tlsOption, &exchangeReq) if err != nil { dialErrors = multierror.Append(dialErrors, fmt.Errorf("failed to exchange peering secret with %q: %w", addr, err)) @@ -457,15 +463,23 @@ func (s *Server) Establish( return resp, nil } -// validatePeeringInPartition makes sure that we don't create a peering in the same partition. We validate by looking at -// the remotePeerID from the PeeringToken and looking up for a peering in the partition. If there is one and the -// request partition is the same, then we are attempting to peer within the partition, which we shouldn't. -func (s *Server) validatePeeringInPartition(remotePeerID, partition string) error { - _, peering, err := s.Backend.Store().PeeringReadByID(nil, remotePeerID) +// validatePeeringLocality makes sure that we don't create a peering in the cluster/partition it was generated. +// We validate by looking at the remote PeerID from the PeeringToken and looking up that peering in the partition. +// If there is one and the request partition is the same, then we are attempting to peer within the partition, which we shouldn't. +// We also perform a check to verify if the ServerName of the PeeringToken overlaps with our own, we do not process it +// unless we've been able to find the peering in the store, i.e. this peering is between two local partitions. +func (s *Server) validatePeeringLocality(token *structs.PeeringToken, partition string) error { + _, peering, err := s.Backend.Store().PeeringReadByID(nil, token.PeerID) if err != nil { return fmt.Errorf("cannot read peering by ID: %w", err) } + // If the token has the same server name as this cluster, but we can't find the peering + // in our store, it indicates a naming conflict. + if s.Backend.GetServerName() == token.ServerName && peering == nil { + return fmt.Errorf("conflict - peering token's server name matches the current cluster's server name, %q, but there is no record in the database", s.Backend.GetServerName()) + } + if peering != nil && acl.EqualPartitions(peering.GetPartition(), partition) { return fmt.Errorf("cannot create a peering within the same partition (ENT) or cluster (OSS)") } @@ -720,11 +734,12 @@ func (s *Server) PeeringDelete(ctx context.Context, req *pbpeering.PeeringDelete return nil, err } - if existing == nil || !existing.IsActive() { + if existing == nil || existing.State == pbpeering.PeeringState_DELETING { // Return early when the Peering doesn't exist or is already marked for deletion. // We don't return nil because the pb will fail to marshal. return &pbpeering.PeeringDeleteResponse{}, nil } + // We are using a write request due to needing to perform a deferred deletion. // The peering gets marked for deletion by setting the DeletedAt field, // and a leader routine will handle deleting the peering. diff --git a/agent/rpc/peering/service_test.go b/agent/rpc/peering/service_test.go index 54770d6a6..5472d081b 100644 --- a/agent/rpc/peering/service_test.go +++ b/agent/rpc/peering/service_test.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/stream" external "github.com/hashicorp/consul/agent/grpc-external" + "github.com/hashicorp/consul/agent/grpc-external/limiter" grpc "github.com/hashicorp/consul/agent/grpc-internal" "github.com/hashicorp/consul/agent/grpc-internal/resolver" "github.com/hashicorp/consul/agent/pool" @@ -344,8 +345,8 @@ func TestPeeringService_Establish_Validation(t *testing.T) { } } -// We define a valid peering by a peering that does not occur over the same server addresses -func TestPeeringService_Establish_validPeeringInPartition(t *testing.T) { +// Loopback peering within the same cluster/partion should throw an error +func TestPeeringService_Establish_invalidPeeringInSamePartition(t *testing.T) { // TODO(peering): see note on newTestServer, refactor to not use this s := newTestServer(t, nil) client := pbpeering.NewPeeringServiceClient(s.ClientConn(t)) @@ -368,12 +369,48 @@ func TestPeeringService_Establish_validPeeringInPartition(t *testing.T) { require.Nil(t, respE) } +// When tokens have the same name as the dialing cluster but are unknown by ID, we +// should be throwing an error to note the server name conflict. +func TestPeeringService_Establish_serverNameConflict(t *testing.T) { + // TODO(peering): see note on newTestServer, refactor to not use this + s := newTestServer(t, nil) + client := pbpeering.NewPeeringServiceClient(s.ClientConn(t)) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + + // Manufacture token to have the same server name but a PeerID not in the store. + id, err := uuid.GenerateUUID() + require.NoError(t, err, "could not generate uuid") + peeringToken := structs.PeeringToken{ + ServerAddresses: []string{"1.2.3.4:8502"}, + ServerName: s.Server.GetPeeringBackend().GetServerName(), + EstablishmentSecret: "foo", + PeerID: id, + } + + jsonToken, err := json.Marshal(peeringToken) + require.NoError(t, err, "could not marshal peering token") + base64Token := base64.StdEncoding.EncodeToString(jsonToken) + + establishReq := &pbpeering.EstablishRequest{ + PeerName: "peerTwo", + PeeringToken: base64Token, + } + + respE, errE := client.Establish(ctx, establishReq) + require.Error(t, errE) + require.Contains(t, errE.Error(), "conflict - peering token's server name matches the current cluster's server name") + require.Nil(t, respE) +} + func TestPeeringService_Establish(t *testing.T) { // TODO(peering): see note on newTestServer, refactor to not use this s1 := newTestServer(t, nil) client1 := pbpeering.NewPeeringServiceClient(s1.ClientConn(t)) s2 := newTestServer(t, func(conf *consul.Config) { + conf.Datacenter = "dc2" conf.GRPCPort = 5301 }) client2 := pbpeering.NewPeeringServiceClient(s2.ClientConn(t)) @@ -621,38 +658,50 @@ func TestPeeringService_Read_ACLEnforcement(t *testing.T) { } func TestPeeringService_Delete(t *testing.T) { - // TODO(peering): see note on newTestServer, refactor to not use this - s := newTestServer(t, nil) - - p := &pbpeering.Peering{ - ID: testUUID(t), - Name: "foo", - State: pbpeering.PeeringState_ESTABLISHING, - PeerCAPems: nil, - PeerServerName: "test", - PeerServerAddresses: []string{"addr1"}, + tt := map[string]pbpeering.PeeringState{ + "active peering": pbpeering.PeeringState_ACTIVE, + "terminated peering": pbpeering.PeeringState_TERMINATED, } - err := s.Server.FSM().State().PeeringWrite(10, &pbpeering.PeeringWriteRequest{Peering: p}) - require.NoError(t, err) - require.Nil(t, p.DeletedAt) - require.True(t, p.IsActive()) - client := pbpeering.NewPeeringServiceClient(s.ClientConn(t)) + for name, overrideState := range tt { + t.Run(name, func(t *testing.T) { + // TODO(peering): see note on newTestServer, refactor to not use this + s := newTestServer(t, nil) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - t.Cleanup(cancel) + // A pointer is kept for the following peering so that we can modify the object without another PeeringWrite. + p := &pbpeering.Peering{ + ID: testUUID(t), + Name: "foo", + PeerCAPems: nil, + PeerServerName: "test", + PeerServerAddresses: []string{"addr1"}, + } + err := s.Server.FSM().State().PeeringWrite(10, &pbpeering.PeeringWriteRequest{Peering: p}) + require.NoError(t, err) + require.Nil(t, p.DeletedAt) + require.True(t, p.IsActive()) - _, err = client.PeeringDelete(ctx, &pbpeering.PeeringDeleteRequest{Name: "foo"}) - require.NoError(t, err) + // Overwrite the peering state to simulate deleting from a non-initial state. + p.State = overrideState - retry.Run(t, func(r *retry.R) { - _, resp, err := s.Server.FSM().State().PeeringRead(nil, state.Query{Value: "foo"}) - require.NoError(r, err) + client := pbpeering.NewPeeringServiceClient(s.ClientConn(t)) - // Initially the peering will be marked for deletion but eventually the leader - // routine will clean it up. - require.Nil(r, resp) - }) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + + _, err = client.PeeringDelete(ctx, &pbpeering.PeeringDeleteRequest{Name: "foo"}) + require.NoError(t, err) + + retry.Run(t, func(r *retry.R) { + _, resp, err := s.Server.FSM().State().PeeringRead(nil, state.Query{Value: "foo"}) + require.NoError(r, err) + + // Initially the peering will be marked for deletion but eventually the leader + // routine will clean it up. + require.Nil(r, resp) + }) + }) + } } func TestPeeringService_Delete_ACLEnforcement(t *testing.T) { @@ -1057,6 +1106,7 @@ func TestPeeringService_validatePeer(t *testing.T) { s2 := newTestServer(t, func(conf *consul.Config) { conf.GRPCPort = 5301 + conf.Datacenter = "dc2" }) client2 := pbpeering.NewPeeringServiceClient(s2.ClientConn(t)) @@ -1434,6 +1484,7 @@ func newDefaultDeps(t *testing.T, c *consul.Config) consul.Deps { EnterpriseDeps: newDefaultDepsEnterprise(t, logger, c), NewRequestRecorderFunc: middleware.NewRequestRecorder, GetNetRPCInterceptorFunc: middleware.GetNetRPCInterceptor, + XDSStreamLimiter: limiter.NewSessionLimiter(), } } diff --git a/agent/setup.go b/agent/setup.go index c32960518..25353e1ac 100644 --- a/agent/setup.go +++ b/agent/setup.go @@ -18,6 +18,8 @@ import ( "github.com/hashicorp/consul/agent/consul/fsm" "github.com/hashicorp/consul/agent/consul/stream" "github.com/hashicorp/consul/agent/consul/usagemetrics" + "github.com/hashicorp/consul/agent/consul/xdscapacity" + "github.com/hashicorp/consul/agent/grpc-external/limiter" grpc "github.com/hashicorp/consul/agent/grpc-internal" "github.com/hashicorp/consul/agent/grpc-internal/resolver" "github.com/hashicorp/consul/agent/local" @@ -150,6 +152,8 @@ func NewBaseDeps(configLoader ConfigLoader, logOut io.Writer) (BaseDeps, error) d.EventPublisher = stream.NewEventPublisher(10 * time.Second) + d.XDSStreamLimiter = limiter.NewSessionLimiter() + return d, nil } @@ -232,7 +236,9 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau gauges = append(gauges, consul.AutopilotGauges, consul.LeaderCertExpirationGauges, - consul.LeaderPeeringMetrics) + consul.LeaderPeeringMetrics, + xdscapacity.StatsGauges, + ) } // Flatten definitions @@ -275,6 +281,7 @@ func getPrometheusDefs(cfg lib.TelemetryConfig, isServer bool) ([]prometheus.Gau consul.RPCCounters, grpc.StatsCounters, local.StateCounters, + xds.StatsCounters, raftCounters, } // Flatten definitions diff --git a/agent/sidecar_service.go b/agent/sidecar_service.go index e0cb24a0e..3b536223f 100644 --- a/agent/sidecar_service.go +++ b/agent/sidecar_service.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "strings" "time" "github.com/hashicorp/consul/ipaddr" @@ -9,8 +10,15 @@ import ( "github.com/hashicorp/consul/agent/structs" ) -func sidecarServiceID(serviceID string) string { - return serviceID + "-sidecar-proxy" +const sidecarIDSuffix = "-sidecar-proxy" + +func sidecarIDFromServiceID(serviceID string) string { + return serviceID + sidecarIDSuffix +} + +// reverses the sidecarIDFromServiceID operation +func serviceIDFromSidecarID(sidecarID string) string { + return strings.TrimSuffix(sidecarID, sidecarIDSuffix) } // sidecarServiceFromNodeService returns a *structs.NodeService representing a @@ -30,7 +38,7 @@ func sidecarServiceID(serviceID string) string { // registration. This will be the same as the token parameter passed unless the // SidecarService definition contains a distinct one. // TODO: return AddServiceRequest -func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token string) (*structs.NodeService, []*structs.CheckType, string, error) { +func sidecarServiceFromNodeService(ns *structs.NodeService, token string) (*structs.NodeService, []*structs.CheckType, string, error) { if ns.Connect.SidecarService == nil { return nil, nil, "", nil } @@ -43,7 +51,7 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str // Override the ID which must always be consistent for a given outer service // ID. We rely on this for lifecycle management of the nested definition. - sidecar.ID = sidecarServiceID(ns.ID) + sidecar.ID = sidecarIDFromServiceID(ns.ID) // Set some meta we can use to disambiguate between service instances we added // later and are responsible for deregistering. @@ -114,30 +122,18 @@ func (a *Agent) sidecarServiceFromNodeService(ns *structs.NodeService, token str } } - if sidecar.Port < 1 { - port, err := a.sidecarPortFromServiceID(sidecar.CompoundServiceID()) - if err != nil { - return nil, nil, "", err - } - sidecar.Port = port - } - // Setup checks checks, err := ns.Connect.SidecarService.CheckTypes() if err != nil { return nil, nil, "", err } - // Setup default check if none given - if len(checks) < 1 { - checks = sidecarDefaultChecks(ns.ID, sidecar.Proxy.LocalServiceAddress, sidecar.Port) - } return sidecar, checks, token, nil } -// sidecarPortFromServiceID is used to allocate a unique port for a sidecar proxy. +// sidecarPortFromServiceIDLocked is used to allocate a unique port for a sidecar proxy. // This is called immediately before registration to avoid value collisions. This function assumes the state lock is already held. -func (a *Agent) sidecarPortFromServiceID(sidecarCompoundServiceID structs.ServiceID) (int, error) { +func (a *Agent) sidecarPortFromServiceIDLocked(sidecarCompoundServiceID structs.ServiceID) (int, error) { sidecarPort := 0 // Allocate port if needed (min and max inclusive). @@ -202,14 +198,23 @@ func (a *Agent) sidecarPortFromServiceID(sidecarCompoundServiceID structs.Servic return sidecarPort, nil } -func sidecarDefaultChecks(serviceID string, localServiceAddress string, port int) []*structs.CheckType { - // Setup default check if none given +func sidecarDefaultChecks(sidecarID string, sidecarAddress string, proxyServiceAddress string, port int) []*structs.CheckType { + // The check should use the sidecar's address because it makes a request to the sidecar. + // If the sidecar's address is empty, we fall back to the address of the local service, as set in + // sidecar.Proxy.LocalServiceAddress, in the hope that the proxy is also accessible on that address + // (which in most cases it is because it's running as a sidecar in the same network). + // We could instead fall back to the address of the service as set by (ns.Address), but I've kept it using + // sidecar.Proxy.LocalServiceAddress so as to not change things too much in the + // process of fixing #14433. + checkAddress := sidecarAddress + if checkAddress == "" { + checkAddress = proxyServiceAddress + } + serviceID := serviceIDFromSidecarID(sidecarID) return []*structs.CheckType{ { - Name: "Connect Sidecar Listening", - // Default to localhost rather than agent/service public IP. The checks - // can always be overridden if a non-loopback IP is needed. - TCP: ipaddr.FormatAddressPort(localServiceAddress, port), + Name: "Connect Sidecar Listening", + TCP: ipaddr.FormatAddressPort(checkAddress, port), Interval: 10 * time.Second, }, { diff --git a/agent/sidecar_service_test.go b/agent/sidecar_service_test.go index f095670ff..0cee2e374 100644 --- a/agent/sidecar_service_test.go +++ b/agent/sidecar_service_test.go @@ -54,7 +54,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "web-sidecar-proxy", - Port: 2222, + Port: 0, LocallyRegisteredAsSidecar: true, Proxy: structs.ConnectProxyConfig{ DestinationServiceName: "web", @@ -63,18 +63,8 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { LocalServicePort: 1111, }, }, - wantChecks: []*structs.CheckType{ - { - Name: "Connect Sidecar Listening", - TCP: "127.0.0.1:2222", - Interval: 10 * time.Second, - }, - { - Name: "Connect Sidecar Aliasing web1", - AliasService: "web1", - }, - }, - wantToken: "foo", + wantChecks: nil, + wantToken: "foo", }, { name: "all the allowed overrides", @@ -157,7 +147,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { Kind: structs.ServiceKindConnectProxy, ID: "web1-sidecar-proxy", Service: "web-sidecar-proxy", - Port: 2222, + Port: 0, Tags: []string{"foo"}, Meta: map[string]string{"foo": "bar"}, LocallyRegisteredAsSidecar: true, @@ -168,17 +158,7 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { LocalServicePort: 1111, }, }, - wantChecks: []*structs.CheckType{ - { - Name: "Connect Sidecar Listening", - TCP: "127.0.0.1:2222", - Interval: 10 * time.Second, - }, - { - Name: "Connect Sidecar Aliasing web1", - AliasService: "web1", - }, - }, + wantChecks: nil, }, { name: "invalid check type", @@ -218,20 +198,11 @@ func TestAgent_sidecarServiceFromNodeService(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - hcl := ` - ports { - sidecar_min_port = 2222 - sidecar_max_port = 2222 - } - ` - a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl}) - defer a.Shutdown() - ns := tt.sd.NodeService() err := ns.Validate() require.NoError(t, err, "Invalid test case - NodeService must validate") - gotNS, gotChecks, gotToken, err := a.sidecarServiceFromNodeService(ns, tt.token) + gotNS, gotChecks, gotToken, err := sidecarServiceFromNodeService(ns, tt.token) if tt.wantErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.wantErr) @@ -329,7 +300,7 @@ func TestAgent_SidecarPortFromServiceID(t *testing.T) { } ` } - a := StartTestAgent(t, TestAgent{Name: "jones", HCL: hcl}) + a := NewTestAgent(t, hcl) defer a.Shutdown() if tt.preRegister != nil { @@ -337,7 +308,7 @@ func TestAgent_SidecarPortFromServiceID(t *testing.T) { require.NoError(t, err) } - gotPort, err := a.sidecarPortFromServiceID(structs.ServiceID{ID: tt.serviceID, EnterpriseMeta: tt.enterpriseMeta}) + gotPort, err := a.sidecarPortFromServiceIDLocked(structs.ServiceID{ID: tt.serviceID, EnterpriseMeta: tt.enterpriseMeta}) if tt.wantErr != "" { require.Error(t, err) @@ -350,3 +321,73 @@ func TestAgent_SidecarPortFromServiceID(t *testing.T) { }) } } + +func TestAgent_SidecarDefaultChecks(t *testing.T) { + tests := []struct { + name string + serviceID string + svcAddress string + proxyLocalSvcAddress string + port int + wantChecks []*structs.CheckType + }{{ + name: "uses proxy address for check", + serviceID: "web1-1-sidecar-proxy", + svcAddress: "123.123.123.123", + proxyLocalSvcAddress: "255.255.255.255", + port: 2222, + wantChecks: []*structs.CheckType{ + { + Name: "Connect Sidecar Listening", + TCP: "123.123.123.123:2222", + Interval: 10 * time.Second, + }, + { + Name: "Connect Sidecar Aliasing web1-1", + AliasService: "web1-1", + }, + }, + }, + { + name: "uses proxy.local_service_address for check if proxy address is empty", + serviceID: "web1-1-sidecar-proxy", + proxyLocalSvcAddress: "1.2.3.4", + port: 2222, + wantChecks: []*structs.CheckType{ + { + Name: "Connect Sidecar Listening", + TCP: "1.2.3.4:2222", + Interval: 10 * time.Second, + }, + { + Name: "Connect Sidecar Aliasing web1-1", + AliasService: "web1-1", + }, + }, + }, + { + name: "redundant name", + serviceID: "1-sidecar-proxy-web1-sidecar-proxy", + svcAddress: "123.123.123.123", + proxyLocalSvcAddress: "255.255.255.255", + port: 2222, + wantChecks: []*structs.CheckType{ + { + Name: "Connect Sidecar Listening", + TCP: "123.123.123.123:2222", + Interval: 10 * time.Second, + }, + { + Name: "Connect Sidecar Aliasing 1-sidecar-proxy-web1", + AliasService: "1-sidecar-proxy-web1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotChecks := sidecarDefaultChecks(tt.serviceID, tt.svcAddress, tt.proxyLocalSvcAddress, tt.port) + require.Equal(t, tt.wantChecks, gotChecks) + }) + } +} diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index 8b3b0a8d2..72efbffce 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -3,12 +3,13 @@ package structs import ( "errors" "fmt" - "github.com/miekg/dns" "net" "strconv" "strings" "time" + "github.com/miekg/dns" + "github.com/hashicorp/go-multierror" "github.com/mitchellh/hashstructure" "github.com/mitchellh/mapstructure" @@ -108,6 +109,8 @@ type ServiceConfigEntry struct { UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` Destination *DestinationConfig `json:",omitempty"` MaxInboundConnections int `json:",omitempty" alias:"max_inbound_connections"` + LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"` + LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"` Meta map[string]string `json:",omitempty"` acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` @@ -362,6 +365,13 @@ func (e *ProxyConfigEntry) Normalize() error { } e.Kind = ProxyDefaults + + // proxy default config only accepts global configs + // this check is replicated in normalize() and validate(), + // since validate is not called by all the endpoints (e.g., delete) + if e.Name != "" && e.Name != ProxyConfigGlobal { + return fmt.Errorf("invalid name (%q), only %q is supported", e.Name, ProxyConfigGlobal) + } e.Name = ProxyConfigGlobal e.EnterpriseMeta.Normalize() @@ -961,6 +971,11 @@ type PassiveHealthCheck struct { // MaxFailures is the count of consecutive failures that results in a host // being removed from the pool. MaxFailures uint32 `json:",omitempty" alias:"max_failures"` + + // EnforcingConsecutive5xx is the % chance that a host will be actually ejected + // when an outlier status is detected through consecutive 5xx. + // This setting can be used to disable ejection or to ramp it up slowly. Defaults to 100. + EnforcingConsecutive5xx *uint32 `json:",omitempty" alias:"enforcing_consecutive_5xx"` } func (chk *PassiveHealthCheck) Clone() *PassiveHealthCheck { diff --git a/agent/structs/config_entry_discoverychain.go b/agent/structs/config_entry_discoverychain.go index aaac8652a..7867602ca 100644 --- a/agent/structs/config_entry_discoverychain.go +++ b/agent/structs/config_entry_discoverychain.go @@ -954,17 +954,28 @@ func (e *ServiceResolverConfigEntry) Validate() error { r := e.Redirect + if err := r.ValidateEnterprise(); err != nil { + return fmt.Errorf("Redirect: %s", err.Error()) + } + if len(e.Failover) > 0 { return fmt.Errorf("Redirect and Failover cannot both be set") } // TODO(rb): prevent subsets and default subsets from being defined? - if r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Partition == "" && r.Datacenter == "" { + if r.isEmpty() { return fmt.Errorf("Redirect is empty") } - if r.Service == "" { + switch { + case r.Peer != "" && r.ServiceSubset != "": + return fmt.Errorf("Redirect.Peer cannot be set with Redirect.ServiceSubset") + case r.Peer != "" && r.Partition != "": + return fmt.Errorf("Redirect.Partition cannot be set with Redirect.Peer") + case r.Peer != "" && r.Datacenter != "": + return fmt.Errorf("Redirect.Peer cannot be set with Redirect.Datacenter") + case r.Service == "": if r.ServiceSubset != "" { return fmt.Errorf("Redirect.ServiceSubset defined without Redirect.Service") } @@ -974,9 +985,12 @@ func (e *ServiceResolverConfigEntry) Validate() error { if r.Partition != "" { return fmt.Errorf("Redirect.Partition defined without Redirect.Service") } - } else if r.Service == e.Name { - if r.ServiceSubset != "" && !isSubset(r.ServiceSubset) { - return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, r.Service) + if r.Peer != "" { + return fmt.Errorf("Redirect.Peer defined without Redirect.Service") + } + case r.ServiceSubset != "" && (r.Service == "" || r.Service == e.Name): + if !isSubset(r.ServiceSubset) { + return fmt.Errorf("Redirect.ServiceSubset %q is not a valid subset of %q", r.ServiceSubset, e.Name) } } } @@ -988,18 +1002,59 @@ func (e *ServiceResolverConfigEntry) Validate() error { return fmt.Errorf("Cross-datacenter failover is only supported in the default partition") } - if subset != "*" && !isSubset(subset) { - return fmt.Errorf("Bad Failover[%q]: not a valid subset", subset) + errorPrefix := fmt.Sprintf("Bad Failover[%q]: ", subset) + + if err := f.ValidateEnterprise(); err != nil { + return fmt.Errorf(errorPrefix + err.Error()) } - if f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 { - return fmt.Errorf("Bad Failover[%q] one of Service, ServiceSubset, Namespace, or Datacenters is required", subset) + if subset != "*" && !isSubset(subset) { + return fmt.Errorf(errorPrefix + "not a valid subset subset") + } + + if f.isEmpty() { + return fmt.Errorf(errorPrefix + "one of Service, ServiceSubset, Namespace, Targets, or Datacenters is required") } if f.ServiceSubset != "" { if f.Service == "" || f.Service == e.Name { if !isSubset(f.ServiceSubset) { - return fmt.Errorf("Bad Failover[%q].ServiceSubset %q is not a valid subset of %q", subset, f.ServiceSubset, f.Service) + return fmt.Errorf("%sServiceSubset %q is not a valid subset of %q", errorPrefix, f.ServiceSubset, f.Service) + } + } + } + + if len(f.Datacenters) != 0 && len(f.Targets) != 0 { + return fmt.Errorf("Bad Failover[%q]: Targets cannot be set with Datacenters", subset) + } + + if f.ServiceSubset != "" && len(f.Targets) != 0 { + return fmt.Errorf("Bad Failover[%q]: Targets cannot be set with ServiceSubset", subset) + } + + if f.Service != "" && len(f.Targets) != 0 { + return fmt.Errorf("Bad Failover[%q]: Targets cannot be set with Service", subset) + } + + for i, target := range f.Targets { + errorPrefix := fmt.Sprintf("Bad Failover[%q].Targets[%d]: ", subset, i) + + if err := target.ValidateEnterprise(); err != nil { + return fmt.Errorf(errorPrefix + err.Error()) + } + + switch { + case target.Peer != "" && target.ServiceSubset != "": + return fmt.Errorf(errorPrefix + "Peer cannot be set with ServiceSubset") + case target.Peer != "" && target.Partition != "": + return fmt.Errorf(errorPrefix + "Partition cannot be set with Peer") + case target.Peer != "" && target.Datacenter != "": + return fmt.Errorf(errorPrefix + "Peer cannot be set with Datacenter") + case target.Partition != "" && target.Datacenter != "": + return fmt.Errorf(errorPrefix + "Partition cannot be set with Datacenter") + case target.ServiceSubset != "" && (target.Service == "" || target.Service == e.Name): + if !isSubset(target.ServiceSubset) { + return fmt.Errorf("%sServiceSubset %q is not a valid subset of %q", errorPrefix, target.ServiceSubset, e.Name) } } } @@ -1107,9 +1162,24 @@ func (e *ServiceResolverConfigEntry) ListRelatedServices() []ServiceID { if len(e.Failover) > 0 { for _, failover := range e.Failover { - failoverID := NewServiceID(defaultIfEmpty(failover.Service, e.Name), failover.GetEnterpriseMeta(&e.EnterpriseMeta)) - if failoverID != svcID { - found[failoverID] = struct{}{} + if len(failover.Targets) == 0 { + failoverID := NewServiceID(defaultIfEmpty(failover.Service, e.Name), failover.GetEnterpriseMeta(&e.EnterpriseMeta)) + if failoverID != svcID { + found[failoverID] = struct{}{} + } + continue + } + + for _, target := range failover.Targets { + // We can't know about related services on cluster peers. + if target.Peer != "" { + continue + } + + failoverID := NewServiceID(defaultIfEmpty(target.Service, e.Name), target.GetEnterpriseMeta(failover.GetEnterpriseMeta(&e.EnterpriseMeta))) + if failoverID != svcID { + found[failoverID] = struct{}{} + } } } } @@ -1171,12 +1241,32 @@ type ServiceResolverRedirect struct { // Datacenter is the datacenter to resolve the service from instead of the // current one (optional). Datacenter string `json:",omitempty"` + + // Peer is the name of the cluster peer to resolve the service from instead + // of the current one (optional). + Peer string `json:",omitempty"` +} + +func (r *ServiceResolverRedirect) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: r.Service, + ServiceSubset: r.ServiceSubset, + Namespace: r.Namespace, + Partition: r.Partition, + Datacenter: r.Datacenter, + Peer: r.Peer, + } +} + +func (r *ServiceResolverRedirect) isEmpty() bool { + return r.Service == "" && r.ServiceSubset == "" && r.Namespace == "" && r.Partition == "" && r.Datacenter == "" && r.Peer == "" } // There are some restrictions on what is allowed in here: // -// - Service, ServiceSubset, Namespace, and Datacenters cannot all be -// empty at once. +// - Service, ServiceSubset, Namespace, Datacenters, and Targets cannot all be +// empty at once. When Targets is defined, the other fields should not be +// populated. // type ServiceResolverFailover struct { // Service is the service to resolve instead of the default as the failover @@ -1205,6 +1295,56 @@ type ServiceResolverFailover struct { // // This is a DESTINATION during failover. Datacenters []string `json:",omitempty"` + + // Targets specifies a fixed list of failover targets to try. We never try a + // target multiple times, so those are subtracted from this list before + // proceeding. + // + // This is a DESTINATION during failover. + Targets []ServiceResolverFailoverTarget `json:",omitempty"` +} + +func (t *ServiceResolverFailover) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + } +} + +func (f *ServiceResolverFailover) isEmpty() bool { + return f.Service == "" && f.ServiceSubset == "" && f.Namespace == "" && len(f.Datacenters) == 0 && len(f.Targets) == 0 +} + +type ServiceResolverFailoverTarget struct { + // Service specifies the name of the service to try during failover. + Service string `json:",omitempty"` + + // ServiceSubset specifies the service subset to try during failover. + ServiceSubset string `json:",omitempty" alias:"service_subset"` + + // Partition specifies the partition to try during failover. + Partition string `json:",omitempty"` + + // Namespace specifies the namespace to try during failover. + Namespace string `json:",omitempty"` + + // Datacenter specifies the datacenter to try during failover. + Datacenter string `json:",omitempty"` + + // Peer specifies the name of the cluster peer to try during failover. + Peer string `json:",omitempty"` +} + +func (t *ServiceResolverFailoverTarget) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + Partition: t.Partition, + Datacenter: t.Datacenter, + Peer: t.Peer, + } } // LoadBalancer determines the load balancing policy and configuration for services diff --git a/agent/structs/config_entry_discoverychain_oss.go b/agent/structs/config_entry_discoverychain_oss.go index 0dcf1cc8f..ef00e3992 100644 --- a/agent/structs/config_entry_discoverychain_oss.go +++ b/agent/structs/config_entry_discoverychain_oss.go @@ -4,6 +4,8 @@ package structs import ( + "fmt" + "github.com/hashicorp/consul/acl" ) @@ -25,12 +27,56 @@ func (redir *ServiceResolverRedirect) GetEnterpriseMeta(_ *acl.EnterpriseMeta) * return DefaultEnterpriseMetaInDefaultPartition() } +// ValidateEnterprise validates that enterprise fields are only set +// with enterprise binaries. +func (redir *ServiceResolverRedirect) ValidateEnterprise() error { + if redir.Partition != "" { + return fmt.Errorf("Setting Partition requires Consul Enterprise") + } + + if redir.Namespace != "" { + return fmt.Errorf("Setting Namespace requires Consul Enterprise") + } + + return nil +} + // GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from // fields in the ServiceResolverFailover func (failover *ServiceResolverFailover) GetEnterpriseMeta(_ *acl.EnterpriseMeta) *acl.EnterpriseMeta { return DefaultEnterpriseMetaInDefaultPartition() } +// ValidateEnterprise validates that enterprise fields are only set +// with enterprise binaries. +func (failover *ServiceResolverFailover) ValidateEnterprise() error { + if failover.Namespace != "" { + return fmt.Errorf("Setting Namespace requires Consul Enterprise") + } + + return nil +} + +// GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from +// fields in the ServiceResolverFailoverTarget +func (target *ServiceResolverFailoverTarget) GetEnterpriseMeta(_ *acl.EnterpriseMeta) *acl.EnterpriseMeta { + return DefaultEnterpriseMetaInDefaultPartition() +} + +// ValidateEnterprise validates that enterprise fields are only set +// with enterprise binaries. +func (redir *ServiceResolverFailoverTarget) ValidateEnterprise() error { + if redir.Partition != "" { + return fmt.Errorf("Setting Partition requires Consul Enterprise") + } + + if redir.Namespace != "" { + return fmt.Errorf("Setting Namespace requires Consul Enterprise") + } + + return nil +} + // GetEnterpriseMeta is used to synthesize the EnterpriseMeta struct from // fields in the DiscoveryChainRequest func (req *DiscoveryChainRequest) GetEnterpriseMeta() *acl.EnterpriseMeta { diff --git a/agent/structs/config_entry_discoverychain_oss_test.go b/agent/structs/config_entry_discoverychain_oss_test.go new file mode 100644 index 000000000..9f962c8bd --- /dev/null +++ b/agent/structs/config_entry_discoverychain_oss_test.go @@ -0,0 +1,153 @@ +//go:build !consulent +// +build !consulent + +package structs + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestServiceResolverConfigEntry_OSS(t *testing.T) { + type testcase struct { + name string + entry *ServiceResolverConfigEntry + normalizeErr string + validateErr string + // check is called between normalize and validate + check func(t *testing.T, entry *ServiceResolverConfigEntry) + } + + cases := []testcase{ + { + name: "failover with a namespace on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Service: "backup", + Namespace: "ns1", + }, + }, + }, + validateErr: `Bad Failover["*"]: Setting Namespace requires Consul Enterprise`, + }, + { + name: "failover Targets cannot set Namespace on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{{Namespace: "ns1"}}, + }, + }, + }, + validateErr: `Bad Failover["*"].Targets[0]: Setting Namespace requires Consul Enterprise`, + }, + { + name: "failover Targets cannot set Partition on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{{Partition: "ap1"}}, + }, + }, + }, + validateErr: `Bad Failover["*"].Targets[0]: Setting Partition requires Consul Enterprise`, + }, + { + name: "setting failover Namespace on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": {Namespace: "ns1"}, + }, + }, + validateErr: `Bad Failover["*"]: Setting Namespace requires Consul Enterprise`, + }, + { + name: "setting redirect Namespace on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Namespace: "ns1", + }, + }, + validateErr: `Redirect: Setting Namespace requires Consul Enterprise`, + }, + { + name: "setting redirect Partition on OSS", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Partition: "ap1", + }, + }, + validateErr: `Redirect: Setting Partition requires Consul Enterprise`, + }, + } + + // Bulk add a bunch of similar validation cases. + for _, invalidSubset := range invalidSubsetNames { + tc := testcase{ + name: "invalid subset name: " + invalidSubset, + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Subsets: map[string]ServiceResolverSubset{ + invalidSubset: {OnlyPassing: true}, + }, + }, + validateErr: fmt.Sprintf("Subset %q is invalid", invalidSubset), + } + cases = append(cases, tc) + } + + for _, goodSubset := range validSubsetNames { + tc := testcase{ + name: "valid subset name: " + goodSubset, + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Subsets: map[string]ServiceResolverSubset{ + goodSubset: {OnlyPassing: true}, + }, + }, + } + cases = append(cases, tc) + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := tc.entry.Normalize() + if tc.normalizeErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.normalizeErr) + return + } + require.NoError(t, err) + + if tc.check != nil { + tc.check(t, tc.entry) + } + + err = tc.entry.Validate() + if tc.validateErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.validateErr) + return + } + require.NoError(t, err) + }) + } +} diff --git a/agent/structs/config_entry_discoverychain_test.go b/agent/structs/config_entry_discoverychain_test.go index a3fb49b4a..0d6691fa0 100644 --- a/agent/structs/config_entry_discoverychain_test.go +++ b/agent/structs/config_entry_discoverychain_test.go @@ -165,6 +165,34 @@ func TestConfigEntries_ListRelatedServices_AndACLs(t *testing.T) { }, }, }, + { + name: "resolver: failover with targets", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{ + {Service: "other1"}, + {Datacenter: "dc2"}, + {Peer: "cluster-01"}, + }, + }, + }, + }, + expectServices: []ServiceID{NewServiceID("other1", nil)}, + expectACLs: []testACL{ + defaultDenyCase, + readTestCase, + writeTestCaseDenied, + { + name: "can write test (with other1:read)", + authorizer: newServiceACL(t, []string{"other1"}, []string{"test"}), + canRead: true, + canWrite: true, + }, + }, + }, { name: "splitter: self", entry: &ServiceSplitterConfigEntry{ @@ -595,6 +623,15 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, validateErr: "Redirect is empty", }, + { + name: "empty redirect", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{}, + }, + validateErr: "Redirect is empty", + }, { name: "redirect subset with no service", entry: &ServiceResolverConfigEntry{ @@ -606,17 +643,6 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, validateErr: "Redirect.ServiceSubset defined without Redirect.Service", }, - { - name: "redirect namespace with no service", - entry: &ServiceResolverConfigEntry{ - Kind: ServiceResolver, - Name: "test", - Redirect: &ServiceResolverRedirect{ - Namespace: "alternate", - }, - }, - validateErr: "Redirect.Namespace defined without Redirect.Service", - }, { name: "self redirect with invalid subset", entry: &ServiceResolverConfigEntry{ @@ -629,6 +655,41 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, validateErr: `Redirect.ServiceSubset "gone" is not a valid subset of "test"`, }, + { + name: "redirect with peer and subset", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Peer: "cluster-01", + ServiceSubset: "gone", + }, + }, + validateErr: `Redirect.Peer cannot be set with Redirect.ServiceSubset`, + }, + { + name: "redirect with peer and datacenter", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Peer: "cluster-01", + Datacenter: "dc2", + }, + }, + validateErr: `Redirect.Peer cannot be set with Redirect.Datacenter`, + }, + { + name: "redirect with peer and datacenter", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Peer: "cluster-01", + }, + }, + validateErr: `Redirect.Peer defined without Redirect.Service`, + }, { name: "self redirect with valid subset", entry: &ServiceResolverConfigEntry{ @@ -643,6 +704,17 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, }, }, + { + name: "redirect to peer", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Redirect: &ServiceResolverRedirect{ + Service: "other", + Peer: "cluster-01", + }, + }, + }, { name: "simple wildcard failover", entry: &ServiceResolverConfigEntry{ @@ -695,7 +767,7 @@ func TestServiceResolverConfigEntry(t *testing.T) { "v1": {}, }, }, - validateErr: `Bad Failover["v1"] one of Service, ServiceSubset, Namespace, or Datacenters is required`, + validateErr: `Bad Failover["v1"]: one of Service, ServiceSubset, Namespace, Targets, or Datacenters is required`, }, { name: "failover to self using invalid subset", @@ -712,7 +784,7 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, }, }, - validateErr: `Bad Failover["v1"].ServiceSubset "gone" is not a valid subset of "test"`, + validateErr: `Bad Failover["v1"]: ServiceSubset "gone" is not a valid subset of "test"`, }, { name: "failover to self using valid subset", @@ -745,6 +817,109 @@ func TestServiceResolverConfigEntry(t *testing.T) { }, validateErr: `Bad Failover["*"].Datacenters: found empty datacenter`, }, + { + name: "failover target with an invalid subset", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{{ServiceSubset: "subset"}}, + }, + }, + }, + validateErr: `Bad Failover["*"].Targets[0]: ServiceSubset "subset" is not a valid subset of "test"`, + }, + { + name: "failover targets can't have Peer and ServiceSubset", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", ServiceSubset: "subset"}}, + }, + }, + }, + validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with ServiceSubset`, + }, + { + name: "failover targets can't have Peer and Datacenter", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01", Datacenter: "dc1"}}, + }, + }, + }, + validateErr: `Bad Failover["*"].Targets[0]: Peer cannot be set with Datacenter`, + }, + { + name: "failover Targets cannot be set with Datacenters", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Datacenters: []string{"a"}, + Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, + }, + }, + }, + validateErr: `Bad Failover["*"]: Targets cannot be set with Datacenters`, + }, + { + name: "failover Targets cannot be set with ServiceSubset", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + ServiceSubset: "v2", + Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, + }, + }, + Subsets: map[string]ServiceResolverSubset{ + "v2": {Filter: "Service.Meta.version == v2"}, + }, + }, + validateErr: `Bad Failover["*"]: Targets cannot be set with ServiceSubset`, + }, + { + name: "failover Targets cannot be set with Service", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Service: "another-service", + Targets: []ServiceResolverFailoverTarget{{Peer: "cluster-01"}}, + }, + }, + Subsets: map[string]ServiceResolverSubset{ + "v2": {Filter: "Service.Meta.version == v2"}, + }, + }, + validateErr: `Bad Failover["*"]: Targets cannot be set with Service`, + }, + { + name: "complicated failover targets", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test", + Failover: map[string]ServiceResolverFailover{ + "*": { + Targets: []ServiceResolverFailoverTarget{ + {Peer: "cluster-01", Service: "test-v2"}, + {Service: "test-v2", ServiceSubset: "test"}, + {Datacenter: "dc2"}, + }, + }, + }, + }, + }, { name: "bad connect timeout", entry: &ServiceResolverConfigEntry{ diff --git a/agent/structs/config_entry_mesh.go b/agent/structs/config_entry_mesh.go index 868c07a9f..4880a3692 100644 --- a/agent/structs/config_entry_mesh.go +++ b/agent/structs/config_entry_mesh.go @@ -17,6 +17,8 @@ type MeshConfigEntry struct { HTTP *MeshHTTPConfig `json:",omitempty"` + Peering *PeeringMeshConfig `json:",omitempty"` + Meta map[string]string `json:",omitempty"` acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` RaftIndex @@ -48,6 +50,16 @@ type MeshHTTPConfig struct { SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"` } +// PeeringMeshConfig contains cluster-wide options pertaining to peering. +type PeeringMeshConfig struct { + // PeerThroughMeshGateways determines whether peering traffic between + // control planes should flow through mesh gateways. If enabled, + // Consul servers will advertise mesh gateway addresses as their own. + // Additionally, mesh gateways will configure themselves to expose + // the local servers using a peering-specific SNI. + PeerThroughMeshGateways bool `alias:"peer_through_mesh_gateways"` +} + func (e *MeshConfigEntry) GetKind() string { return MeshConfig } diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index c3f5c7a98..7a699417f 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -216,6 +216,85 @@ func testConfigEntries_ListRelatedServices_AndACLs(t *testing.T, cases []configE } } +func TestDecodeConfigEntry_ServiceDefaults(t *testing.T) { + + for _, tc := range []struct { + name string + camel string + snake string + expect ConfigEntry + expectErr string + }{ + { + name: "service-defaults-with-MaxInboundConnections", + snake: ` + kind = "service-defaults" + name = "external" + protocol = "tcp" + destination { + addresses = [ + "api.google.com", + "web.google.com" + ] + port = 8080 + } + max_inbound_connections = 14 + `, + camel: ` + Kind = "service-defaults" + Name = "external" + Protocol = "tcp" + Destination { + Addresses = [ + "api.google.com", + "web.google.com" + ] + Port = 8080 + } + MaxInboundConnections = 14 + `, + expect: &ServiceConfigEntry{ + Kind: "service-defaults", + Name: "external", + Protocol: "tcp", + Destination: &DestinationConfig{ + Addresses: []string{ + "api.google.com", + "web.google.com", + }, + Port: 8080, + }, + MaxInboundConnections: 14, + }, + }, + } { + tc := tc + + testbody := func(t *testing.T, body string) { + var raw map[string]interface{} + err := hcl.Decode(&raw, body) + require.NoError(t, err) + + got, err := DecodeConfigEntry(raw) + if tc.expectErr != "" { + require.Nil(t, got) + require.Error(t, err) + requireContainsLower(t, err.Error(), tc.expectErr) + } else { + require.NoError(t, err) + require.Equal(t, tc.expect, got) + } + } + + t.Run(tc.name+" (snake case)", func(t *testing.T) { + testbody(t, tc.snake) + }) + t.Run(tc.name+" (camel case)", func(t *testing.T) { + testbody(t, tc.camel) + }) + } +} + // TestDecodeConfigEntry is the 'structs' mirror image of // command/config/write/config_write_test.go:TestParseConfigEntry func TestDecodeConfigEntry(t *testing.T) { @@ -1736,6 +1815,9 @@ func TestDecodeConfigEntry(t *testing.T) { http { sanitize_x_forwarded_client_cert = true } + peering { + peer_through_mesh_gateways = true + } `, camel: ` Kind = "mesh" @@ -1766,7 +1848,10 @@ func TestDecodeConfigEntry(t *testing.T) { } HTTP { SanitizeXForwardedClientCert = true - } + } + Peering { + PeerThroughMeshGateways = true + } `, expect: &MeshConfigEntry{ Meta: map[string]string{ @@ -1797,6 +1882,9 @@ func TestDecodeConfigEntry(t *testing.T) { HTTP: &MeshHTTPConfig{ SanitizeXForwardedClientCert: true, }, + Peering: &PeeringMeshConfig{ + PeerThroughMeshGateways: true, + }, }, }, { @@ -2675,8 +2763,9 @@ func TestUpstreamConfig_MergeInto(t *testing.T) { MaxConcurrentRequests: intPointer(12), }, "passive_health_check": &PassiveHealthCheck{ - MaxFailures: 13, - Interval: 14 * time.Second, + MaxFailures: 13, + Interval: 14 * time.Second, + EnforcingConsecutive5xx: uintPointer(80), }, "mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeLocal}, }, @@ -2691,8 +2780,9 @@ func TestUpstreamConfig_MergeInto(t *testing.T) { MaxConcurrentRequests: intPointer(12), }, "passive_health_check": &PassiveHealthCheck{ - MaxFailures: 13, - Interval: 14 * time.Second, + MaxFailures: 13, + Interval: 14 * time.Second, + EnforcingConsecutive5xx: uintPointer(80), }, "mesh_gateway": MeshGatewayConfig{Mode: MeshGatewayModeLocal}, }, @@ -2865,6 +2955,28 @@ func TestParseUpstreamConfig(t *testing.T) { } } +func TestProxyConfigEntry(t *testing.T) { + cases := map[string]configEntryTestcase{ + "proxy config name provided is not global": { + entry: &ProxyConfigEntry{ + Name: "foo", + }, + normalizeErr: `invalid name ("foo"), only "global" is supported`, + }, + "proxy config has no name": { + entry: &ProxyConfigEntry{ + Name: "", + }, + expected: &ProxyConfigEntry{ + Name: ProxyConfigGlobal, + Kind: ProxyDefaults, + EnterpriseMeta: *acl.DefaultEnterpriseMeta(), + }, + }, + } + testConfigEntryNormalizeAndValidate(t, cases) +} + func requireContainsLower(t *testing.T, haystack, needle string) { t.Helper() require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle)) @@ -2967,3 +3079,7 @@ func testConfigEntryNormalizeAndValidate(t *testing.T, cases map[string]configEn }) } } + +func uintPointer(v uint32) *uint32 { + return &v +} diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index dfb0c9ab4..9f319ad09 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -224,6 +224,10 @@ type IssuedCert struct { // AgentURI is the cert URI value. AgentURI string `json:",omitempty"` + // ServerURI is the URI value of a cert issued for a server agent. + // The same URI is shared by all servers in a Consul datacenter. + ServerURI string `json:",omitempty"` + // Kind is the kind of service for which the cert was issued. Kind ServiceKind `json:",omitempty"` // KindURI is the cert URI value. @@ -378,9 +382,12 @@ func (c *CAConfiguration) GetCommonConfig() (*CommonCAProviderConfig, error) { } type CommonCAProviderConfig struct { - LeafCertTTL time.Duration + LeafCertTTL time.Duration + RootCertTTL time.Duration + + // IntermediateCertTTL is only valid in the primary datacenter, and determines + // the duration that any signed intermediates are valid for. IntermediateCertTTL time.Duration - RootCertTTL time.Duration SkipValidate bool diff --git a/agent/structs/discovery_chain.go b/agent/structs/discovery_chain.go index 9d4a8ef91..ca64d070d 100644 --- a/agent/structs/discovery_chain.go +++ b/agent/structs/discovery_chain.go @@ -53,28 +53,15 @@ type CompiledDiscoveryChain struct { Targets map[string]*DiscoveryTarget `json:",omitempty"` } -func (c *CompiledDiscoveryChain) WillFailoverThroughMeshGateway(node *DiscoveryGraphNode) bool { - if node.Type != DiscoveryGraphNodeTypeResolver { - return false - } - failover := node.Resolver.Failover - - if failover != nil && len(failover.Targets) > 0 { - for _, failTargetID := range failover.Targets { - failTarget := c.Targets[failTargetID] - switch failTarget.MeshGateway.Mode { - case MeshGatewayModeLocal, MeshGatewayModeRemote: - return true - } - } - } - return false -} - // ID returns an ID that encodes the service, namespace, partition, and datacenter. // This ID allows us to compare a discovery chain target to the chain upstream itself. func (c *CompiledDiscoveryChain) ID() string { - return chainID("", c.ServiceName, c.Namespace, c.Partition, c.Datacenter) + return chainID(DiscoveryTargetOpts{ + Service: c.ServiceName, + Namespace: c.Namespace, + Partition: c.Partition, + Datacenter: c.Datacenter, + }) } func (c *CompiledDiscoveryChain) CompoundServiceName() ServiceName { @@ -203,6 +190,7 @@ type DiscoveryTarget struct { Namespace string `json:",omitempty"` Partition string `json:",omitempty"` Datacenter string `json:",omitempty"` + Peer string `json:",omitempty"` MeshGateway MeshGatewayConfig `json:",omitempty"` Subset ServiceResolverSubset `json:",omitempty"` @@ -258,28 +246,52 @@ func (t *DiscoveryTarget) UnmarshalJSON(data []byte) error { return nil } -func NewDiscoveryTarget(service, serviceSubset, namespace, partition, datacenter string) *DiscoveryTarget { +type DiscoveryTargetOpts struct { + Service string + ServiceSubset string + Namespace string + Partition string + Datacenter string + Peer string +} + +func NewDiscoveryTarget(opts DiscoveryTargetOpts) *DiscoveryTarget { t := &DiscoveryTarget{ - Service: service, - ServiceSubset: serviceSubset, - Namespace: namespace, - Partition: partition, - Datacenter: datacenter, + Service: opts.Service, + ServiceSubset: opts.ServiceSubset, + Namespace: opts.Namespace, + Partition: opts.Partition, + Datacenter: opts.Datacenter, + Peer: opts.Peer, } t.setID() return t } -func chainID(subset, service, namespace, partition, dc string) string { - // NOTE: this format is similar to the SNI syntax for simplicity - if subset == "" { - return fmt.Sprintf("%s.%s.%s.%s", service, namespace, partition, dc) +func (t *DiscoveryTarget) ToDiscoveryTargetOpts() DiscoveryTargetOpts { + return DiscoveryTargetOpts{ + Service: t.Service, + ServiceSubset: t.ServiceSubset, + Namespace: t.Namespace, + Partition: t.Partition, + Datacenter: t.Datacenter, + Peer: t.Peer, } - return fmt.Sprintf("%s.%s.%s.%s.%s", subset, service, namespace, partition, dc) +} + +func chainID(opts DiscoveryTargetOpts) string { + // NOTE: this format is similar to the SNI syntax for simplicity + if opts.Peer != "" { + return fmt.Sprintf("%s.%s.default.external.%s", opts.Service, opts.Namespace, opts.Peer) + } + if opts.ServiceSubset == "" { + return fmt.Sprintf("%s.%s.%s.%s", opts.Service, opts.Namespace, opts.Partition, opts.Datacenter) + } + return fmt.Sprintf("%s.%s.%s.%s.%s", opts.ServiceSubset, opts.Service, opts.Namespace, opts.Partition, opts.Datacenter) } func (t *DiscoveryTarget) setID() { - t.ID = chainID(t.ServiceSubset, t.Service, t.Namespace, t.Partition, t.Datacenter) + t.ID = chainID(t.ToDiscoveryTargetOpts()) } func (t *DiscoveryTarget) String() string { diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 22fb47ca9..b3b832567 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -353,7 +353,7 @@ func (q QueryOptions) Timeout(rpcHoldTimeout, maxQueryTime, defaultQueryTime tim q.MaxQueryTime = defaultQueryTime } // Timeout after maximum jitter has elapsed. - q.MaxQueryTime += lib.RandomStagger(q.MaxQueryTime / JitterFraction) + q.MaxQueryTime += q.MaxQueryTime / JitterFraction return q.MaxQueryTime + rpcHoldTimeout } @@ -1257,8 +1257,9 @@ type NodeService struct { // a pointer so that we never have to nil-check this. Connect ServiceConnect + // TODO: rename to reflect that this is used to express future intent to register. // LocallyRegisteredAsSidecar is private as it is only used by a local agent - // state to track if the service was registered from a nested sidecar_service + // state to track if the service was or will be registered from a nested sidecar_service // block. We need to track that so we can know whether we need to deregister // it automatically too if it's removed from the service definition or if the // parent service is deregistered. Relying only on ID would cause us to diff --git a/agent/structs/testing_catalog.go b/agent/structs/testing_catalog.go index c9fcf017d..f026f6091 100644 --- a/agent/structs/testing_catalog.go +++ b/agent/structs/testing_catalog.go @@ -53,6 +53,28 @@ func TestNodeServiceWithName(t testing.T, name string) *NodeService { } } +const peerTrustDomain = "1c053652-8512-4373-90cf-5a7f6263a994.consul" + +func TestNodeServiceWithNameInPeer(t testing.T, name string, peer string) *NodeService { + service := "payments" + return &NodeService{ + Kind: ServiceKindTypical, + Service: name, + Port: 8080, + Connect: ServiceConnect{ + PeerMeta: &PeeringServiceMeta{ + SNI: []string{ + service + ".default.default." + peer + ".external." + peerTrustDomain, + }, + SpiffeID: []string{ + "spiffe://" + peerTrustDomain + "/ns/default/dc/" + peer + "-dc/svc/" + service, + }, + Protocol: "tcp", + }, + }, + } +} + // TestNodeServiceProxy returns a *NodeService representing a valid // Connect proxy. func TestNodeServiceProxy(t testing.T) *NodeService { diff --git a/agent/structs/testing_connect_proxy_config.go b/agent/structs/testing_connect_proxy_config.go index ad918927a..fdee3f693 100644 --- a/agent/structs/testing_connect_proxy_config.go +++ b/agent/structs/testing_connect_proxy_config.go @@ -26,7 +26,7 @@ func TestUpstreams(t testing.T) Upstreams { Config: map[string]interface{}{ // Float because this is how it is decoded by JSON decoder so this // enables the value returned to be compared directly to a decoded JSON - // response without spurios type loss. + // response without spurious type loss. "connect_timeout_ms": float64(1000), }, }, diff --git a/agent/submatview/local_materializer.go b/agent/submatview/local_materializer.go index 6e32b3602..b3d4480bd 100644 --- a/agent/submatview/local_materializer.go +++ b/agent/submatview/local_materializer.go @@ -66,6 +66,10 @@ func (m *LocalMaterializer) Run(ctx context.Context) { if ctx.Err() != nil { return } + if m.isTerminalError(err) { + return + } + m.mat.handleError(req, err) if err := m.mat.retryWaiter.Wait(ctx); err != nil { @@ -74,6 +78,14 @@ func (m *LocalMaterializer) Run(ctx context.Context) { } } +// isTerminalError determines whether the given error cannot be recovered from +// and should cause the materializer to halt and be evicted from the view store. +// +// This roughly matches the logic in agent/proxycfg-glue.newUpdateEvent. +func (m *LocalMaterializer) isTerminalError(err error) bool { + return acl.IsErrNotFound(err) +} + // subscribeOnce opens a new subscription to a local backend and runs // for its lifetime or until the view is closed. func (m *LocalMaterializer) subscribeOnce(ctx context.Context, req *pbsubscribe.SubscribeRequest) error { diff --git a/agent/submatview/store.go b/agent/submatview/store.go index 242a0d70d..dacf2d8ba 100644 --- a/agent/submatview/store.go +++ b/agent/submatview/store.go @@ -47,6 +47,9 @@ type entry struct { // requests is the count of active requests using this entry. This entry will // remain in the store as long as this count remains > 0. requests int + // evicting is used to mark an entry that will be evicted when the current in- + // flight requests finish. + evicting bool } // NewStore creates and returns a Store that is ready for use. The caller must @@ -89,6 +92,7 @@ func (s *Store) Run(ctx context.Context) { // Only stop the materializer if there are no active requests. if e.requests == 0 { + s.logger.Trace("evicting item from store", "key", he.Key()) e.stop() delete(s.byKey, he.Key()) } @@ -187,13 +191,13 @@ func (s *Store) NotifyCallback( "error", err, "request-type", req.Type(), "index", index) - continue } index = result.Index cb(ctx, cache.UpdateEvent{ CorrelationID: correlationID, Result: result.Value, + Err: err, Meta: cache.ResultMeta{Index: result.Index, Hit: result.Cached}, }) } @@ -211,6 +215,9 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { defer s.lock.Unlock() e, ok := s.byKey[key] if ok { + if e.evicting { + return "", nil, errors.New("item is marked for eviction") + } e.requests++ s.byKey[key] = e return key, e.materializer, nil @@ -222,7 +229,18 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { } ctx, cancel := context.WithCancel(context.Background()) - go mat.Run(ctx) + go func() { + mat.Run(ctx) + + // Materializers run until they either reach their TTL and are evicted (which + // cancels the given context) or encounter an irrecoverable error. + // + // If the context hasn't been canceled, we know it's the error case so we + // trigger an immediate eviction. + if ctx.Err() == nil { + s.evictNow(key) + } + }() e = entry{ materializer: mat, @@ -233,6 +251,28 @@ func (s *Store) readEntry(req Request) (string, Materializer, error) { return key, e.materializer, nil } +// evictNow causes the item with the given key to be evicted immediately. +// +// If there are requests in-flight, the item is marked for eviction such that +// once the requests have been served releaseEntry will move it to the top of +// the expiry heap. If there are no requests in-flight, evictNow will move the +// item to the top of the expiry heap itself. +// +// In either case, the entry's evicting flag prevents it from being served by +// readEntry (and thereby gaining new in-flight requests). +func (s *Store) evictNow(key string) { + s.lock.Lock() + defer s.lock.Unlock() + + e := s.byKey[key] + e.evicting = true + s.byKey[key] = e + + if e.requests == 0 { + s.expireNowLocked(key) + } +} + // releaseEntry decrements the request count and starts an expiry timer if the // count has reached 0. Must be called once for every call to readEntry. func (s *Store) releaseEntry(key string) { @@ -246,6 +286,11 @@ func (s *Store) releaseEntry(key string) { return } + if e.evicting { + s.expireNowLocked(key) + return + } + if e.expiry.Index() == ttlcache.NotIndexed { e.expiry = s.expiryHeap.Add(key, s.idleTTL) s.byKey[key] = e @@ -255,6 +300,17 @@ func (s *Store) releaseEntry(key string) { s.expiryHeap.Update(e.expiry.Index(), s.idleTTL) } +// expireNowLocked moves the item with the given key to the top of the expiry +// heap, causing it to be picked up by the expiry loop and evicted immediately. +func (s *Store) expireNowLocked(key string) { + e := s.byKey[key] + if idx := e.expiry.Index(); idx != ttlcache.NotIndexed { + s.expiryHeap.Remove(idx) + } + e.expiry = s.expiryHeap.Add(key, time.Duration(0)) + s.byKey[key] = e +} + // makeEntryKey matches agent/cache.makeEntryKey, but may change in the future. func makeEntryKey(typ string, r cache.RequestInfo) string { return fmt.Sprintf("%s/%s/%s/%s", typ, r.Datacenter, r.Token, r.Key) diff --git a/agent/submatview/store_test.go b/agent/submatview/store_test.go index 1d5789c05..aab099599 100644 --- a/agent/submatview/store_test.go +++ b/agent/submatview/store_test.go @@ -509,3 +509,75 @@ func TestStore_Run_ExpiresEntries(t *testing.T) { require.Len(t, store.byKey, 0) require.Equal(t, ttlcache.NotIndexed, e.expiry.Index()) } + +func TestStore_Run_FailingMaterializer(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + store := NewStore(hclog.NewNullLogger()) + store.idleTTL = 24 * time.Hour + go store.Run(ctx) + + t.Run("with an in-flight request", func(t *testing.T) { + req := &failingMaterializerRequest{ + doneCh: make(chan struct{}), + } + + ch := make(chan cache.UpdateEvent) + reqCtx, reqCancel := context.WithCancel(context.Background()) + t.Cleanup(reqCancel) + require.NoError(t, store.Notify(reqCtx, req, "", ch)) + + assertRequestCount(t, store, req, 1) + + // Cause the materializer to "fail" (exit before its context is canceled). + close(req.doneCh) + + // End the in-flight request. + reqCancel() + + // Check that the item was evicted. + retry.Run(t, func(r *retry.R) { + store.lock.Lock() + defer store.lock.Unlock() + + require.Len(r, store.byKey, 0) + }) + }) + + t.Run("with no in-flight requests", func(t *testing.T) { + req := &failingMaterializerRequest{ + doneCh: make(chan struct{}), + } + + // Cause the materializer to "fail" (exit before its context is canceled). + close(req.doneCh) + + // Check that the item was evicted. + retry.Run(t, func(r *retry.R) { + store.lock.Lock() + defer store.lock.Unlock() + + require.Len(r, store.byKey, 0) + }) + }) +} + +type failingMaterializerRequest struct { + doneCh chan struct{} +} + +func (failingMaterializerRequest) CacheInfo() cache.RequestInfo { return cache.RequestInfo{} } +func (failingMaterializerRequest) Type() string { return "test.FailingMaterializerRequest" } + +func (r *failingMaterializerRequest) NewMaterializer() (Materializer, error) { + return &failingMaterializer{doneCh: r.doneCh}, nil +} + +type failingMaterializer struct { + doneCh <-chan struct{} +} + +func (failingMaterializer) Query(context.Context, uint64) (Result, error) { return Result{}, nil } + +func (m *failingMaterializer) Run(context.Context) { <-m.doneCh } diff --git a/agent/testagent.go b/agent/testagent.go index 5701834b7..ea5afff81 100644 --- a/agent/testagent.go +++ b/agent/testagent.go @@ -66,9 +66,13 @@ type TestAgent struct { // and the directory will be removed once the test ends. DataDir string - // UseTLS, if true, will disable the HTTP port and enable the HTTPS + // UseHTTPS, if true, will disable the HTTP port and enable the HTTPS // one. - UseTLS bool + UseHTTPS bool + + // UseGRPCTLS, if true, will disable the GRPC port and enable the GRPC+TLS + // one. + UseGRPCTLS bool // dns is a reference to the first started DNS endpoint. // It is valid after Start(). @@ -183,7 +187,7 @@ func (a *TestAgent) Start(t *testing.T) error { Name: name, }) - portsConfig := randomPortsSource(t, a.UseTLS) + portsConfig := randomPortsSource(t, a.UseHTTPS, a.UseGRPCTLS) // Create NodeID outside the closure, so that it does not change testHCLConfig := TestConfigHCL(NodeID()) @@ -401,11 +405,11 @@ func (a *TestAgent) consulConfig() *consul.Config { // chance of port conflicts for concurrently executed test binaries. // Instead of relying on one set of ports to be sufficient we retry // starting the agent with different ports on port conflict. -func randomPortsSource(t *testing.T, tls bool) string { - ports := freeport.GetN(t, 7) +func randomPortsSource(t *testing.T, useHTTPS bool, useGRPCTLS bool) string { + ports := freeport.GetN(t, 8) var http, https int - if tls { + if useHTTPS { http = -1 https = ports[2] } else { @@ -413,6 +417,15 @@ func randomPortsSource(t *testing.T, tls bool) string { https = -1 } + var grpc, grpcTLS int + if useGRPCTLS { + grpc = -1 + grpcTLS = ports[7] + } else { + grpc = ports[6] + grpcTLS = -1 + } + return ` ports = { dns = ` + strconv.Itoa(ports[0]) + ` @@ -421,7 +434,8 @@ func randomPortsSource(t *testing.T, tls bool) string { serf_lan = ` + strconv.Itoa(ports[3]) + ` serf_wan = ` + strconv.Itoa(ports[4]) + ` server = ` + strconv.Itoa(ports[5]) + ` - grpc = ` + strconv.Itoa(ports[6]) + ` + grpc = ` + strconv.Itoa(grpc) + ` + grpc_tls = ` + strconv.Itoa(grpcTLS) + ` } ` } diff --git a/agent/txn_endpoint.go b/agent/txn_endpoint.go index 4e898bfce..f528bbda4 100644 --- a/agent/txn_endpoint.go +++ b/agent/txn_endpoint.go @@ -185,6 +185,7 @@ func (s *HTTPHandlers) convertOps(resp http.ResponseWriter, req *http.Request) ( Address: node.Address, Datacenter: node.Datacenter, TaggedAddresses: node.TaggedAddresses, + PeerName: node.PeerName, Meta: node.Meta, RaftIndex: structs.RaftIndex{ ModifyIndex: node.ModifyIndex, @@ -207,6 +208,7 @@ func (s *HTTPHandlers) convertOps(resp http.ResponseWriter, req *http.Request) ( Service: structs.NodeService{ ID: svc.ID, Service: svc.Service, + Kind: structs.ServiceKind(svc.Kind), Tags: svc.Tags, Address: svc.Address, Meta: svc.Meta, @@ -226,6 +228,39 @@ func (s *HTTPHandlers) convertOps(resp http.ResponseWriter, req *http.Request) ( }, }, } + + if svc.Proxy != nil { + out.Service.Service.Proxy = structs.ConnectProxyConfig{} + t := &out.Service.Service.Proxy + if svc.Proxy.DestinationServiceName != "" { + t.DestinationServiceName = svc.Proxy.DestinationServiceName + } + if svc.Proxy.DestinationServiceID != "" { + t.DestinationServiceID = svc.Proxy.DestinationServiceID + } + if svc.Proxy.LocalServiceAddress != "" { + t.LocalServiceAddress = svc.Proxy.LocalServiceAddress + } + if svc.Proxy.LocalServicePort != 0 { + t.LocalServicePort = svc.Proxy.LocalServicePort + } + if svc.Proxy.LocalServiceSocketPath != "" { + t.LocalServiceSocketPath = svc.Proxy.LocalServiceSocketPath + } + if svc.Proxy.MeshGateway.Mode != "" { + t.MeshGateway.Mode = structs.MeshGatewayMode(svc.Proxy.MeshGateway.Mode) + } + + if svc.Proxy.TransparentProxy != nil { + if svc.Proxy.TransparentProxy.DialedDirectly { + t.TransparentProxy.DialedDirectly = svc.Proxy.TransparentProxy.DialedDirectly + } + + if svc.Proxy.TransparentProxy.OutboundListenerPort != 0 { + t.TransparentProxy.OutboundListenerPort = svc.Proxy.TransparentProxy.OutboundListenerPort + } + } + } opsRPC = append(opsRPC, out) case in.Check != nil: @@ -265,6 +300,8 @@ func (s *HTTPHandlers) convertOps(resp http.ResponseWriter, req *http.Request) ( ServiceID: check.ServiceID, ServiceName: check.ServiceName, ServiceTags: check.ServiceTags, + PeerName: check.PeerName, + ExposedPort: check.ExposedPort, Definition: structs.HealthCheckDefinition{ HTTP: check.Definition.HTTP, TLSServerName: check.Definition.TLSServerName, diff --git a/agent/txn_endpoint_test.go b/agent/txn_endpoint_test.go index 4b529d5de..f6f47b8fa 100644 --- a/agent/txn_endpoint_test.go +++ b/agent/txn_endpoint_test.go @@ -585,6 +585,7 @@ func TestTxnEndpoint_UpdateCheck(t *testing.T) { "Output": "success", "ServiceID": "", "ServiceName": "", + "ExposedPort": 5678, "Definition": { "IntervalDuration": "15s", "TimeoutDuration": "15s", @@ -600,12 +601,8 @@ func TestTxnEndpoint_UpdateCheck(t *testing.T) { req, _ := http.NewRequest("PUT", "/v1/txn", buf) resp := httptest.NewRecorder() obj, err := a.srv.Txn(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - if resp.Code != 200 { - t.Fatalf("expected 200, got %d", resp.Code) - } + require.NoError(t, err) + require.Equal(t, 200, resp.Code, resp.Body) txnResp, ok := obj.(structs.TxnResponse) if !ok { @@ -662,12 +659,13 @@ func TestTxnEndpoint_UpdateCheck(t *testing.T) { }, &structs.TxnResult{ Check: &structs.HealthCheck{ - Node: a.config.NodeName, - CheckID: "nodecheck", - Name: "Node http check", - Status: api.HealthPassing, - Notes: "Http based health check", - Output: "success", + Node: a.config.NodeName, + CheckID: "nodecheck", + Name: "Node http check", + Status: api.HealthPassing, + Notes: "Http based health check", + Output: "success", + ExposedPort: 5678, Definition: structs.HealthCheckDefinition{ Interval: 15 * time.Second, Timeout: 15 * time.Second, @@ -686,3 +684,117 @@ func TestTxnEndpoint_UpdateCheck(t *testing.T) { } assert.Equal(t, expected, txnResp) } + +func TestTxnEndpoint_NodeService(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := NewTestAgent(t, "") + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + // Make sure the fields of a check are handled correctly when both creating and + // updating, and test both sets of duration fields to ensure backwards compatibility. + buf := bytes.NewBuffer([]byte(fmt.Sprintf(` +[ + { + "Service": { + "Verb": "set", + "Node": "%s", + "Service": { + "Service": "test", + "Port": 4444 + } + } + }, + { + "Service": { + "Verb": "set", + "Node": "%s", + "Service": { + "Service": "test-sidecar-proxy", + "Port": 20000, + "Kind": "connect-proxy", + "Proxy": { + "DestinationServiceName": "test", + "DestinationServiceID": "test", + "LocalServiceAddress": "127.0.0.1", + "LocalServicePort": 4444, + "upstreams": [ + { + "DestinationName": "fake-backend", + "LocalBindPort": 25001 + } + ] + } + } + } + } +] +`, a.config.NodeName, a.config.NodeName))) + req, _ := http.NewRequest("PUT", "/v1/txn", buf) + resp := httptest.NewRecorder() + obj, err := a.srv.Txn(resp, req) + require.NoError(t, err) + require.Equal(t, 200, resp.Code) + + txnResp, ok := obj.(structs.TxnResponse) + if !ok { + t.Fatalf("bad type: %T", obj) + } + require.Equal(t, 2, len(txnResp.Results)) + + index := txnResp.Results[0].Service.ModifyIndex + expected := structs.TxnResponse{ + Results: structs.TxnResults{ + &structs.TxnResult{ + Service: &structs.NodeService{ + Service: "test", + ID: "test", + Port: 4444, + Weights: &structs.Weights{ + Passing: 1, + Warning: 1, + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: index, + ModifyIndex: index, + }, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + }, + &structs.TxnResult{ + Service: &structs.NodeService{ + Service: "test-sidecar-proxy", + ID: "test-sidecar-proxy", + Port: 20000, + Kind: "connect-proxy", + Weights: &structs.Weights{ + Passing: 1, + Warning: 1, + }, + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "test", + DestinationServiceID: "test", + LocalServiceAddress: "127.0.0.1", + LocalServicePort: 4444, + }, + TaggedAddresses: map[string]structs.ServiceAddress{ + "consul-virtual": { + Address: "240.0.0.1", + Port: 20000, + }, + }, + RaftIndex: structs.RaftIndex{ + CreateIndex: index, + ModifyIndex: index, + }, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + }, + }, + } + assert.Equal(t, expected, txnResp) +} diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index 2f74d8e59..a418a4017 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -211,7 +211,9 @@ func (s *HTTPHandlers) UIServices(resp http.ResponseWriter, req *http.Request) ( if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil } - + if peer := req.URL.Query().Get("peer"); peer != "" { + args.PeerName = peer + } if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil { return nil, err } @@ -769,6 +771,7 @@ func (s *HTTPHandlers) UIMetricsProxy(resp http.ResponseWriter, req *http.Reques Director: func(r *http.Request) { r.URL = u }, + Transport: s.proxyTransport, ErrorLog: log.StandardLogger(&hclog.StandardLoggerOptions{ InferLevels: true, }), diff --git a/agent/xds/clusters.go b/agent/xds/clusters.go index 6f16b9bee..a425f829e 100644 --- a/agent/xds/clusters.go +++ b/agent/xds/clusters.go @@ -10,6 +10,7 @@ import ( envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + envoy_aggregate_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/clusters/aggregate/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" envoy_upstreams_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" @@ -31,6 +32,7 @@ import ( const ( meshGatewayExportedClusterNamePrefix = "exported~" + failoverClusterNamePrefix = "failover-target~" ) // clustersFromSnapshot returns the xDS API representation of the "clusters" in the snapshot. @@ -86,29 +88,26 @@ func (s *ResourceGenerator) clustersFromSnapshotConnectProxy(cfgSnap *proxycfg.C clusters = append(clusters, passthroughs...) } - // NOTE: Any time we skip a chain below we MUST also skip that discovery chain in endpoints.go - // so that the sets of endpoints generated matches the sets of clusters. - for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { + getUpstream := func(uid proxycfg.UpstreamID) (*structs.Upstream, bool) { upstream := cfgSnap.ConnectProxy.UpstreamConfig[uid] explicit := upstream.HasLocalPortOrSocket() implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { - // Discovery chain is not associated with a known explicit or implicit upstream so it is skipped. - continue - } + return upstream, !implicit && !explicit + } - chainEndpoints, ok := cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[uid] - if !ok { - // this should not happen - return nil, fmt.Errorf("no endpoint map for upstream %q", uid) + // NOTE: Any time we skip a chain below we MUST also skip that discovery chain in endpoints.go + // so that the sets of endpoints generated matches the sets of clusters. + for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { + upstream, skip := getUpstream(uid) + if skip { + continue } upstreamClusters, err := s.makeUpstreamClustersForDiscoveryChain( uid, upstream, chain, - chainEndpoints, cfgSnap, false, ) @@ -125,18 +124,15 @@ func (s *ResourceGenerator) clustersFromSnapshotConnectProxy(cfgSnap *proxycfg.C // upstream in endpoints.go so that the sets of endpoints generated matches // the sets of clusters. for _, uid := range cfgSnap.ConnectProxy.PeeredUpstreamIDs() { - upstreamCfg := cfgSnap.ConnectProxy.UpstreamConfig[uid] - - explicit := upstreamCfg.HasLocalPortOrSocket() - implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { - // Not associated with a known explicit or implicit upstream so it is skipped. + upstream, skip := getUpstream(uid) + if skip { continue } peerMeta := cfgSnap.ConnectProxy.UpstreamPeerMeta(uid) + cfg := s.getAndModifyUpstreamConfigForPeeredListener(uid, upstream, peerMeta) - upstreamCluster, err := s.makeUpstreamClusterForPeerService(uid, upstreamCfg, peerMeta, cfgSnap) + upstreamCluster, err := s.makeUpstreamClusterForPeerService(uid, cfg, peerMeta, cfgSnap) if err != nil { return nil, err } @@ -650,17 +646,10 @@ func (s *ResourceGenerator) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg return nil, fmt.Errorf("no discovery chain for upstream %q", uid) } - chainEndpoints, ok := cfgSnap.IngressGateway.WatchedUpstreamEndpoints[uid] - if !ok { - // this should not happen - return nil, fmt.Errorf("no endpoint map for upstream %q", uid) - } - upstreamClusters, err := s.makeUpstreamClustersForDiscoveryChain( uid, &u, chain, - chainEndpoints, cfgSnap, false, ) @@ -743,7 +732,7 @@ func (s *ResourceGenerator) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, nam func (s *ResourceGenerator) makeUpstreamClusterForPeerService( uid proxycfg.UpstreamID, - upstream *structs.Upstream, + upstreamConfig structs.UpstreamConfig, peerMeta structs.PeeringServiceMeta, cfgSnap *proxycfg.ConfigSnapshot, ) (*envoy_cluster_v3.Cluster, error) { @@ -752,16 +741,21 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService( err error ) - cfg := s.getAndModifyUpstreamConfigForPeeredListener(uid, upstream, peerMeta) - if cfg.EnvoyClusterJSON != "" { - c, err = makeClusterFromUserConfig(cfg.EnvoyClusterJSON) + if upstreamConfig.EnvoyClusterJSON != "" { + c, err = makeClusterFromUserConfig(upstreamConfig.EnvoyClusterJSON) if err != nil { return c, err } // In the happy path don't return yet as we need to inject TLS config still. } - tbs, ok := cfgSnap.ConnectProxy.UpstreamPeerTrustBundles.Get(uid.Peer) + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + + if err != nil { + return c, err + } + + tbs, ok := upstreamsSnapshot.UpstreamPeerTrustBundles.Get(uid.Peer) if !ok { // this should never happen since we loop through upstreams with // set trust bundles @@ -770,22 +764,29 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService( clusterName := generatePeeredClusterName(uid, tbs) + outlierDetection := ToOutlierDetection(upstreamConfig.PassiveHealthCheck) + // We can't rely on health checks for services on cluster peers because they + // don't take into account service resolvers, splitters and routers. Setting + // MaxEjectionPercent too 100% gives outlier detection the power to eject the + // entire cluster. + outlierDetection.MaxEjectionPercent = &wrappers.UInt32Value{Value: 100} + s.Logger.Trace("generating cluster for", "cluster", clusterName) if c == nil { c = &envoy_cluster_v3.Cluster{ Name: clusterName, - ConnectTimeout: durationpb.New(time.Duration(cfg.ConnectTimeoutMs) * time.Millisecond), + ConnectTimeout: durationpb.New(time.Duration(upstreamConfig.ConnectTimeoutMs) * time.Millisecond), CommonLbConfig: &envoy_cluster_v3.Cluster_CommonLbConfig{ HealthyPanicThreshold: &envoy_type_v3.Percent{ Value: 0, // disable panic threshold }, }, CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ - Thresholds: makeThresholdsIfNeeded(cfg.Limits), + Thresholds: makeThresholdsIfNeeded(upstreamConfig.Limits), }, - OutlierDetection: ToOutlierDetection(cfg.PassiveHealthCheck), + OutlierDetection: outlierDetection, } - if cfg.Protocol == "http2" || cfg.Protocol == "grpc" { + if upstreamConfig.Protocol == "http2" || upstreamConfig.Protocol == "grpc" { if err := s.setHttp2ProtocolOptions(c); err != nil { return c, err } @@ -819,12 +820,11 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService( false, /*onlyPassing*/ ) } - } rootPEMs := cfgSnap.RootPEMs() if uid.Peer != "" { - tbs, _ := cfgSnap.ConnectProxy.UpstreamPeerTrustBundles.Get(uid.Peer) + tbs, _ := upstreamsSnapshot.UpstreamPeerTrustBundles.Get(uid.Peer) rootPEMs = tbs.ConcatenatedRootPEMs() } @@ -959,7 +959,6 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain( uid proxycfg.UpstreamID, upstream *structs.Upstream, chain *structs.CompiledDiscoveryChain, - chainEndpoints map[string]structs.CheckServiceNodes, cfgSnap *proxycfg.ConfigSnapshot, forMeshGateway bool, ) ([]*envoy_cluster_v3.Cluster, error) { @@ -976,7 +975,15 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain( upstreamConfigMap = upstream.Config } - cfg, err := structs.ParseUpstreamConfigNoDefaults(upstreamConfigMap) + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + + // Mesh gateways are exempt because upstreamsSnapshot is only used for + // cluster peering targets and transative failover/redirects are unsupported. + if err != nil && !forMeshGateway { + return nil, err + } + + rawUpstreamConfig, err := structs.ParseUpstreamConfigNoDefaults(upstreamConfigMap) if err != nil { // Don't hard fail on a config typo, just warn. The parse func returns // default config if there is an error so it's safe to continue. @@ -984,13 +991,28 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain( "error", err) } + finalizeUpstreamConfig := func(cfg structs.UpstreamConfig, connectTimeout time.Duration) structs.UpstreamConfig { + if cfg.Protocol == "" { + cfg.Protocol = chain.Protocol + } + + if cfg.Protocol == "" { + cfg.Protocol = "tcp" + } + + if cfg.ConnectTimeoutMs == 0 { + cfg.ConnectTimeoutMs = int(connectTimeout / time.Millisecond) + } + return cfg + } + var escapeHatchCluster *envoy_cluster_v3.Cluster if !forMeshGateway { - if cfg.EnvoyClusterJSON != "" { + if rawUpstreamConfig.EnvoyClusterJSON != "" { if chain.Default { // If you haven't done anything to setup the discovery chain, then // you can use the envoy_cluster_json escape hatch. - escapeHatchCluster, err = makeClusterFromUserConfig(cfg.EnvoyClusterJSON) + escapeHatchCluster, err = makeClusterFromUserConfig(rawUpstreamConfig.EnvoyClusterJSON) if err != nil { return nil, err } @@ -1004,184 +1026,168 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain( var out []*envoy_cluster_v3.Cluster for _, node := range chain.Nodes { - if node.Type != structs.DiscoveryGraphNodeTypeResolver { + switch { + case node == nil: + return nil, fmt.Errorf("impossible to process a nil node") + case node.Type != structs.DiscoveryGraphNodeTypeResolver: continue + case node.Resolver == nil: + return nil, fmt.Errorf("impossible to process a non-resolver node") } failover := node.Resolver.Failover - targetID := node.Resolver.Target + // These variables are prefixed with primary to avoid shaddowing bugs. + primaryTargetID := node.Resolver.Target + primaryTarget := chain.Targets[primaryTargetID] + primaryTargetClusterData, ok := s.getTargetClusterData(upstreamsSnapshot, chain, primaryTargetID, forMeshGateway, false) + if !ok { + continue + } + upstreamConfig := finalizeUpstreamConfig(rawUpstreamConfig, node.Resolver.ConnectTimeout) - target := chain.Targets[targetID] - - if forMeshGateway && !cfgSnap.Locality.Matches(target.Datacenter, target.Partition) { + if forMeshGateway && !cfgSnap.Locality.Matches(primaryTarget.Datacenter, primaryTarget.Partition) { s.Logger.Warn("ignoring discovery chain target that crosses a datacenter or partition boundary in a mesh gateway", - "target", target, + "target", primaryTarget, "gatewayLocality", cfgSnap.Locality, ) continue } - // Determine if we have to generate the entire cluster differently. - failoverThroughMeshGateway := chain.WillFailoverThroughMeshGateway(node) && !forMeshGateway + // Construct the information required to make target clusters. When + // failover is configured, create the aggregate cluster. + var targetClustersData []targetClusterData + if failover != nil && !forMeshGateway { + var failoverClusterNames []string + for _, tid := range append([]string{primaryTargetID}, failover.Targets...) { + if td, ok := s.getTargetClusterData(upstreamsSnapshot, chain, tid, forMeshGateway, true); ok { + targetClustersData = append(targetClustersData, td) + failoverClusterNames = append(failoverClusterNames, td.clusterName) + } + } - sni := target.SNI - clusterName := CustomizeClusterName(target.Name, chain) - if forMeshGateway { - clusterName = meshGatewayExportedClusterNamePrefix + clusterName + aggregateClusterConfig, err := anypb.New(&envoy_aggregate_cluster_v3.ClusterConfig{ + Clusters: failoverClusterNames, + }) + if err != nil { + return nil, fmt.Errorf("failed to construct the aggregate cluster %q: %v", primaryTargetClusterData.clusterName, err) + } + + c := &envoy_cluster_v3.Cluster{ + Name: primaryTargetClusterData.clusterName, + AltStatName: primaryTargetClusterData.clusterName, + ConnectTimeout: durationpb.New(node.Resolver.ConnectTimeout), + LbPolicy: envoy_cluster_v3.Cluster_CLUSTER_PROVIDED, + ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{ + ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{ + Name: "envoy.clusters.aggregate", + TypedConfig: aggregateClusterConfig, + }, + }, + } + + out = append(out, c) + } else { + targetClustersData = append(targetClustersData, primaryTargetClusterData) } - // Get the SpiffeID for upstream SAN validation. - // - // For imported services the SpiffeID is embedded in the proxy instances. - // Whereas for local services we can construct the SpiffeID from the chain target. - var targetSpiffeID string - var additionalSpiffeIDs []string - if uid.Peer != "" { - for _, e := range chainEndpoints[targetID] { - targetSpiffeID = e.Service.Connect.PeerMeta.SpiffeID[0] - additionalSpiffeIDs = e.Service.Connect.PeerMeta.SpiffeID[1:] + // Construct the target clusters. + for _, targetData := range targetClustersData { + target := chain.Targets[targetData.targetID] + sni := target.SNI + var additionalSpiffeIDs []string - // Only grab the first instance because it is the same for all instances. - break - } - } else { - targetSpiffeID = connect.SpiffeIDService{ + targetSpiffeID := connect.SpiffeIDService{ Host: cfgSnap.Roots.TrustDomain, Namespace: target.Namespace, Partition: target.Partition, Datacenter: target.Datacenter, Service: target.Service, }.URI().String() - } - - if failoverThroughMeshGateway { - actualTargetID := firstHealthyTarget( - chain.Targets, - chainEndpoints, - targetID, - failover.Targets, - ) - - if actualTargetID != targetID { - actualTarget := chain.Targets[actualTargetID] - sni = actualTarget.SNI - } - } - - spiffeIDs := append([]string{targetSpiffeID}, additionalSpiffeIDs...) - seenIDs := map[string]struct{}{ - targetSpiffeID: {}, - } - - if failover != nil { - // When failovers are present we need to add them as valid SANs to validate against. - // Envoy makes the failover decision independently based on the endpoint health it has available. - for _, tid := range failover.Targets { - target, ok := chain.Targets[tid] - if !ok { + targetUID := proxycfg.NewUpstreamIDFromTargetID(targetData.targetID) + if targetUID.Peer != "" { + peerMeta := upstreamsSnapshot.UpstreamPeerMeta(targetUID) + upstreamCluster, err := s.makeUpstreamClusterForPeerService(targetUID, upstreamConfig, peerMeta, cfgSnap) + if err != nil { continue } - - id := connect.SpiffeIDService{ - Host: cfgSnap.Roots.TrustDomain, - Namespace: target.Namespace, - Partition: target.Partition, - Datacenter: target.Datacenter, - Service: target.Service, - }.URI().String() - - // Failover targets might be subsets of the same service, so these are deduplicated. - if _, ok := seenIDs[id]; ok { - continue - } - seenIDs[id] = struct{}{} - - spiffeIDs = append(spiffeIDs, id) + // Override the cluster name to include the failover-target~ prefix. + upstreamCluster.Name = targetData.clusterName + out = append(out, upstreamCluster) + continue } - } - sort.Strings(spiffeIDs) - s.Logger.Trace("generating cluster for", "cluster", clusterName) - c := &envoy_cluster_v3.Cluster{ - Name: clusterName, - AltStatName: clusterName, - ConnectTimeout: durationpb.New(node.Resolver.ConnectTimeout), - ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_EDS}, - CommonLbConfig: &envoy_cluster_v3.Cluster_CommonLbConfig{ - HealthyPanicThreshold: &envoy_type_v3.Percent{ - Value: 0, // disable panic threshold - }, - }, - EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ - EdsConfig: &envoy_core_v3.ConfigSource{ - ResourceApiVersion: envoy_core_v3.ApiVersion_V3, - ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_Ads{ - Ads: &envoy_core_v3.AggregatedConfigSource{}, + s.Logger.Debug("generating cluster for", "cluster", targetData.clusterName) + c := &envoy_cluster_v3.Cluster{ + Name: targetData.clusterName, + AltStatName: targetData.clusterName, + ConnectTimeout: durationpb.New(node.Resolver.ConnectTimeout), + ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{Type: envoy_cluster_v3.Cluster_EDS}, + CommonLbConfig: &envoy_cluster_v3.Cluster_CommonLbConfig{ + HealthyPanicThreshold: &envoy_type_v3.Percent{ + Value: 0, // disable panic threshold }, }, - }, - // TODO(peering): make circuit breakers or outlier detection work? - CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ - Thresholds: makeThresholdsIfNeeded(cfg.Limits), - }, - OutlierDetection: ToOutlierDetection(cfg.PassiveHealthCheck), - } - - var lb *structs.LoadBalancer - if node.LoadBalancer != nil { - lb = node.LoadBalancer - } - if err := injectLBToCluster(lb, c); err != nil { - return nil, fmt.Errorf("failed to apply load balancer configuration to cluster %q: %v", clusterName, err) - } - - var proto string - if !forMeshGateway { - proto = cfg.Protocol - } - if proto == "" { - proto = chain.Protocol - } - - if proto == "" { - proto = "tcp" - } - - if proto == "http2" || proto == "grpc" { - if err := s.setHttp2ProtocolOptions(c); err != nil { - return nil, err - } - } - - configureTLS := true - if forMeshGateway { - // We only initiate TLS if we're doing an L7 proxy. - configureTLS = structs.IsProtocolHTTPLike(proto) - } - - if configureTLS { - commonTLSContext := makeCommonTLSContext( - cfgSnap.Leaf(), - cfgSnap.RootPEMs(), - makeTLSParametersFromProxyTLSConfig(cfgSnap.MeshConfigTLSOutgoing()), - ) - - err = injectSANMatcher(commonTLSContext, spiffeIDs...) - if err != nil { - return nil, fmt.Errorf("failed to inject SAN matcher rules for cluster %q: %v", sni, err) + EdsClusterConfig: &envoy_cluster_v3.Cluster_EdsClusterConfig{ + EdsConfig: &envoy_core_v3.ConfigSource{ + ResourceApiVersion: envoy_core_v3.ApiVersion_V3, + ConfigSourceSpecifier: &envoy_core_v3.ConfigSource_Ads{ + Ads: &envoy_core_v3.AggregatedConfigSource{}, + }, + }, + }, + // TODO(peering): make circuit breakers or outlier detection work? + CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{ + Thresholds: makeThresholdsIfNeeded(upstreamConfig.Limits), + }, + OutlierDetection: ToOutlierDetection(upstreamConfig.PassiveHealthCheck), } - tlsContext := &envoy_tls_v3.UpstreamTlsContext{ - CommonTlsContext: commonTLSContext, - Sni: sni, + var lb *structs.LoadBalancer + if node.LoadBalancer != nil { + lb = node.LoadBalancer } - transportSocket, err := makeUpstreamTLSTransportSocket(tlsContext) - if err != nil { - return nil, err + if err := injectLBToCluster(lb, c); err != nil { + return nil, fmt.Errorf("failed to apply load balancer configuration to cluster %q: %v", targetData.clusterName, err) } - c.TransportSocket = transportSocket - } - out = append(out, c) + if upstreamConfig.Protocol == "http2" || upstreamConfig.Protocol == "grpc" { + if err := s.setHttp2ProtocolOptions(c); err != nil { + return nil, err + } + } + + configureTLS := true + if forMeshGateway { + // We only initiate TLS if we're doing an L7 proxy. + configureTLS = structs.IsProtocolHTTPLike(upstreamConfig.Protocol) + } + + if configureTLS { + commonTLSContext := makeCommonTLSContext( + cfgSnap.Leaf(), + cfgSnap.RootPEMs(), + makeTLSParametersFromProxyTLSConfig(cfgSnap.MeshConfigTLSOutgoing()), + ) + + spiffeIDs := append([]string{targetSpiffeID}, additionalSpiffeIDs...) + sort.Strings(spiffeIDs) + err = injectSANMatcher(commonTLSContext, spiffeIDs...) + if err != nil { + return nil, fmt.Errorf("failed to inject SAN matcher rules for cluster %q: %v", sni, err) + } + + tlsContext := &envoy_tls_v3.UpstreamTlsContext{ + CommonTlsContext: commonTLSContext, + Sni: sni, + } + transportSocket, err := makeUpstreamTLSTransportSocket(tlsContext) + if err != nil { + return nil, err + } + c.TransportSocket = transportSocket + } + + out = append(out, c) + } } if escapeHatchCluster != nil { @@ -1225,7 +1231,6 @@ func (s *ResourceGenerator) makeExportedUpstreamClustersForMeshGateway(cfgSnap * proxycfg.NewUpstreamIDFromServiceName(svc), nil, chain, - nil, cfgSnap, true, ) @@ -1641,3 +1646,39 @@ func generatePeeredClusterName(uid proxycfg.UpstreamID, tb *pbpeering.PeeringTru tb.TrustDomain, }, ".") } + +type targetClusterData struct { + targetID string + clusterName string +} + +func (s *ResourceGenerator) getTargetClusterData(upstreamsSnapshot *proxycfg.ConfigSnapshotUpstreams, chain *structs.CompiledDiscoveryChain, tid string, forMeshGateway bool, failover bool) (targetClusterData, bool) { + target := chain.Targets[tid] + clusterName := target.Name + targetUID := proxycfg.NewUpstreamIDFromTargetID(tid) + if targetUID.Peer != "" { + tbs, ok := upstreamsSnapshot.UpstreamPeerTrustBundles.Get(targetUID.Peer) + // We can't generate cluster on peers without the trust bundle. The + // trust bundle should be ready soon. + if !ok { + s.Logger.Debug("peer trust bundle not ready for discovery chain target", + "peer", targetUID.Peer, + "target", tid, + ) + return targetClusterData{}, false + } + + clusterName = generatePeeredClusterName(targetUID, tbs) + } + clusterName = CustomizeClusterName(clusterName, chain) + if failover { + clusterName = failoverClusterNamePrefix + clusterName + } + if forMeshGateway { + clusterName = meshGatewayExportedClusterNamePrefix + clusterName + } + return targetClusterData{ + targetID: tid, + clusterName: clusterName, + }, true +} diff --git a/agent/xds/clusters_test.go b/agent/xds/clusters_test.go index a56853b81..de0fefcd7 100644 --- a/agent/xds/clusters_test.go +++ b/agent/xds/clusters_test.go @@ -169,6 +169,18 @@ func TestClustersFromSnapshot(t *testing.T) { }, nil) }, }, + { + name: "custom-passive-healthcheck", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { + ns.Proxy.Upstreams[0].Config["passive_health_check"] = map[string]interface{}{ + "enforcing_consecutive_5xx": float64(80), + "max_failures": float64(5), + "interval": float64(10), + } + }, nil) + }, + }, { name: "custom-max-inbound-connections", create: func(t testinf.T) *proxycfg.ConfigSnapshot { @@ -495,6 +507,13 @@ func TestClustersFromSnapshot(t *testing.T) { "failover", nil, nil, nil) }, }, + { + name: "ingress-with-chain-and-failover-to-cluster-peer", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotIngressGateway(t, true, "tcp", + "failover-to-cluster-peer", nil, nil, nil) + }, + }, { name: "ingress-with-tcp-chain-failover-through-remote-gateway", create: func(t testinf.T) *proxycfg.ConfigSnapshot { diff --git a/agent/xds/config.go b/agent/xds/config.go index 2fdf9d115..0736fb44c 100644 --- a/agent/xds/config.go +++ b/agent/xds/config.go @@ -27,6 +27,12 @@ type ProxyConfig struct { // Note: This escape hatch is compatible with the discovery chain. PublicListenerJSON string `mapstructure:"envoy_public_listener_json"` + // ListenerTracingJSON is a complete override ("escape hatch") for the + // listeners tracing configuration. + // + // Note: This escape hatch is compatible with the discovery chain. + ListenerTracingJSON string `mapstructure:"envoy_listener_tracing_json"` + // LocalClusterJSON is a complete override ("escape hatch") for the // local application cluster. // @@ -168,5 +174,10 @@ func ToOutlierDetection(p *structs.PassiveHealthCheck) *envoy_cluster_v3.Outlier if p.MaxFailures != 0 { od.Consecutive_5Xx = &wrappers.UInt32Value{Value: p.MaxFailures} } + + if p.EnforcingConsecutive5xx != nil { + od.EnforcingConsecutive_5Xx = &wrappers.UInt32Value{Value: *p.EnforcingConsecutive5xx} + } + return od } diff --git a/agent/xds/delta.go b/agent/xds/delta.go index 701c04f2e..aa038214c 100644 --- a/agent/xds/delta.go +++ b/agent/xds/delta.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + "github.com/armon/go-metrics" envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" @@ -29,6 +30,8 @@ import ( "github.com/hashicorp/consul/logging" ) +var errOverwhelmed = status.Error(codes.ResourceExhausted, "this server has too many xDS streams open, please try another") + type deltaRecvResponse int const ( @@ -81,6 +84,17 @@ const ( ) func (s *Server) processDelta(stream ADSDeltaStream, reqCh <-chan *envoy_discovery_v3.DeltaDiscoveryRequest) error { + // Handle invalid ACL tokens up-front. + if _, err := s.authenticate(stream.Context()); err != nil { + return err + } + + session, err := s.SessionLimiter.BeginSession() + if err != nil { + return errOverwhelmed + } + defer session.End() + // Loop state var ( cfgSnap *proxycfg.ConfigSnapshot @@ -154,6 +168,10 @@ func (s *Server) processDelta(stream ADSDeltaStream, reqCh <-chan *envoy_discove for { select { + case <-session.Terminated(): + generator.Logger.Debug("draining stream to rebalance load") + metrics.IncrCounter([]string{"xds", "server", "streamDrained"}, 1) + return errOverwhelmed case <-authTimer: // It's been too long since a Discovery{Request,Response} so recheck ACLs. if err := checkStreamACLs(cfgSnap); err != nil { @@ -200,7 +218,18 @@ func (s *Server) processDelta(stream ADSDeltaStream, reqCh <-chan *envoy_discove } } - case cfgSnap = <-stateCh: + case cs, ok := <-stateCh: + if !ok { + // stateCh is closed either when *we* cancel the watch (on-exit via defer) + // or by the proxycfg.Manager when an irrecoverable error is encountered + // such as the ACL token getting deleted. + // + // We know for sure that this is the latter case, because in the former we + // would've already exited this loop. + return status.Error(codes.Aborted, "xDS stream terminated due to an irrecoverable error, please try again") + } + cfgSnap = cs + newRes, err := generator.allResourcesFromSnapshot(cfgSnap) if err != nil { return status.Errorf(codes.Unavailable, "failed to generate all xDS resources from the snapshot: %v", err) diff --git a/agent/xds/delta_test.go b/agent/xds/delta_test.go index 21ec701dc..c2706c768 100644 --- a/agent/xds/delta_test.go +++ b/agent/xds/delta_test.go @@ -17,6 +17,7 @@ import ( "google.golang.org/grpc/status" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/grpc-external/limiter" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/xds/xdscommon" @@ -36,7 +37,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP(t *testing.T) { // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, serverlessPluginEnabled) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, serverlessPluginEnabled, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -238,7 +239,7 @@ func TestServer_DeltaAggregatedResources_v3_NackLoop(t *testing.T) { // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -369,7 +370,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2(t *testing.T) { // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -522,7 +523,7 @@ func TestServer_DeltaAggregatedResources_v3_SlowEndpointPopulation(t *testing.T) // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, nil) server, mgr, errCh, envoy := scenario.server, scenario.mgr, scenario.errCh, scenario.envoy // This mutateFn causes any endpoint with a name containing "geo-cache" to be @@ -663,7 +664,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP_clusterChangesImpa // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -799,7 +800,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2_RDS_listenerChan // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -1059,7 +1060,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) { return acl.NewPolicyAuthorizerWithDefaults(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil) } - scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", tt.token, 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", tt.token, 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("web-sidecar-proxy", nil) @@ -1137,6 +1138,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedDuri scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", token, 100*time.Millisecond, // Make this short. false, + nil, ) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy @@ -1236,6 +1238,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedInBa scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", token, 100*time.Millisecond, // Make this short. false, + nil, ) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy @@ -1316,7 +1319,7 @@ func TestServer_DeltaAggregatedResources_v3_IngressEmptyResponse(t *testing.T) { // Allow all return acl.RootAuthorizer("manage"), nil } - scenario := newTestServerDeltaScenario(t, aclResolve, "ingress-gateway", "", 0, false) + scenario := newTestServerDeltaScenario(t, aclResolve, "ingress-gateway", "", 0, false, nil) mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy sid := structs.NewServiceID("ingress-gateway", nil) @@ -1368,6 +1371,115 @@ func TestServer_DeltaAggregatedResources_v3_IngressEmptyResponse(t *testing.T) { } } +func TestServer_DeltaAggregatedResources_v3_CapacityReached(t *testing.T) { + aclResolve := func(id string) (acl.Authorizer, error) { return acl.ManageAll(), nil } + + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, capacityReachedLimiter{}) + mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy + + sid := structs.NewServiceID("web-sidecar-proxy", nil) + + mgr.RegisterProxy(t, sid) + + snap := newTestSnapshot(t, nil, "") + + envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{ + InitialResourceVersions: mustMakeVersionMap(t, + makeTestCluster(t, snap, "tcp:geo-cache"), + ), + }) + + select { + case err := <-errCh: + require.Error(t, err) + require.Equal(t, codes.ResourceExhausted.String(), status.Code(err).String()) + case <-time.After(50 * time.Millisecond): + t.Fatalf("timed out waiting for handler to finish") + } +} + +type capacityReachedLimiter struct{} + +func (capacityReachedLimiter) BeginSession() (limiter.Session, error) { + return nil, limiter.ErrCapacityReached +} + +func TestServer_DeltaAggregatedResources_v3_StreamDrained(t *testing.T) { + limiter := &testLimiter{} + + aclResolve := func(id string) (acl.Authorizer, error) { return acl.ManageAll(), nil } + scenario := newTestServerDeltaScenario(t, aclResolve, "web-sidecar-proxy", "", 0, false, limiter) + mgr, errCh, envoy := scenario.mgr, scenario.errCh, scenario.envoy + + sid := structs.NewServiceID("web-sidecar-proxy", nil) + + mgr.RegisterProxy(t, sid) + + testutil.RunStep(t, "successful request/response", func(t *testing.T) { + snap := newTestSnapshot(t, nil, "") + + envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{ + InitialResourceVersions: mustMakeVersionMap(t, + makeTestCluster(t, snap, "tcp:geo-cache"), + ), + }) + + mgr.DeliverConfig(t, sid, snap) + + assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ + TypeUrl: xdscommon.ClusterType, + Nonce: hexString(1), + Resources: makeTestResources(t, + makeTestCluster(t, snap, "tcp:local_app"), + makeTestCluster(t, snap, "tcp:db"), + ), + }) + }) + + testutil.RunStep(t, "terminate limiter session", func(t *testing.T) { + limiter.TerminateSession() + + select { + case err := <-errCh: + require.Error(t, err) + require.Equal(t, codes.ResourceExhausted.String(), status.Code(err).String()) + case <-time.After(50 * time.Millisecond): + t.Fatalf("timed out waiting for handler to finish") + } + }) + + testutil.RunStep(t, "check drain counter incremeted", func(t *testing.T) { + data := scenario.sink.Data() + require.Len(t, data, 1) + + item := data[0] + require.Len(t, item.Counters, 1) + + val, ok := item.Counters["consul.xds.test.xds.server.streamDrained"] + require.True(t, ok) + require.Equal(t, 1, val.Count) + }) +} + +type testLimiter struct { + termCh chan struct{} +} + +func (t *testLimiter) BeginSession() (limiter.Session, error) { + t.termCh = make(chan struct{}) + return &testSession{termCh: t.termCh}, nil +} + +func (t *testLimiter) TerminateSession() { close(t.termCh) } + +type testSession struct { + termCh chan struct{} +} + +func (t *testSession) Terminated() <-chan struct{} { return t.termCh } + +func (*testSession) End() {} + func assertDeltaChanBlocked(t *testing.T, ch chan *envoy_discovery_v3.DeltaDiscoveryResponse) { t.Helper() select { diff --git a/agent/xds/endpoints.go b/agent/xds/endpoints.go index 2b187a68f..b5588ce64 100644 --- a/agent/xds/endpoints.go +++ b/agent/xds/endpoints.go @@ -50,14 +50,19 @@ func (s *ResourceGenerator) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg. cfgSnap.ConnectProxy.PeerUpstreamEndpoints.Len()+ len(cfgSnap.ConnectProxy.WatchedUpstreamEndpoints)) - // NOTE: Any time we skip a chain below we MUST also skip that discovery chain in clusters.go - // so that the sets of endpoints generated matches the sets of clusters. - for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { + getUpstream := func(uid proxycfg.UpstreamID) (*structs.Upstream, bool) { upstream := cfgSnap.ConnectProxy.UpstreamConfig[uid] explicit := upstream.HasLocalPortOrSocket() implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { + return upstream, !implicit && !explicit + } + + // NOTE: Any time we skip a chain below we MUST also skip that discovery chain in clusters.go + // so that the sets of endpoints generated matches the sets of clusters. + for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { + upstream, skip := getUpstream(uid) + if skip { // Discovery chain is not associated with a known explicit or implicit upstream so it is skipped. continue } @@ -70,6 +75,7 @@ func (s *ResourceGenerator) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg. es, err := s.endpointsFromDiscoveryChain( uid, chain, + cfgSnap, cfgSnap.Locality, upstreamConfigMap, cfgSnap.ConnectProxy.WatchedUpstreamEndpoints[uid], @@ -86,12 +92,9 @@ func (s *ResourceGenerator) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg. // upstream in clusters.go so that the sets of endpoints generated matches // the sets of clusters. for _, uid := range cfgSnap.ConnectProxy.PeeredUpstreamIDs() { - upstreamCfg := cfgSnap.ConnectProxy.UpstreamConfig[uid] - - explicit := upstreamCfg.HasLocalPortOrSocket() - implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { - // Not associated with a known explicit or implicit upstream so it is skipped. + _, skip := getUpstream(uid) + if skip { + // Discovery chain is not associated with a known explicit or implicit upstream so it is skipped. continue } @@ -104,22 +107,14 @@ func (s *ResourceGenerator) endpointsFromSnapshotConnectProxy(cfgSnap *proxycfg. clusterName := generatePeeredClusterName(uid, tbs) - // Also skip peer instances with a hostname as their address. EDS - // cannot resolve hostnames, so we provide them through CDS instead. - if _, ok := cfgSnap.ConnectProxy.PeerUpstreamEndpointsUseHostnames[uid]; ok { - continue + loadAssignment, err := s.makeUpstreamLoadAssignmentForPeerService(cfgSnap, clusterName, uid) + + if err != nil { + return nil, err } - endpoints, ok := cfgSnap.ConnectProxy.PeerUpstreamEndpoints.Get(uid) - if ok { - la := makeLoadAssignment( - clusterName, - []loadAssignmentEndpointGroup{ - {Endpoints: endpoints}, - }, - proxycfg.GatewayKey{ /*empty so it never matches*/ }, - ) - resources = append(resources, la) + if loadAssignment != nil { + resources = append(resources, loadAssignment) } } @@ -375,6 +370,7 @@ func (s *ResourceGenerator) endpointsFromSnapshotIngressGateway(cfgSnap *proxycf es, err := s.endpointsFromDiscoveryChain( uid, cfgSnap.IngressGateway.DiscoveryChain[uid], + cfgSnap, proxycfg.GatewayKey{Datacenter: cfgSnap.Datacenter, Partition: u.DestinationPartition}, u.Config, cfgSnap.IngressGateway.WatchedUpstreamEndpoints[uid], @@ -412,9 +408,38 @@ func makePipeEndpoint(path string) *envoy_endpoint_v3.LbEndpoint { } } +func (s *ResourceGenerator) makeUpstreamLoadAssignmentForPeerService(cfgSnap *proxycfg.ConfigSnapshot, clusterName string, uid proxycfg.UpstreamID) (*envoy_endpoint_v3.ClusterLoadAssignment, error) { + var la *envoy_endpoint_v3.ClusterLoadAssignment + + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + if err != nil { + return la, err + } + + // Also skip peer instances with a hostname as their address. EDS + // cannot resolve hostnames, so we provide them through CDS instead. + if _, ok := upstreamsSnapshot.PeerUpstreamEndpointsUseHostnames[uid]; ok { + return la, nil + } + + endpoints, ok := upstreamsSnapshot.PeerUpstreamEndpoints.Get(uid) + if !ok { + return nil, nil + } + la = makeLoadAssignment( + clusterName, + []loadAssignmentEndpointGroup{ + {Endpoints: endpoints}, + }, + proxycfg.GatewayKey{ /*empty so it never matches*/ }, + ) + return la, nil +} + func (s *ResourceGenerator) endpointsFromDiscoveryChain( uid proxycfg.UpstreamID, chain *structs.CompiledDiscoveryChain, + cfgSnap *proxycfg.ConfigSnapshot, gatewayKey proxycfg.GatewayKey, upstreamConfigMap map[string]interface{}, upstreamEndpoints map[string]structs.CheckServiceNodes, @@ -432,6 +457,14 @@ func (s *ResourceGenerator) endpointsFromDiscoveryChain( upstreamConfigMap = make(map[string]interface{}) // TODO:needed? } + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + + // Mesh gateways are exempt because upstreamsSnapshot is only used for + // cluster peering targets and transative failover/redirects are unsupported. + if err != nil && !forMeshGateway { + return nil, err + } + var resources []proto.Message var escapeHatchCluster *envoy_cluster_v3.Cluster @@ -465,80 +498,73 @@ func (s *ResourceGenerator) endpointsFromDiscoveryChain( if node.Type != structs.DiscoveryGraphNodeTypeResolver { continue } + primaryTargetID := node.Resolver.Target failover := node.Resolver.Failover - targetID := node.Resolver.Target - target := chain.Targets[targetID] - - clusterName := CustomizeClusterName(target.Name, chain) - if escapeHatchCluster != nil { - clusterName = escapeHatchCluster.Name - } - if forMeshGateway { - clusterName = meshGatewayExportedClusterNamePrefix + clusterName - } - s.Logger.Debug("generating endpoints for", "cluster", clusterName) - - // Determine if we have to generate the entire cluster differently. - failoverThroughMeshGateway := chain.WillFailoverThroughMeshGateway(node) && !forMeshGateway - - if failoverThroughMeshGateway { - actualTargetID := firstHealthyTarget( - chain.Targets, - upstreamEndpoints, - targetID, - failover.Targets, - ) - if actualTargetID != targetID { - targetID = actualTargetID - } - - failover = nil - } - - primaryGroup, valid := makeLoadAssignmentEndpointGroup( - chain.Targets, - upstreamEndpoints, - gatewayEndpoints, - targetID, - gatewayKey, - forMeshGateway, - ) - if !valid { - continue // skip the cluster if we're still populating the snapshot + type targetLoadAssignmentOption struct { + targetID string + clusterName string } + var targetsClustersData []targetClusterData var numFailoverTargets int if failover != nil { numFailoverTargets = len(failover.Targets) } - - endpointGroups := make([]loadAssignmentEndpointGroup, 0, numFailoverTargets+1) - endpointGroups = append(endpointGroups, primaryGroup) - - if failover != nil && len(failover.Targets) > 0 { - for _, failTargetID := range failover.Targets { - failoverGroup, valid := makeLoadAssignmentEndpointGroup( - chain.Targets, - upstreamEndpoints, - gatewayEndpoints, - failTargetID, - gatewayKey, - forMeshGateway, - ) - if !valid { - continue // skip the failover target if we're still populating the snapshot + if numFailoverTargets > 0 && !forMeshGateway { + for _, targetID := range append([]string{primaryTargetID}, failover.Targets...) { + targetData, ok := s.getTargetClusterData(upstreamsSnapshot, chain, targetID, forMeshGateway, true) + if !ok { + continue } - endpointGroups = append(endpointGroups, failoverGroup) + if escapeHatchCluster != nil { + targetData.clusterName = escapeHatchCluster.Name + } + + targetsClustersData = append(targetsClustersData, targetData) + } + } else { + if td, ok := s.getTargetClusterData(upstreamsSnapshot, chain, primaryTargetID, forMeshGateway, false); ok { + if escapeHatchCluster != nil { + td.clusterName = escapeHatchCluster.Name + } + targetsClustersData = append(targetsClustersData, td) } } - la := makeLoadAssignment( - clusterName, - endpointGroups, - gatewayKey, - ) - resources = append(resources, la) + for _, targetOpt := range targetsClustersData { + s.Logger.Debug("generating endpoints for", "cluster", targetOpt.clusterName) + targetUID := proxycfg.NewUpstreamIDFromTargetID(targetOpt.targetID) + if targetUID.Peer != "" { + loadAssignment, err := s.makeUpstreamLoadAssignmentForPeerService(cfgSnap, targetOpt.clusterName, targetUID) + if err != nil { + return nil, err + } + if loadAssignment != nil { + resources = append(resources, loadAssignment) + } + continue + } + + endpointGroup, valid := makeLoadAssignmentEndpointGroup( + chain.Targets, + upstreamEndpoints, + gatewayEndpoints, + targetOpt.targetID, + gatewayKey, + forMeshGateway, + ) + if !valid { + continue // skip the cluster if we're still populating the snapshot + } + + la := makeLoadAssignment( + targetOpt.clusterName, + []loadAssignmentEndpointGroup{endpointGroup}, + gatewayKey, + ) + resources = append(resources, la) + } } return resources, nil @@ -591,6 +617,7 @@ func (s *ResourceGenerator) makeExportedUpstreamEndpointsForMeshGateway(cfgSnap clusterEndpoints, err := s.endpointsFromDiscoveryChain( proxycfg.NewUpstreamIDFromServiceName(svc), chain, + cfgSnap, cfgSnap.Locality, nil, chainEndpoints, @@ -645,11 +672,12 @@ func makeLoadAssignment(clusterName string, endpointGroups []loadAssignmentEndpo healthStatus = endpointGroup.OverrideHealth } + endpoint := &envoy_endpoint_v3.Endpoint{ + Address: makeAddress(addr, port), + } es = append(es, &envoy_endpoint_v3.LbEndpoint{ HostIdentifier: &envoy_endpoint_v3.LbEndpoint_Endpoint{ - Endpoint: &envoy_endpoint_v3.Endpoint{ - Address: makeAddress(addr, port), - }, + Endpoint: endpoint, }, HealthStatus: healthStatus, LoadBalancingWeight: makeUint32Value(weight), diff --git a/agent/xds/endpoints_test.go b/agent/xds/endpoints_test.go index b02bdd725..432ecfa2c 100644 --- a/agent/xds/endpoints_test.go +++ b/agent/xds/endpoints_test.go @@ -396,6 +396,13 @@ func TestEndpointsFromSnapshot(t *testing.T) { "failover", nil, nil, nil) }, }, + { + name: "ingress-with-chain-and-failover-to-cluster-peer", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotIngressGateway(t, true, "tcp", + "failover-to-cluster-peer", nil, nil, nil) + }, + }, { name: "ingress-with-tcp-chain-failover-through-remote-gateway", create: func(t testinf.T) *proxycfg.ConfigSnapshot { diff --git a/agent/xds/envoy_versioning_test.go b/agent/xds/envoy_versioning_test.go index 4e446de1e..b0e9c0dba 100644 --- a/agent/xds/envoy_versioning_test.go +++ b/agent/xds/envoy_versioning_test.go @@ -138,7 +138,7 @@ func TestDetermineSupportedProxyFeaturesFromString(t *testing.T) { "1.20.0", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", "1.21.0", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.22.0", "1.22.1", "1.22.2", - "1.23.0", + "1.23.0", "1.23.1", } { cases[v] = testcase{expect: supportedProxyFeatures{}} } diff --git a/agent/xds/failover_math_test.go b/agent/xds/failover_math_test.go index 29ac17ffe..296d1cc77 100644 --- a/agent/xds/failover_math_test.go +++ b/agent/xds/failover_math_test.go @@ -15,15 +15,40 @@ func TestFirstHealthyTarget(t *testing.T) { warning := proxycfg.TestUpstreamNodesInStatus(t, "warning") critical := proxycfg.TestUpstreamNodesInStatus(t, "critical") - warnOnlyPassingTarget := structs.NewDiscoveryTarget("all-warn", "", "default", "default", "dc1") + warnOnlyPassingTarget := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-warn", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }) warnOnlyPassingTarget.Subset.OnlyPassing = true - failOnlyPassingTarget := structs.NewDiscoveryTarget("all-fail", "", "default", "default", "dc1") + failOnlyPassingTarget := structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-fail", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }) failOnlyPassingTarget.Subset.OnlyPassing = true targets := map[string]*structs.DiscoveryTarget{ - "all-ok.default.dc1": structs.NewDiscoveryTarget("all-ok", "", "default", "default", "dc1"), - "all-warn.default.dc1": structs.NewDiscoveryTarget("all-warn", "", "default", "default", "dc1"), - "all-fail.default.default.dc1": structs.NewDiscoveryTarget("all-fail", "", "default", "default", "dc1"), + "all-ok.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-ok", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), + "all-warn.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-warn", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), + "all-fail.default.default.dc1": structs.NewDiscoveryTarget(structs.DiscoveryTargetOpts{ + Service: "all-fail", + Namespace: "default", + Partition: "default", + Datacenter: "dc1", + }), "all-warn-onlypassing.default.dc1": warnOnlyPassingTarget, "all-fail-onlypassing.default.dc1": failOnlyPassingTarget, } diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index 95b84c94c..cfea25cbc 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -3,7 +3,6 @@ package xds import ( "errors" "fmt" - envoy_extensions_filters_listener_http_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3" "net" "net/url" "regexp" @@ -12,6 +11,8 @@ import ( "strings" "time" + envoy_extensions_filters_listener_http_inspector_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/http_inspector/v3" + envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -107,12 +108,36 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. } } - for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { - upstreamCfg := cfgSnap.ConnectProxy.UpstreamConfig[uid] + proxyCfg, err := ParseProxyConfig(cfgSnap.Proxy.Config) + if err != nil { + // Don't hard fail on a config typo, just warn. The parse func returns + // default config if there is an error so it's safe to continue. + s.Logger.Warn("failed to parse Connect.Proxy.Config", "error", err) + } + var tracing *envoy_http_v3.HttpConnectionManager_Tracing + if proxyCfg.ListenerTracingJSON != "" { + if tracing, err = makeTracingFromUserConfig(proxyCfg.ListenerTracingJSON); err != nil { + s.Logger.Warn("failed to parse ListenerTracingJSON config", "error", err) + } + } - explicit := upstreamCfg.HasLocalPortOrSocket() + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + if err != nil { + return nil, err + } + + getUpstream := func(uid proxycfg.UpstreamID) (*structs.Upstream, bool) { + upstream := cfgSnap.ConnectProxy.UpstreamConfig[uid] + + explicit := upstream.HasLocalPortOrSocket() implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { + return upstream, !implicit && !explicit + } + + for uid, chain := range cfgSnap.ConnectProxy.DiscoveryChain { + upstreamCfg, skip := getUpstream(uid) + + if skip { // Discovery chain is not associated with a known explicit or implicit upstream so it is skipped. continue } @@ -132,7 +157,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. // RDS, Envoy's Route Discovery Service, is only used for HTTP services with a customized discovery chain. useRDS := chain.Protocol != "tcp" && !chain.Default - var clusterName string + var targetClusterData targetClusterData if !useRDS { // When not using RDS we must generate a cluster name to attach to the filter chain. // With RDS, cluster names get attached to the dynamic routes instead. @@ -140,7 +165,12 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. if err != nil { return nil, err } - clusterName = CustomizeClusterName(target.Name, chain) + + td, ok := s.getTargetClusterData(upstreamsSnapshot, chain, target.ID, false, false) + if !ok { + continue + } + targetClusterData = td } filterName := fmt.Sprintf("%s.%s.%s.%s", chain.ServiceName, chain.Namespace, chain.Partition, chain.Datacenter) @@ -149,10 +179,11 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. if upstreamCfg != nil && upstreamCfg.HasLocalPortOrSocket() { filterChain, err := s.makeUpstreamFilterChain(filterChainOpts{ routeName: uid.EnvoyID(), - clusterName: clusterName, + clusterName: targetClusterData.clusterName, filterName: filterName, protocol: cfg.Protocol, useRDS: useRDS, + tracing: tracing, }) if err != nil { return nil, err @@ -174,10 +205,11 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. filterChain, err := s.makeUpstreamFilterChain(filterChainOpts{ routeName: uid.EnvoyID(), - clusterName: clusterName, + clusterName: targetClusterData.clusterName, filterName: filterName, protocol: cfg.Protocol, useRDS: useRDS, + tracing: tracing, }) if err != nil { return nil, err @@ -249,6 +281,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. filterName: routeName, protocol: svcConfig.Protocol, useRDS: true, + tracing: tracing, }) if err != nil { return err @@ -265,6 +298,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. clusterName: clusterName, filterName: clusterName, protocol: svcConfig.Protocol, + tracing: tracing, }) if err != nil { return err @@ -301,11 +335,9 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. // Looping over explicit and implicit upstreams is only needed for cross-peer // because they do not have discovery chains. for _, uid := range cfgSnap.ConnectProxy.PeeredUpstreamIDs() { - upstreamCfg := cfgSnap.ConnectProxy.UpstreamConfig[uid] + upstreamCfg, skip := getUpstream(uid) - explicit := upstreamCfg.HasLocalPortOrSocket() - implicit := cfgSnap.ConnectProxy.IsImplicitUpstream(uid) - if !implicit && !explicit { + if skip { // Not associated with a known explicit or implicit upstream so it is skipped. continue } @@ -376,6 +408,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. protocol: cfg.Protocol, useRDS: false, statPrefix: "upstream_peered.", + tracing: tracing, }) if err != nil { return nil, err @@ -533,6 +566,7 @@ func (s *ResourceGenerator) listenersFromSnapshotConnectProxy(cfgSnap *proxycfg. filterName: uid.EnvoyID(), routeName: uid.EnvoyID(), protocol: cfg.Protocol, + tracing: tracing, }) if err != nil { return nil, err @@ -1188,12 +1222,20 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot l = makePortListener(name, addr, port, envoy_core_v3.TrafficDirection_INBOUND) + var tracing *envoy_http_v3.HttpConnectionManager_Tracing + if cfg.ListenerTracingJSON != "" { + if tracing, err = makeTracingFromUserConfig(cfg.ListenerTracingJSON); err != nil { + s.Logger.Warn("failed to parse ListenerTracingJSON config", "error", err) + } + } + filterOpts := listenerFilterOpts{ protocol: cfg.Protocol, filterName: name, routeName: name, cluster: LocalAppClusterName, requestTimeoutMs: cfg.LocalRequestTimeoutMs, + tracing: tracing, } if useHTTPFilter { filterOpts.httpAuthzFilter, err = makeRBACHTTPFilter( @@ -1214,16 +1256,38 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot filterOpts.forwardClientPolicy = envoy_http_v3.HttpConnectionManager_APPEND_FORWARD } } + + // If an inbound connect limit is set, inject a connection limit filter on each chain. + if cfg.MaxInboundConnections > 0 { + connectionLimitFilter, err := makeConnectionLimitFilter(cfg.MaxInboundConnections) + if err != nil { + return nil, err + } + l.FilterChains = []*envoy_listener_v3.FilterChain{ + { + Filters: []*envoy_listener_v3.Filter{ + connectionLimitFilter, + }, + }, + } + } + filter, err := makeListenerFilter(filterOpts) if err != nil { return nil, err } - l.FilterChains = []*envoy_listener_v3.FilterChain{ - { - Filters: []*envoy_listener_v3.Filter{ - filter, + + if len(l.FilterChains) > 0 { + // The list of FilterChains has already been initialized + l.FilterChains[0].Filters = append(l.FilterChains[0].Filters, filter) + } else { + l.FilterChains = []*envoy_listener_v3.FilterChain{ + { + Filters: []*envoy_listener_v3.Filter{ + filter, + }, }, - }, + } } err = s.finalizePublicListenerFromConfig(l, cfgSnap, cfg, useHTTPFilter) @@ -1249,17 +1313,6 @@ func (s *ResourceGenerator) finalizePublicListenerFromConfig(l *envoy_listener_v return nil } - // If an inbound connect limit is set, inject a connection limit filter on each chain. - if proxyCfg.MaxInboundConnections > 0 { - filter, err := makeConnectionLimitFilter(proxyCfg.MaxInboundConnections) - if err != nil { - return nil - } - for idx := range l.FilterChains { - l.FilterChains[idx].Filters = append(l.FilterChains[idx].Filters, filter) - } - } - return nil } @@ -1299,6 +1352,7 @@ func (s *ResourceGenerator) makeExposedCheckListener(cfgSnap *proxycfg.ConfigSna statPrefix: "", routePath: path.Path, httpAuthzFilter: nil, + // in the exposed check listener we don't set the tracing configuration } f, err := makeListenerFilter(opts) if err != nil { @@ -1531,6 +1585,19 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg. filterChain.Filters = append(filterChain.Filters, authFilter) } + proxyCfg, err := ParseProxyConfig(cfgSnap.Proxy.Config) + if err != nil { + // Don't hard fail on a config typo, just warn. The parse func returns + // default config if there is an error so it's safe to continue. + s.Logger.Warn("failed to parse Connect.Proxy.Config", "error", err) + } + var tracing *envoy_http_v3.HttpConnectionManager_Tracing + if proxyCfg.ListenerTracingJSON != "" { + if tracing, err = makeTracingFromUserConfig(proxyCfg.ListenerTracingJSON); err != nil { + s.Logger.Warn("failed to parse ListenerTracingJSON config", "error", err) + } + } + // Lastly we setup the actual proxying component. For L4 this is a straight // tcp proxy. For L7 this is a very hands-off HTTP proxy just to inject an // HTTP filter to do intention checks here instead. @@ -1541,6 +1608,7 @@ func (s *ResourceGenerator) makeFilterChainTerminatingGateway(cfgSnap *proxycfg. cluster: tgtwyOpts.cluster, statPrefix: "upstream.", routePath: "", + tracing: tracing, } if useHTTPFilter { @@ -1787,6 +1855,7 @@ type filterChainOpts struct { statPrefix string forwardClientDetails bool forwardClientPolicy envoy_http_v3.HttpConnectionManager_ForwardClientCertDetails + tracing *envoy_http_v3.HttpConnectionManager_Tracing } func (s *ResourceGenerator) makeUpstreamFilterChain(opts filterChainOpts) (*envoy_listener_v3.FilterChain, error) { @@ -1802,6 +1871,7 @@ func (s *ResourceGenerator) makeUpstreamFilterChain(opts filterChainOpts) (*envo statPrefix: opts.statPrefix, forwardClientDetails: opts.forwardClientDetails, forwardClientPolicy: opts.forwardClientPolicy, + tracing: opts.tracing, }) if err != nil { return nil, err @@ -1944,6 +2014,7 @@ type listenerFilterOpts struct { httpAuthzFilter *envoy_http_v3.HttpFilter forwardClientDetails bool forwardClientPolicy envoy_http_v3.HttpConnectionManager_ForwardClientCertDetails + tracing *envoy_http_v3.HttpConnectionManager_Tracing } func makeListenerFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error) { @@ -1990,6 +2061,7 @@ func makeTCPProxyFilter(filterName, cluster, statPrefix string) (*envoy_listener func makeConnectionLimitFilter(limit int) (*envoy_listener_v3.Filter, error) { cfg := &envoy_connection_limit_v3.ConnectionLimit{ + StatPrefix: "inbound_connection_limit", MaxConnections: wrapperspb.UInt64(uint64(limit)), } return makeFilter("envoy.filters.network.connection_limit", cfg) @@ -2002,6 +2074,19 @@ func makeStatPrefix(prefix, filterName string) string { return fmt.Sprintf("%s%s", prefix, strings.Replace(filterName, ":", "_", -1)) } +func makeTracingFromUserConfig(configJSON string) (*envoy_http_v3.HttpConnectionManager_Tracing, error) { + // Type field is present so decode it as a any.Any + var any any.Any + if err := jsonpb.UnmarshalString(configJSON, &any); err != nil { + return nil, err + } + var t envoy_http_v3.HttpConnectionManager_Tracing + if err := proto.Unmarshal(any.Value, &t); err != nil { + return nil, err + } + return &t, nil +} + func makeHTTPFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error) { router, err := makeEnvoyHTTPFilter("envoy.filters.http.router", &envoy_http_router_v3.Router{}) if err != nil { @@ -2022,6 +2107,10 @@ func makeHTTPFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error) }, } + if opts.tracing != nil { + cfg.Tracing = opts.tracing + } + if opts.useRDS { if opts.cluster != "" { return nil, fmt.Errorf("cannot specify cluster name when using RDS") diff --git a/agent/xds/listeners_test.go b/agent/xds/listeners_test.go index c51730074..1112222f3 100644 --- a/agent/xds/listeners_test.go +++ b/agent/xds/listeners_test.go @@ -772,6 +772,15 @@ func TestListenersFromSnapshot(t *testing.T) { name: "transparent-proxy-terminating-gateway", create: proxycfg.TestConfigSnapshotTransparentProxyTerminatingGatewayCatalogDestinationsOnly, }, + { + name: "custom-trace-listener", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { + ns.Proxy.Config["protocol"] = "http" + ns.Proxy.Config["envoy_listener_tracing_json"] = customTraceJSON(t) + }, nil) + }, + }, } latestEnvoyVersion := proxysupport.EnvoyVersions[0] @@ -947,6 +956,40 @@ func customHTTPListenerJSON(t testinf.T, opts customHTTPListenerJSONOptions) str return buf.String() } +func customTraceJSON(t testinf.T) string { + t.Helper() + return ` + { + "@type" : "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.Tracing", + "provider" : { + "name" : "envoy.tracers.zipkin", + "typed_config" : { + "@type" : "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig", + "collector_cluster" : "otelcolector", + "collector_endpoint" : "/api/v2/spans", + "collector_endpoint_version" : "HTTP_JSON", + "shared_span_context" : false + } + }, + "custom_tags" : [ + { + "tag" : "custom_header", + "request_header" : { + "name" : "x-custom-traceid", + "default_value" : "" + } + }, + { + "tag" : "alloc_id", + "environment" : { + "name" : "NOMAD_ALLOC_ID" + } + } + ] + } + ` +} + type configFetcherFunc func() string var _ ConfigFetcher = (configFetcherFunc)(nil) diff --git a/agent/xds/proxysupport/proxysupport.go b/agent/xds/proxysupport/proxysupport.go index bdb7cc864..80befe05c 100644 --- a/agent/xds/proxysupport/proxysupport.go +++ b/agent/xds/proxysupport/proxysupport.go @@ -7,7 +7,7 @@ package proxysupport // // see: https://www.consul.io/docs/connect/proxies/envoy#supported-versions var EnvoyVersions = []string{ - "1.23.0", + "1.23.1", "1.22.2", "1.21.4", "1.20.6", diff --git a/agent/xds/resources_test.go b/agent/xds/resources_test.go index fbb98b121..53274d719 100644 --- a/agent/xds/resources_test.go +++ b/agent/xds/resources_test.go @@ -159,6 +159,7 @@ func TestAllResourcesFromSnapshot(t *testing.T) { } tests = append(tests, getConnectProxyTransparentProxyGoldenTestCases()...) tests = append(tests, getMeshGatewayPeeringGoldenTestCases()...) + tests = append(tests, getTrafficControlPeeringGoldenTestCases()...) tests = append(tests, getEnterpriseGoldenTestCases()...) latestEnvoyVersion := proxysupport.EnvoyVersions[0] @@ -216,3 +217,20 @@ func getMeshGatewayPeeringGoldenTestCases() []goldenTestCase { }, } } + +func getTrafficControlPeeringGoldenTestCases() []goldenTestCase { + return []goldenTestCase{ + { + name: "connect-proxy-with-chain-and-failover-to-cluster-peer", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover-to-cluster-peer", nil, nil) + }, + }, + { + name: "connect-proxy-with-chain-and-redirect-to-cluster-peer", + create: func(t testinf.T) *proxycfg.ConfigSnapshot { + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "redirect-to-cluster-peer", nil, nil) + }, + }, + } +} diff --git a/agent/xds/routes.go b/agent/xds/routes.go index 321faa5cd..9bbf3ff71 100644 --- a/agent/xds/routes.go +++ b/agent/xds/routes.go @@ -57,7 +57,7 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh continue } - virtualHost, err := makeUpstreamRouteForDiscoveryChain(uid.EnvoyID(), chain, []string{"*"}, "") + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false) if err != nil { return nil, err } @@ -249,11 +249,12 @@ func (s *ResourceGenerator) routesForMeshGateway(cfgSnap *proxycfg.ConfigSnapsho uid := proxycfg.NewUpstreamIDFromServiceName(svc) - virtualHost, err := makeUpstreamRouteForDiscoveryChain( - uid.EnvoyID(), + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain( + cfgSnap, + uid, chain, []string{"*"}, - meshGatewayExportedClusterNamePrefix, + true, ) if err != nil { return nil, err @@ -367,7 +368,7 @@ func (s *ResourceGenerator) routesForIngressGateway(cfgSnap *proxycfg.ConfigSnap } domains := generateUpstreamIngressDomains(listenerKey, u) - virtualHost, err := makeUpstreamRouteForDiscoveryChain(uid.EnvoyID(), chain, domains, "") + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false) if err != nil { return nil, err } @@ -500,12 +501,14 @@ func generateUpstreamIngressDomains(listenerKey proxycfg.IngressListenerKey, u s return domains } -func makeUpstreamRouteForDiscoveryChain( - routeName string, +func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain( + cfgSnap *proxycfg.ConfigSnapshot, + uid proxycfg.UpstreamID, chain *structs.CompiledDiscoveryChain, serviceDomains []string, - clusterNamePrefix string, + forMeshGateway bool, ) (*envoy_route_v3.VirtualHost, error) { + routeName := uid.EnvoyID() var routes []*envoy_route_v3.Route startNode := chain.Nodes[chain.StartNode] @@ -513,6 +516,11 @@ func makeUpstreamRouteForDiscoveryChain( return nil, fmt.Errorf("missing first node in compiled discovery chain for: %s", chain.ServiceName) } + upstreamsSnapshot, err := cfgSnap.ToConfigSnapshotUpstreams() + if err != nil && !forMeshGateway { + return nil, err + } + switch startNode.Type { case structs.DiscoveryGraphNodeTypeRouter: routes = make([]*envoy_route_v3.Route, 0, len(startNode.Routes)) @@ -534,13 +542,17 @@ func makeUpstreamRouteForDiscoveryChain( switch nextNode.Type { case structs.DiscoveryGraphNodeTypeSplitter: - routeAction, err = makeRouteActionForSplitter(nextNode.Splits, chain, clusterNamePrefix) + routeAction, err = s.makeRouteActionForSplitter(upstreamsSnapshot, nextNode.Splits, chain, forMeshGateway) if err != nil { return nil, err } case structs.DiscoveryGraphNodeTypeResolver: - routeAction = makeRouteActionForChainCluster(nextNode.Resolver.Target, chain, clusterNamePrefix) + ra, ok := s.makeRouteActionForChainCluster(upstreamsSnapshot, nextNode.Resolver.Target, chain, forMeshGateway) + if !ok { + continue + } + routeAction = ra default: return nil, fmt.Errorf("unexpected graph node after route %q", nextNode.Type) @@ -599,11 +611,10 @@ func makeUpstreamRouteForDiscoveryChain( } case structs.DiscoveryGraphNodeTypeSplitter: - routeAction, err := makeRouteActionForSplitter(startNode.Splits, chain, clusterNamePrefix) + routeAction, err := s.makeRouteActionForSplitter(upstreamsSnapshot, startNode.Splits, chain, forMeshGateway) if err != nil { return nil, err } - var lb *structs.LoadBalancer if startNode.LoadBalancer != nil { lb = startNode.LoadBalancer @@ -620,8 +631,10 @@ func makeUpstreamRouteForDiscoveryChain( routes = []*envoy_route_v3.Route{defaultRoute} case structs.DiscoveryGraphNodeTypeResolver: - routeAction := makeRouteActionForChainCluster(startNode.Resolver.Target, chain, clusterNamePrefix) - + routeAction, ok := s.makeRouteActionForChainCluster(upstreamsSnapshot, startNode.Resolver.Target, chain, forMeshGateway) + if !ok { + break + } var lb *structs.LoadBalancer if startNode.LoadBalancer != nil { lb = startNode.LoadBalancer @@ -782,13 +795,17 @@ func makeDefaultRouteMatch() *envoy_route_v3.RouteMatch { } } -func makeRouteActionForChainCluster( +func (s *ResourceGenerator) makeRouteActionForChainCluster( + upstreamsSnapshot *proxycfg.ConfigSnapshotUpstreams, targetID string, chain *structs.CompiledDiscoveryChain, - clusterNamePrefix string, -) *envoy_route_v3.Route_Route { - target := chain.Targets[targetID] - return makeRouteActionFromName(clusterNamePrefix + CustomizeClusterName(target.Name, chain)) + forMeshGateway bool, +) (*envoy_route_v3.Route_Route, bool) { + td, ok := s.getTargetClusterData(upstreamsSnapshot, chain, targetID, forMeshGateway, false) + if !ok { + return nil, false + } + return makeRouteActionFromName(td.clusterName), true } func makeRouteActionFromName(clusterName string) *envoy_route_v3.Route_Route { @@ -801,10 +818,11 @@ func makeRouteActionFromName(clusterName string) *envoy_route_v3.Route_Route { } } -func makeRouteActionForSplitter( +func (s *ResourceGenerator) makeRouteActionForSplitter( + upstreamsSnapshot *proxycfg.ConfigSnapshotUpstreams, splits []*structs.DiscoverySplit, chain *structs.CompiledDiscoveryChain, - clusterNamePrefix string, + forMeshGateway bool, ) (*envoy_route_v3.Route_Route, error) { clusters := make([]*envoy_route_v3.WeightedCluster_ClusterWeight, 0, len(splits)) for _, split := range splits { @@ -815,15 +833,16 @@ func makeRouteActionForSplitter( } targetID := nextNode.Resolver.Target - target := chain.Targets[targetID] - - clusterName := clusterNamePrefix + CustomizeClusterName(target.Name, chain) + targetOptions, ok := s.getTargetClusterData(upstreamsSnapshot, chain, targetID, forMeshGateway, false) + if !ok { + continue + } // The smallest representable weight is 1/10000 or .01% but envoy // deals with integers so scale everything up by 100x. cw := &envoy_route_v3.WeightedCluster_ClusterWeight{ Weight: makeUint32Value(int(split.Weight * 100)), - Name: clusterName, + Name: targetOptions.clusterName, } if err := injectHeaderManipToWeightedCluster(split.Definition, cw); err != nil { return nil, err diff --git a/agent/xds/server.go b/agent/xds/server.go index cc27f3fde..74f386fc5 100644 --- a/agent/xds/server.go +++ b/agent/xds/server.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/consul/acl" external "github.com/hashicorp/consul/agent/grpc-external" + "github.com/hashicorp/consul/agent/grpc-external/limiter" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/xds/xdscommon" @@ -29,6 +30,13 @@ var StatsGauges = []prometheus.GaugeDefinition{ }, } +var StatsCounters = []prometheus.CounterDefinition{ + { + Name: []string{"xds", "server", "streamDrained"}, + Help: "Counts the number of xDS streams that are drained when rebalancing the load between servers.", + }, +} + // ADSStream is a shorter way of referring to this thing... type ADSStream = envoy_discovery_v3.AggregatedDiscoveryService_StreamAggregatedResourcesServer @@ -97,17 +105,24 @@ type ProxyConfigSource interface { Watch(id structs.ServiceID, nodeName string, token string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc, error) } +// SessionLimiter is the interface exposed by limiter.SessionLimiter. We depend +// on an interface rather than the concrete type so we can mock it in tests. +type SessionLimiter interface { + BeginSession() (limiter.Session, error) +} + // Server represents a gRPC server that can handle xDS requests from Envoy. All // of it's public members must be set before the gRPC server is started. // // A full description of the XDS protocol can be found at // https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol type Server struct { - NodeName string - Logger hclog.Logger - CfgSrc ProxyConfigSource - ResolveToken ACLResolverFunc - CfgFetcher ConfigFetcher + NodeName string + Logger hclog.Logger + CfgSrc ProxyConfigSource + ResolveToken ACLResolverFunc + CfgFetcher ConfigFetcher + SessionLimiter SessionLimiter // AuthCheckFrequency is how often we should re-check the credentials used // during a long-lived gRPC Stream after it has been initially established. @@ -159,6 +174,7 @@ func NewServer( cfgMgr ProxyConfigSource, resolveToken ACLResolverFunc, cfgFetcher ConfigFetcher, + limiter SessionLimiter, ) *Server { return &Server{ NodeName: nodeName, @@ -166,6 +182,7 @@ func NewServer( CfgSrc: cfgMgr, ResolveToken: resolveToken, CfgFetcher: cfgFetcher, + SessionLimiter: limiter, AuthCheckFrequency: DefaultAuthCheckFrequency, activeStreams: &activeStreamCounters{}, serverlessPluginEnabled: serverlessPluginEnabled, @@ -186,6 +203,18 @@ func (s *Server) Register(srv *grpc.Server) { envoy_discovery_v3.RegisterAggregatedDiscoveryServiceServer(srv, s) } +func (s *Server) authenticate(ctx context.Context) (acl.Authorizer, error) { + authz, err := s.ResolveToken(external.TokenFromContext(ctx)) + if acl.IsErrNotFound(err) { + return nil, status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err) + } else if acl.IsErrPermissionDenied(err) { + return nil, status.Error(codes.PermissionDenied, err.Error()) + } else if err != nil { + return nil, status.Errorf(codes.Internal, "error resolving acl token: %v", err) + } + return authz, nil +} + // authorize the xDS request using the token stored in ctx. This authorization is // a bit different from most interfaces. Instead of explicitly authorizing or // filtering each piece of data in the response, the request is authorized @@ -201,13 +230,9 @@ func (s *Server) authorize(ctx context.Context, cfgSnap *proxycfg.ConfigSnapshot return status.Errorf(codes.Unauthenticated, "unauthenticated: no config snapshot") } - authz, err := s.ResolveToken(external.TokenFromContext(ctx)) - if acl.IsErrNotFound(err) { - return status.Errorf(codes.Unauthenticated, "unauthenticated: %v", err) - } else if acl.IsErrPermissionDenied(err) { - return status.Error(codes.PermissionDenied, err.Error()) - } else if err != nil { - return status.Errorf(codes.Internal, "error resolving acl token: %v", err) + authz, err := s.authenticate(ctx) + if err != nil { + return err } var authzContext acl.AuthorizerContext diff --git a/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..61de6b2e2 --- /dev/null +++ b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,219 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.cluster-01.external.peer1.domain" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.cluster-01.external.peer1.domain", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "1s", + "circuitBreakers": { + + }, + "outlierDetection": { + "maxEjectionPercent": 100 + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "peer1-root-1\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://1c053652-8512-4373-90cf-5a7f6263a994.consul/ns/default/dc/cluster-01-dc/svc/payments" + } + ] + } + }, + "sni": "payments.default.default.cluster-01.external.1c053652-8512-4373-90cf-5a7f6263a994.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" + }, + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" + } + ] + } + }, + "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connectTimeout": "5s", + "loadAssignment": { + "clusterName": "local_app", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 8080 + } + } + } + } + ] + } + ] + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover.latest.golden index 1ee533e39..8a6f88a86 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-failover.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,14 +68,69 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/fail" } ] } }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + "sni": "fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } }, diff --git a/agent/xds/testdata/clusters/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden new file mode 100644 index 000000000..0bd578a1c --- /dev/null +++ b/agent/xds/testdata/clusters/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden @@ -0,0 +1,144 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.cluster-01.external.peer1.domain", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "1s", + "circuitBreakers": { + + }, + "outlierDetection": { + "maxEjectionPercent": 100 + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "peer1-root-1\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://1c053652-8512-4373-90cf-5a7f6263a994.consul/ns/default/dc/cluster-01-dc/svc/payments" + } + ] + } + }, + "sni": "payments.default.default.cluster-01.external.1c053652-8512-4373-90cf-5a7f6263a994.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" + }, + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" + } + ] + } + }, + "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connectTimeout": "5s", + "loadAssignment": { + "clusterName": "local_app", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 8080 + } + } + } + } + ] + } + ] + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/clusters/connect-proxy-with-peered-upstreams.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-peered-upstreams.latest.golden index d7c23515f..29059b143 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-peered-upstreams.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-peered-upstreams.latest.golden @@ -58,7 +58,7 @@ "dnsRefreshRate": "10s", "dnsLookupFamily": "V4_ONLY", "outlierDetection": { - + "maxEjectionPercent": 100 }, "commonLbConfig": { "healthyPanicThreshold": { @@ -115,7 +115,7 @@ }, "outlierDetection": { - + "maxEjectionPercent": 100 }, "commonLbConfig": { "healthyPanicThreshold": { diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden index e2462b797..e434b71c1 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,10 +69,120 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden index 947dab839..e434b71c1 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,17 +69,127 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } ] } }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + "sni": "db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" } } }, diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden index e2462b797..e434b71c1 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,10 +69,120 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden index 947dab839..e434b71c1 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,17 +69,127 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } ] } }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + "sni": "db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" } } }, diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden index 1fb114f4c..8a032b321 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,7 +68,62 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden index 8dea5d4a9..8a032b321 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,14 +68,69 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } ] } }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" } } }, diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden index 1fb114f4c..8a032b321 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,7 +68,62 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } diff --git a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden index 8dea5d4a9..8a032b321 100644 --- a/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/clusters/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,14 +68,69 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } ] } }, - "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" } } }, diff --git a/agent/xds/testdata/clusters/custom-passive-healthcheck.latest.golden b/agent/xds/testdata/clusters/custom-passive-healthcheck.latest.golden new file mode 100644 index 000000000..41bc16f6e --- /dev/null +++ b/agent/xds/testdata/clusters/custom-passive-healthcheck.latest.golden @@ -0,0 +1,147 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + "consecutive5xx": 5, + "interval": "0.000000010s", + "enforcingConsecutive5xx": 80 + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" + }, + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" + } + ] + } + }, + "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connectTimeout": "5s", + "loadAssignment": { + "clusterName": "local_app", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 8080 + } + } + } + } + ] + } + ] + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/clusters/ingress-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/clusters/ingress-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..94521dc8f --- /dev/null +++ b/agent/xds/testdata/clusters/ingress-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,139 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.cluster-01.external.peer1.domain" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.cluster-01.external.peer1.domain", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + "maxEjectionPercent": 100 + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "peer1-root-1\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://1c053652-8512-4373-90cf-5a7f6263a994.consul/ns/default/dc/cluster-01-dc/svc/payments" + } + ] + } + }, + "sni": "payments.default.default.cluster-01.external.1c053652-8512-4373-90cf-5a7f6263a994.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/clusters/ingress-with-chain-and-failover.latest.golden b/agent/xds/testdata/clusters/ingress-with-chain-and-failover.latest.golden index 70a32347f..7db8a7d2c 100644 --- a/agent/xds/testdata/clusters/ingress-with-chain-and-failover.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-chain-and-failover.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,9 +68,6 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/fail" } ] } @@ -61,6 +75,64 @@ "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/fail" + } + ] + } + }, + "sni": "fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } } ], "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden index 3835d6c41..58962bf83 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,10 +69,120 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden index 6df722098..58962bf83 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,12 +69,6 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } ] } @@ -64,6 +76,122 @@ "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" + } + ] + } + }, + "sni": "db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + } + } } ], "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden index 3835d6c41..58962bf83 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,10 +69,120 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden index 6df722098..58962bf83 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden @@ -5,6 +5,24 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,12 +69,6 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" } ] } @@ -64,6 +76,122 @@ "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc3/svc/db" + } + ] + } + }, + "sni": "db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul" + } + } } ], "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden index e175f5bc8..787f0d621 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,7 +68,62 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden index 7ac10864a..787f0d621 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,9 +68,6 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } ] } @@ -61,6 +75,64 @@ "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } } ], "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden index e175f5bc8..787f0d621 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,7 +68,62 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } diff --git a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden index 7ac10864a..787f0d621 100644 --- a/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/clusters/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden @@ -5,6 +5,23 @@ "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterType": { + "name": "envoy.clusters.aggregate", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.clusters.aggregate.v3.ClusterConfig", + "clusters": [ + "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + ] + } + }, + "connectTimeout": "33s", + "lbPolicy": "CLUSTER_PROVIDED" + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "type": "EDS", "edsClusterConfig": { "edsConfig": { @@ -51,9 +68,6 @@ "matchSubjectAltNames": [ { "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" - }, - { - "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" } ] } @@ -61,6 +75,64 @@ "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" } } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "33s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/db" + } + ] + } + }, + "sni": "db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul" + } + } } ], "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", diff --git a/agent/xds/testdata/clusters/transparent-proxy-with-peered-upstreams.latest.golden b/agent/xds/testdata/clusters/transparent-proxy-with-peered-upstreams.latest.golden index 0dbbf4277..d1f6d0bb0 100644 --- a/agent/xds/testdata/clusters/transparent-proxy-with-peered-upstreams.latest.golden +++ b/agent/xds/testdata/clusters/transparent-proxy-with-peered-upstreams.latest.golden @@ -18,7 +18,7 @@ }, "outlierDetection": { - + "maxEjectionPercent": 100 }, "commonLbConfig": { "healthyPanicThreshold": { @@ -75,7 +75,7 @@ }, "outlierDetection": { - + "maxEjectionPercent": 100 }, "commonLbConfig": { "healthyPanicThreshold": { @@ -157,7 +157,7 @@ }, "outlierDetection": { - + "maxEjectionPercent": 100 }, "commonLbConfig": { "healthyPanicThreshold": { diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..feaea9055 --- /dev/null +++ b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,109 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.cluster-01.external.peer1.domain", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.20.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + } + ], + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover.latest.golden index 86e7209e7..c3e5de807 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-failover.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -32,7 +32,13 @@ "loadBalancingWeight": 1 } ] - }, + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ { "lbEndpoints": [ { @@ -59,13 +65,9 @@ "healthStatus": "HEALTHY", "loadBalancingWeight": 1 } - ], - "priority": 1 + ] } - ], - "policy": { - "overprovisioningFactor": 100000 - } + ] }, { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden new file mode 100644 index 000000000..830d3941e --- /dev/null +++ b/agent/xds/testdata/endpoints/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden @@ -0,0 +1,75 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "db.default.cluster-01.external.peer1.domain", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.20.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + } + ], + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden index 57bbeada3..d8c936d61 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden @@ -3,7 +3,75 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden index 6e4d37bc3..40ae7a089 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-local-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -35,6 +35,40 @@ } ] }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden index 2cc64bbba..356b5b847 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden @@ -3,7 +3,75 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden index 6e4d37bc3..1f88cfbb3 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-double-failover-through-remote-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -35,6 +35,40 @@ } ] }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.38.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.38.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden index 57bbeada3..dd6083fa2 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden @@ -3,7 +3,41 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden index 6e4d37bc3..5e45cbd43 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-local-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -35,6 +35,40 @@ } ] }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden index 554520f7e..6a840c993 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden @@ -3,7 +3,41 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden index 6e4d37bc3..29f1a8c59 100644 --- a/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/connect-proxy-with-tcp-chain-failover-through-remote-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -35,6 +35,40 @@ } ] }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", diff --git a/agent/xds/testdata/endpoints/ingress-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/endpoints/ingress-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..c799a5a0c --- /dev/null +++ b/agent/xds/testdata/endpoints/ingress-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,75 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.cluster-01.external.peer1.domain", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.40.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + } + ], + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/endpoints/ingress-with-chain-and-failover.latest.golden b/agent/xds/testdata/endpoints/ingress-with-chain-and-failover.latest.golden index e0685f1ae..1ef4d0329 100644 --- a/agent/xds/testdata/endpoints/ingress-with-chain-and-failover.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-chain-and-failover.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -32,7 +32,13 @@ "loadBalancingWeight": 1 } ] - }, + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~fail.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ { "lbEndpoints": [ { @@ -59,13 +65,9 @@ "healthStatus": "HEALTHY", "loadBalancingWeight": 1 } - ], - "priority": 1 + ] } - ], - "policy": { - "overprovisioningFactor": 100000 - } + ] } ], "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden index 61392962e..f38f82acf 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway-triggered.latest.golden @@ -3,7 +3,75 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden index ff3888642..b55f54586 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-local-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -34,6 +34,40 @@ ] } ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] } ], "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden index 961d515a1..9d5d1c1fa 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway-triggered.latest.golden @@ -3,7 +3,75 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden index ff3888642..ebbf0281e 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-double-failover-through-remote-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -34,6 +34,40 @@ ] } ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc3.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.38.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.38.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] } ], "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden index 61392962e..0aad49182 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway-triggered.latest.golden @@ -3,7 +3,41 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden index ff3888642..c5ee31d45 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-local-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -34,6 +34,40 @@ ] } ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] } ], "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden index d808b212f..324a48430 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway-triggered.latest.golden @@ -3,7 +3,41 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "UNHEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ diff --git a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden index ff3888642..2021f8fab 100644 --- a/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden +++ b/agent/xds/testdata/endpoints/ingress-with-tcp-chain-failover-through-remote-gateway.latest.golden @@ -3,7 +3,7 @@ "resources": [ { "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", - "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "clusterName": "failover-target~db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", "endpoints": [ { "lbEndpoints": [ @@ -34,6 +34,40 @@ ] } ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "failover-target~db.default.dc2.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.1", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "198.18.1.2", + "portValue": 443 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] } ], "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", diff --git a/agent/xds/testdata/listeners/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/listeners/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..57d50f71c --- /dev/null +++ b/agent/xds/testdata/listeners/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,119 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "db:127.0.0.1:9191", + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 9191 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.db.default.default.dc1", + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "prepared_query:geo-cache:127.10.10.10:8181", + "address": { + "socketAddress": { + "address": "127.10.10.10", + "portValue": 8181 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.prepared_query_geo-cache", + "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:0.0.0.0:9999", + "address": { + "socketAddress": { + "address": "0.0.0.0", + "portValue": 9999 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + + }, + "statPrefix": "connect_authz" + } + }, + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "public_listener", + "cluster": "local_app" + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + } + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden b/agent/xds/testdata/listeners/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden new file mode 100644 index 000000000..e061148c0 --- /dev/null +++ b/agent/xds/testdata/listeners/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden @@ -0,0 +1,119 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "db:127.0.0.1:9191", + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 9191 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.db.default.default.dc1", + "cluster": "db.default.cluster-01.external.peer1.domain" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "prepared_query:geo-cache:127.10.10.10:8181", + "address": { + "socketAddress": { + "address": "127.10.10.10", + "portValue": 8181 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.prepared_query_geo-cache", + "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:0.0.0.0:9999", + "address": { + "socketAddress": { + "address": "0.0.0.0", + "portValue": 9999 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC", + "rules": { + + }, + "statPrefix": "connect_authz" + } + }, + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "public_listener", + "cluster": "local_app" + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + } + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/custom-trace-listener.latest.golden b/agent/xds/testdata/listeners/custom-trace-listener.latest.golden new file mode 100644 index 000000000..5fce12bb7 --- /dev/null +++ b/agent/xds/testdata/listeners/custom-trace-listener.latest.golden @@ -0,0 +1,180 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "db:127.0.0.1:9191", + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 9191 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.db.default.default.dc1", + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "prepared_query:geo-cache:127.10.10.10:8181", + "address": { + "socketAddress": { + "address": "127.10.10.10", + "portValue": 8181 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.prepared_query_geo-cache", + "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:0.0.0.0:9999", + "address": { + "socketAddress": { + "address": "0.0.0.0", + "portValue": 9999 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "public_listener", + "routeConfig": { + "name": "public_listener", + "virtualHosts": [ + { + "name": "public_listener", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + + } + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "customTags": [ + { + "tag": "custom_header", + "requestHeader": { + "name": "x-custom-traceid" + } + }, + { + "tag": "alloc_id", + "environment": { + "name": "NOMAD_ALLOC_ID" + } + } + ], + "provider": { + "name": "envoy.tracers.zipkin", + "typedConfig": { + "@type": "type.googleapis.com/envoy.config.trace.v3.ZipkinConfig", + "collectorCluster": "otelcolector", + "collectorEndpoint": "/api/v2/spans", + "sharedSpanContext": false, + "collectorEndpointVersion": "HTTP_JSON" + } + } + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + } + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden b/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden index be3b83433..cbfda69f5 100644 --- a/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden +++ b/agent/xds/testdata/listeners/listener-max-inbound-connections.latest.golden @@ -73,6 +73,14 @@ "statPrefix": "connect_authz" } }, + { + "name": "envoy.filters.network.connection_limit", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit", + "statPrefix": "inbound_connection_limit", + "maxConnections": "222" + } + }, { "name": "envoy.filters.network.tcp_proxy", "typedConfig": { @@ -80,13 +88,6 @@ "statPrefix": "public_listener", "cluster": "local_app" } - }, - { - "name": "envoy.filters.network.connection_limit", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit", - "maxConnections": "222" - } } ], "transportSocket": { diff --git a/agent/xds/testdata/routes/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden b/agent/xds/testdata/routes/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden new file mode 100644 index 000000000..547b923b0 --- /dev/null +++ b/agent/xds/testdata/routes/connect-proxy-with-chain-and-failover-to-cluster-peer.latest.golden @@ -0,0 +1,30 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "name": "db", + "virtualHosts": [ + { + "name": "db", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "validateClusters": true + } + ], + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/routes/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden b/agent/xds/testdata/routes/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden new file mode 100644 index 000000000..e63a643ef --- /dev/null +++ b/agent/xds/testdata/routes/connect-proxy-with-chain-and-redirect-to-cluster-peer.latest.golden @@ -0,0 +1,30 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "name": "db", + "virtualHosts": [ + { + "name": "db", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "db.default.cluster-01.external.peer1.domain" + } + } + ] + } + ], + "validateClusters": true + } + ], + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/xds_protocol_helpers_test.go b/agent/xds/xds_protocol_helpers_test.go index a27eaba2f..7cb413def 100644 --- a/agent/xds/xds_protocol_helpers_test.go +++ b/agent/xds/xds_protocol_helpers_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/grpc-external/limiter" envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" @@ -136,6 +137,7 @@ func newTestServerDeltaScenario( token string, authCheckFrequency time.Duration, serverlessPluginEnabled bool, + sessionLimiter SessionLimiter, ) *testServerScenario { mgr := newTestManager(t) envoy := NewTestEnvoy(t, proxyID, token) @@ -154,6 +156,10 @@ func newTestServerDeltaScenario( metrics.NewGlobal(cfg, sink) }) + if sessionLimiter == nil { + sessionLimiter = limiter.NewSessionLimiter() + } + s := NewServer( "node-123", testutil.Logger(t), @@ -161,6 +167,7 @@ func newTestServerDeltaScenario( mgr, resolveToken, nil, /*cfgFetcher ConfigFetcher*/ + sessionLimiter, ) if authCheckFrequency > 0 { s.AuthCheckFrequency = authCheckFrequency diff --git a/api/agent_test.go b/api/agent_test.go index d67aba7b8..0c1660b1e 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -363,7 +363,7 @@ func TestAPI_AgentServicesWithFilterOpts(t *testing.T) { } require.NoError(t, agent.ServiceRegister(reg)) - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} services, err := agent.ServicesWithFilterOpts("foo in Tags", opts) require.NoError(t, err) require.Len(t, services, 1) @@ -791,8 +791,8 @@ func TestAPI_AgentService(t *testing.T) { Warning: 1, }, Meta: map[string]string{}, - Namespace: splitDefaultNamespace, - Partition: splitDefaultPartition, + Namespace: defaultNamespace, + Partition: defaultPartition, Datacenter: "dc1", } require.Equal(t, expect, got) @@ -932,7 +932,7 @@ func TestAPI_AgentUpdateTTLOpts(t *testing.T) { } } - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} if err := agent.UpdateTTLOpts("service:foo", "foo", HealthWarning, opts); err != nil { t.Fatalf("err: %v", err) @@ -1007,7 +1007,7 @@ func TestAPI_AgentChecksWithFilterOpts(t *testing.T) { reg.TTL = "15s" require.NoError(t, agent.CheckRegister(reg)) - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} checks, err := agent.ChecksWithFilterOpts("Name == foo", opts) require.NoError(t, err) require.Len(t, checks, 1) @@ -1382,7 +1382,7 @@ func TestAPI_ServiceMaintenanceOpts(t *testing.T) { } // Specify namespace in query option - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} // Enable maintenance mode if err := agent.EnableServiceMaintenanceOpts("redis", "broken", opts); err != nil { @@ -1701,7 +1701,7 @@ func TestAPI_AgentHealthServiceOpts(t *testing.T) { requireServiceHealthID := func(t *testing.T, serviceID, expected string, shouldExist bool) { msg := fmt.Sprintf("service id:%s, shouldExist:%v, expectedStatus:%s : bad %%s", serviceID, shouldExist, expected) - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} state, out, err := agent.AgentHealthServiceByIDOpts(serviceID, opts) require.Nil(t, err, msg, "err") require.Equal(t, expected, state, msg, "state") @@ -1715,7 +1715,7 @@ func TestAPI_AgentHealthServiceOpts(t *testing.T) { requireServiceHealthName := func(t *testing.T, serviceName, expected string, shouldExist bool) { msg := fmt.Sprintf("service name:%s, shouldExist:%v, expectedStatus:%s : bad %%s", serviceName, shouldExist, expected) - opts := &QueryOptions{Namespace: splitDefaultNamespace} + opts := &QueryOptions{Namespace: defaultNamespace} state, outs, err := agent.AgentHealthServiceByNameOpts(serviceName, opts) require.Nil(t, err, msg, "err") require.Equal(t, expected, state, msg, "state") diff --git a/api/catalog.go b/api/catalog.go index 80ae325ea..84a2bdbc6 100644 --- a/api/catalog.go +++ b/api/catalog.go @@ -20,6 +20,7 @@ type Node struct { CreateIndex uint64 ModifyIndex uint64 Partition string `json:",omitempty"` + PeerName string `json:",omitempty"` } type ServiceAddress struct { diff --git a/api/catalog_test.go b/api/catalog_test.go index 2c926d199..b0071e87f 100644 --- a/api/catalog_test.go +++ b/api/catalog_test.go @@ -51,7 +51,7 @@ func TestAPI_CatalogNodes(t *testing.T) { want := &Node{ ID: s.Config.NodeID, Node: s.Config.NodeName, - Partition: splitDefaultPartition, + Partition: defaultPartition, Address: "127.0.0.1", Datacenter: "dc1", TaggedAddresses: map[string]string{ @@ -1144,8 +1144,8 @@ func TestAPI_CatalogGatewayServices_Terminating(t *testing.T) { expect := []*GatewayService{ { - Service: CompoundServiceName{Name: "api", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, - Gateway: CompoundServiceName{Name: "terminating", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, + Service: CompoundServiceName{Name: "api", Namespace: defaultNamespace, Partition: defaultPartition}, + Gateway: CompoundServiceName{Name: "terminating", Namespace: defaultNamespace, Partition: defaultPartition}, GatewayKind: ServiceKindTerminatingGateway, CAFile: "api/ca.crt", CertFile: "api/client.crt", @@ -1153,8 +1153,8 @@ func TestAPI_CatalogGatewayServices_Terminating(t *testing.T) { SNI: "my-domain", }, { - Service: CompoundServiceName{Name: "redis", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, - Gateway: CompoundServiceName{Name: "terminating", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, + Service: CompoundServiceName{Name: "redis", Namespace: defaultNamespace, Partition: defaultPartition}, + Gateway: CompoundServiceName{Name: "terminating", Namespace: defaultNamespace, Partition: defaultPartition}, GatewayKind: ServiceKindTerminatingGateway, CAFile: "ca.crt", CertFile: "client.crt", @@ -1212,15 +1212,15 @@ func TestAPI_CatalogGatewayServices_Ingress(t *testing.T) { expect := []*GatewayService{ { - Service: CompoundServiceName{Name: "api", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, - Gateway: CompoundServiceName{Name: "ingress", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, + Service: CompoundServiceName{Name: "api", Namespace: defaultNamespace, Partition: defaultPartition}, + Gateway: CompoundServiceName{Name: "ingress", Namespace: defaultNamespace, Partition: defaultPartition}, GatewayKind: ServiceKindIngressGateway, Protocol: "tcp", Port: 8888, }, { - Service: CompoundServiceName{Name: "redis", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, - Gateway: CompoundServiceName{Name: "ingress", Namespace: splitDefaultNamespace, Partition: splitDefaultPartition}, + Service: CompoundServiceName{Name: "redis", Namespace: defaultNamespace, Partition: defaultPartition}, + Gateway: CompoundServiceName{Name: "ingress", Namespace: defaultNamespace, Partition: defaultPartition}, GatewayKind: ServiceKindIngressGateway, Protocol: "tcp", Port: 9999, diff --git a/api/config_entry.go b/api/config_entry.go index ee55b55ad..acdb5bfa8 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -196,6 +196,11 @@ type PassiveHealthCheck struct { // MaxFailures is the count of consecutive failures that results in a host // being removed from the pool. MaxFailures uint32 `alias:"max_failures"` + + // EnforcingConsecutive5xx is the % chance that a host will be actually ejected + // when an outlier status is detected through consecutive 5xx. + // This setting can be used to disable ejection or to ramp it up slowly. + EnforcingConsecutive5xx *uint32 `json:",omitempty" alias:"enforcing_consecutive_5xx"` } // UpstreamLimits describes the limits that are associated with a specific @@ -218,21 +223,24 @@ type UpstreamLimits struct { } type ServiceConfigEntry struct { - Kind string - Name string - Partition string `json:",omitempty"` - Namespace string `json:",omitempty"` - Protocol string `json:",omitempty"` - Mode ProxyMode `json:",omitempty"` - TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` - MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` - Expose ExposeConfig `json:",omitempty"` - ExternalSNI string `json:",omitempty" alias:"external_sni"` - UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` - Destination *DestinationConfig `json:",omitempty"` - Meta map[string]string `json:",omitempty"` - CreateIndex uint64 - ModifyIndex uint64 + Kind string + Name string + Partition string `json:",omitempty"` + Namespace string `json:",omitempty"` + Protocol string `json:",omitempty"` + Mode ProxyMode `json:",omitempty"` + TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` + MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` + Expose ExposeConfig `json:",omitempty"` + ExternalSNI string `json:",omitempty" alias:"external_sni"` + UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` + Destination *DestinationConfig `json:",omitempty"` + MaxInboundConnections int `json:",omitempty" alias:"max_inbound_connections"` + LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"` + LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"` + Meta map[string]string `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 } func (s *ServiceConfigEntry) GetKind() string { return s.Kind } diff --git a/api/config_entry_discoverychain.go b/api/config_entry_discoverychain.go index dfb2bcc10..f827708ee 100644 --- a/api/config_entry_discoverychain.go +++ b/api/config_entry_discoverychain.go @@ -219,14 +219,25 @@ type ServiceResolverRedirect struct { Namespace string `json:",omitempty"` Partition string `json:",omitempty"` Datacenter string `json:",omitempty"` + Peer string `json:",omitempty"` } type ServiceResolverFailover struct { Service string `json:",omitempty"` ServiceSubset string `json:",omitempty" alias:"service_subset"` // Referencing other partitions is not supported. - Namespace string `json:",omitempty"` - Datacenters []string `json:",omitempty"` + Namespace string `json:",omitempty"` + Datacenters []string `json:",omitempty"` + Targets []ServiceResolverFailoverTarget `json:",omitempty"` +} + +type ServiceResolverFailoverTarget struct { + Service string `json:",omitempty"` + ServiceSubset string `json:",omitempty" alias:"service_subset"` + Partition string `json:",omitempty"` + Namespace string `json:",omitempty"` + Datacenter string `json:",omitempty"` + Peer string `json:",omitempty"` } // LoadBalancer determines the load balancing policy and configuration for services diff --git a/api/config_entry_discoverychain_test.go b/api/config_entry_discoverychain_test.go index 95dc54280..8facb72e1 100644 --- a/api/config_entry_discoverychain_test.go +++ b/api/config_entry_discoverychain_test.go @@ -139,8 +139,8 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { entry: &ServiceResolverConfigEntry{ Kind: ServiceResolver, Name: "test-failover", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, DefaultSubset: "v1", Subsets: map[string]ServiceResolverSubset{ "v1": { @@ -149,6 +149,9 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { "v2": { Filter: "Service.Meta.version == v2", }, + "v3": { + Filter: "Service.Meta.version == v3", + }, }, Failover: map[string]ServiceResolverFailover{ "*": { @@ -156,7 +159,14 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { }, "v1": { Service: "alternate", - Namespace: splitDefaultNamespace, + Namespace: defaultNamespace, + }, + "v3": { + Targets: []ServiceResolverFailoverTarget{ + {Peer: "cluster-01"}, + {Datacenter: "dc1"}, + {Service: "another-service", ServiceSubset: "v1"}, + }, }, }, ConnectTimeout: 5 * time.Second, @@ -172,30 +182,44 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { entry: &ServiceResolverConfigEntry{ Kind: ServiceResolver, Name: "test-redirect", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, Redirect: &ServiceResolverRedirect{ Service: "test-failover", ServiceSubset: "v2", - Namespace: splitDefaultNamespace, + Namespace: defaultNamespace, Datacenter: "d", }, }, verify: verifyResolver, }, + { + name: "redirect to peer", + entry: &ServiceResolverConfigEntry{ + Kind: ServiceResolver, + Name: "test-redirect", + Partition: defaultPartition, + Namespace: defaultNamespace, + Redirect: &ServiceResolverRedirect{ + Service: "test-failover", + Peer: "cluster-01", + }, + }, + verify: verifyResolver, + }, { name: "mega splitter", // use one mega object to avoid multiple trips entry: &ServiceSplitterConfigEntry{ Kind: ServiceSplitter, Name: "test-split", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, Splits: []ServiceSplit{ { Weight: 90, Service: "test-failover", ServiceSubset: "v1", - Namespace: splitDefaultNamespace, + Namespace: defaultNamespace, RequestHeaders: &HTTPHeaderModifiers{ Set: map[string]string{ "x-foo": "bar", @@ -208,7 +232,7 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { { Weight: 10, Service: "test-redirect", - Namespace: splitDefaultNamespace, + Namespace: defaultNamespace, }, }, Meta: map[string]string{ @@ -223,8 +247,8 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { entry: &ServiceRouterConfigEntry{ Kind: ServiceRouter, Name: "test-route", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, Routes: []ServiceRoute{ { Match: &ServiceRouteMatch{ @@ -241,8 +265,8 @@ func TestAPI_ConfigEntry_DiscoveryChain(t *testing.T) { Destination: &ServiceRouteDestination{ Service: "test-failover", ServiceSubset: "v2", - Namespace: splitDefaultNamespace, - Partition: splitDefaultPartition, + Namespace: defaultNamespace, + Partition: defaultPartition, PrefixRewrite: "/", RequestTimeout: 5 * time.Second, NumRetries: 5, @@ -334,8 +358,8 @@ func TestAPI_ConfigEntry_ServiceResolver_LoadBalancer(t *testing.T) { entry: &ServiceResolverConfigEntry{ Kind: ServiceResolver, Name: "test-least-req", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, LoadBalancer: &LoadBalancer{ Policy: "least_request", LeastRequestConfig: &LeastRequestConfig{ChoiceCount: 10}, @@ -348,8 +372,8 @@ func TestAPI_ConfigEntry_ServiceResolver_LoadBalancer(t *testing.T) { entry: &ServiceResolverConfigEntry{ Kind: ServiceResolver, Name: "test-ring-hash", - Namespace: splitDefaultNamespace, - Partition: splitDefaultPartition, + Namespace: defaultNamespace, + Partition: defaultPartition, LoadBalancer: &LoadBalancer{ Policy: "ring_hash", RingHashConfig: &RingHashConfig{ diff --git a/api/config_entry_exports.go b/api/config_entry_exports.go index e162b5fa6..0827e5816 100644 --- a/api/config_entry_exports.go +++ b/api/config_entry_exports.go @@ -57,7 +57,7 @@ type ServiceConsumer struct { func (e *ExportedServicesConfigEntry) GetKind() string { return ExportedServices } func (e *ExportedServicesConfigEntry) GetName() string { return e.Name } func (e *ExportedServicesConfigEntry) GetPartition() string { return e.Name } -func (e *ExportedServicesConfigEntry) GetNamespace() string { return splitDefaultNamespace } +func (e *ExportedServicesConfigEntry) GetNamespace() string { return "" } func (e *ExportedServicesConfigEntry) GetMeta() map[string]string { return e.Meta } func (e *ExportedServicesConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex } func (e *ExportedServicesConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex } diff --git a/api/config_entry_exports_test.go b/api/config_entry_exports_test.go index 8d56be0ab..4a6f3c7a2 100644 --- a/api/config_entry_exports_test.go +++ b/api/config_entry_exports_test.go @@ -17,7 +17,7 @@ func TestAPI_ConfigEntries_ExportedServices(t *testing.T) { testutil.RunStep(t, "set and get", func(t *testing.T) { exports := &ExportedServicesConfigEntry{ Name: PartitionDefaultName, - Partition: splitDefaultPartition, + Partition: defaultPartition, Meta: map[string]string{ "gir": "zim", }, @@ -48,7 +48,7 @@ func TestAPI_ConfigEntries_ExportedServices(t *testing.T) { Services: []ExportedService{ { Name: "db", - Namespace: splitDefaultNamespace, + Namespace: defaultNamespace, Consumers: []ServiceConsumer{ { PeerName: "alpha", @@ -60,7 +60,7 @@ func TestAPI_ConfigEntries_ExportedServices(t *testing.T) { "foo": "bar", "gir": "zim", }, - Partition: splitDefaultPartition, + Partition: defaultPartition, } _, wm, err := entries.Set(updated, nil) diff --git a/api/config_entry_mesh.go b/api/config_entry_mesh.go index 406e87dfc..98b882247 100644 --- a/api/config_entry_mesh.go +++ b/api/config_entry_mesh.go @@ -23,6 +23,8 @@ type MeshConfigEntry struct { HTTP *MeshHTTPConfig `json:",omitempty"` + Peering *PeeringMeshConfig `json:",omitempty"` + Meta map[string]string `json:",omitempty"` // CreateIndex is the Raft index this entry was created at. This is a @@ -54,6 +56,10 @@ type MeshHTTPConfig struct { SanitizeXForwardedClientCert bool `alias:"sanitize_x_forwarded_client_cert"` } +type PeeringMeshConfig struct { + PeerThroughMeshGateways bool `json:",omitempty" alias:"peer_through_mesh_gateways"` +} + func (e *MeshConfigEntry) GetKind() string { return MeshConfig } func (e *MeshConfigEntry) GetName() string { return MeshConfigMesh } func (e *MeshConfigEntry) GetPartition() string { return e.Partition } diff --git a/api/config_entry_test.go b/api/config_entry_test.go index 4249e7547..1502111d8 100644 --- a/api/config_entry_test.go +++ b/api/config_entry_test.go @@ -104,6 +104,9 @@ func TestAPI_ConfigEntries(t *testing.T) { "foo": "bar", "gir": "zim", }, + MaxInboundConnections: 5, + LocalConnectTimeoutMs: 5000, + LocalRequestTimeoutMs: 7000, } dest := &DestinationConfig{ @@ -144,6 +147,9 @@ func TestAPI_ConfigEntries(t *testing.T) { require.Equal(t, service.Protocol, readService.Protocol) require.Equal(t, service.Meta, readService.Meta) require.Equal(t, service.Meta, readService.GetMeta()) + require.Equal(t, service.MaxInboundConnections, readService.MaxInboundConnections) + require.Equal(t, service.LocalConnectTimeoutMs, readService.LocalConnectTimeoutMs) + require.Equal(t, service.LocalRequestTimeoutMs, readService.LocalRequestTimeoutMs) // update it service.Protocol = "tcp" @@ -213,8 +219,8 @@ func TestAPI_ConfigEntries(t *testing.T) { "foo": "bar", "gir": "zim", }, - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, } ce := c.ConfigEntries() @@ -446,7 +452,8 @@ func TestDecodeConfigEntry(t *testing.T) { "Name": "redis", "PassiveHealthCheck": { "MaxFailures": 3, - "Interval": "2s" + "Interval": "2s", + "EnforcingConsecutive5xx": 60 } }, { @@ -496,8 +503,9 @@ func TestDecodeConfigEntry(t *testing.T) { { Name: "redis", PassiveHealthCheck: &PassiveHealthCheck{ - MaxFailures: 3, - Interval: 2 * time.Second, + MaxFailures: 3, + Interval: 2 * time.Second, + EnforcingConsecutive5xx: uint32Pointer(60), }, }, { @@ -1308,6 +1316,9 @@ func TestDecodeConfigEntry(t *testing.T) { }, "HTTP": { "SanitizeXForwardedClientCert": true + }, + "Peering": { + "PeerThroughMeshGateways": true } } `, @@ -1340,6 +1351,9 @@ func TestDecodeConfigEntry(t *testing.T) { HTTP: &MeshHTTPConfig{ SanitizeXForwardedClientCert: true, }, + Peering: &PeeringMeshConfig{ + PeerThroughMeshGateways: true, + }, }, }, } { @@ -1375,3 +1389,7 @@ func TestDecodeConfigEntry(t *testing.T) { func intPointer(v int) *int { return &v } + +func uint32Pointer(v uint32) *uint32 { + return &v +} diff --git a/api/coordinate_test.go b/api/coordinate_test.go index 984167e17..071b1f99e 100644 --- a/api/coordinate_test.go +++ b/api/coordinate_test.go @@ -87,7 +87,7 @@ func TestAPI_CoordinateUpdate(t *testing.T) { newCoord.Height = 0.5 entry := &CoordinateEntry{ Node: node, - Partition: splitDefaultPartition, + Partition: defaultPartition, Coord: newCoord, } _, err = coord.Update(entry, nil) diff --git a/api/health.go b/api/health.go index 2bcb3cb52..0886bb12a 100644 --- a/api/health.go +++ b/api/health.go @@ -45,6 +45,8 @@ type HealthCheck struct { Type string Namespace string `json:",omitempty"` Partition string `json:",omitempty"` + ExposedPort int + PeerName string `json:",omitempty"` Definition HealthCheckDefinition @@ -176,8 +178,7 @@ type HealthChecks []*HealthCheck // attached, this function determines the best representative of the status as // as single string using the following heuristic: // -// maintenance > critical > warning > passing -// +// maintenance > critical > warning > passing func (c HealthChecks) AggregatedStatus() string { var passing, warning, critical, maintenance bool for _, check := range c { diff --git a/api/health_test.go b/api/health_test.go index 7fc7a3f12..b69e9275f 100644 --- a/api/health_test.go +++ b/api/health_test.go @@ -223,8 +223,8 @@ func TestAPI_HealthChecks(t *testing.T) { ServiceName: "foo", ServiceTags: []string{"bar"}, Type: "ttl", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, }, } diff --git a/api/oss.go b/api/oss_test.go similarity index 79% rename from api/oss.go rename to api/oss_test.go index 93d639e69..e4e266a38 100644 --- a/api/oss.go +++ b/api/oss_test.go @@ -6,5 +6,5 @@ package api // The following defaults return "default" in enterprise and "" in OSS. // This constant is useful when a default value is needed for an // operation that will reject non-empty values in OSS. -const splitDefaultNamespace = "" -const splitDefaultPartition = "" +const defaultNamespace = "" +const defaultPartition = "" diff --git a/api/peering_test.go b/api/peering_test.go index 300584090..9c299b7a2 100644 --- a/api/peering_test.go +++ b/api/peering_test.go @@ -51,6 +51,7 @@ func TestAPI_Peering_ACLDeny(t *testing.T) { serverConfig.ACL.Enabled = true serverConfig.ACL.DefaultPolicy = "deny" serverConfig.Ports.GRPC = 5301 + serverConfig.Datacenter = "dc2" }) defer s2.Stop() diff --git a/api/prepared_query.go b/api/prepared_query.go index 60cd437cb..7e0518f58 100644 --- a/api/prepared_query.go +++ b/api/prepared_query.go @@ -17,6 +17,9 @@ type QueryFailoverOptions struct { Targets []QueryFailoverTarget } +// Deprecated: use QueryFailoverOptions instead. +type QueryDatacenterOptions = QueryFailoverOptions + type QueryFailoverTarget struct { // PeerName specifies a peer to try during failover. PeerName string diff --git a/api/txn.go b/api/txn.go index 59fd1c0d9..4aa06d9f5 100644 --- a/api/txn.go +++ b/api/txn.go @@ -67,6 +67,7 @@ const ( KVLock KVOp = "lock" KVUnlock KVOp = "unlock" KVGet KVOp = "get" + KVGetOrEmpty KVOp = "get-or-empty" KVGetTree KVOp = "get-tree" KVCheckSession KVOp = "check-session" KVCheckIndex KVOp = "check-index" diff --git a/api/txn_test.go b/api/txn_test.go index bf69b7bc8..81348a8c2 100644 --- a/api/txn_test.go +++ b/api/txn_test.go @@ -187,7 +187,7 @@ func TestAPI_ClientTxn(t *testing.T) { CreateIndex: ret.Results[0].KV.CreateIndex, ModifyIndex: ret.Results[0].KV.ModifyIndex, Namespace: ret.Results[0].KV.Namespace, - Partition: splitDefaultPartition, + Partition: defaultPartition, }, }, &TxnResult{ @@ -199,14 +199,14 @@ func TestAPI_ClientTxn(t *testing.T) { CreateIndex: ret.Results[1].KV.CreateIndex, ModifyIndex: ret.Results[1].KV.ModifyIndex, Namespace: ret.Results[0].KV.Namespace, - Partition: splitDefaultPartition, + Partition: defaultPartition, }, }, &TxnResult{ Node: &Node{ ID: nodeID, Node: "foo", - Partition: splitDefaultPartition, + Partition: defaultPartition, Address: "2.2.2.2", Datacenter: "dc1", CreateIndex: ret.Results[2].Node.CreateIndex, @@ -218,8 +218,8 @@ func TestAPI_ClientTxn(t *testing.T) { ID: "foo1", CreateIndex: ret.Results[3].Service.CreateIndex, ModifyIndex: ret.Results[3].Service.CreateIndex, - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, }, }, &TxnResult{ @@ -237,8 +237,8 @@ func TestAPI_ClientTxn(t *testing.T) { DeregisterCriticalServiceAfterDuration: 20 * time.Second, }, Type: "tcp", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, CreateIndex: ret.Results[4].Check.CreateIndex, ModifyIndex: ret.Results[4].Check.CreateIndex, }, @@ -258,8 +258,8 @@ func TestAPI_ClientTxn(t *testing.T) { DeregisterCriticalServiceAfterDuration: 160 * time.Second, }, Type: "tcp", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, CreateIndex: ret.Results[4].Check.CreateIndex, ModifyIndex: ret.Results[4].Check.CreateIndex, }, @@ -279,8 +279,8 @@ func TestAPI_ClientTxn(t *testing.T) { DeregisterCriticalServiceAfterDuration: 20 * time.Second, }, Type: "udp", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, CreateIndex: ret.Results[4].Check.CreateIndex, ModifyIndex: ret.Results[4].Check.CreateIndex, }, @@ -300,8 +300,8 @@ func TestAPI_ClientTxn(t *testing.T) { DeregisterCriticalServiceAfterDuration: 20 * time.Second, }, Type: "udp", - Partition: splitDefaultPartition, - Namespace: splitDefaultNamespace, + Partition: defaultPartition, + Namespace: defaultNamespace, CreateIndex: ret.Results[4].Check.CreateIndex, ModifyIndex: ret.Results[4].Check.CreateIndex, }, @@ -342,14 +342,14 @@ func TestAPI_ClientTxn(t *testing.T) { CreateIndex: ret.Results[0].KV.CreateIndex, ModifyIndex: ret.Results[0].KV.ModifyIndex, Namespace: ret.Results[0].KV.Namespace, - Partition: splitDefaultPartition, + Partition: defaultPartition, }, }, &TxnResult{ Node: &Node{ ID: s.Config.NodeID, Node: s.Config.NodeName, - Partition: splitDefaultPartition, + Partition: defaultPartition, Address: "127.0.0.1", Datacenter: "dc1", TaggedAddresses: map[string]string{ diff --git a/build-support/docker/Consul-Dev-Multiarch.dockerfile b/build-support/docker/Consul-Dev-Multiarch.dockerfile new file mode 100644 index 000000000..a3069bd99 --- /dev/null +++ b/build-support/docker/Consul-Dev-Multiarch.dockerfile @@ -0,0 +1,5 @@ +ARG CONSUL_IMAGE_VERSION=latest +FROM consul:${CONSUL_IMAGE_VERSION} +RUN apk update && apk add iptables +ARG TARGETARCH +COPY linux_${TARGETARCH}/consul /bin/consul diff --git a/command/agent/agent.go b/command/agent/agent.go index cc08213e1..ae455297a 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -213,8 +213,8 @@ func (c *cmd) run(args []string) int { } ui.Info(fmt.Sprintf(" Datacenter: '%s' (Segment: '%s')", config.Datacenter, segment)) ui.Info(fmt.Sprintf(" Server: %v (Bootstrap: %v)", config.ServerMode, config.Bootstrap)) - ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, gRPC: %d, DNS: %d)", config.ClientAddrs, - config.HTTPPort, config.HTTPSPort, config.GRPCPort, config.DNSPort)) + ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, gRPC: %d, gRPC-TLS: %d, DNS: %d)", config.ClientAddrs, + config.HTTPPort, config.HTTPSPort, config.GRPCPort, config.GRPCTLSPort, config.DNSPort)) ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddrLAN, config.SerfPortLAN, config.SerfPortWAN)) ui.Info(fmt.Sprintf("Gossip Encryption: %t", config.EncryptKey != "")) diff --git a/command/connect/envoy/envoy.go b/command/connect/envoy/envoy.go index 342357dfa..5567809fd 100644 --- a/command/connect/envoy/envoy.go +++ b/command/connect/envoy/envoy.go @@ -639,7 +639,7 @@ func (c *cmd) xdsAddress(httpCfg *api.Config) (GRPC, error) { addr := c.grpcAddr if addr == "" { - port, err := c.lookupXDSPort() + port, protocol, err := c.lookupXDSPort() if err != nil { c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) } @@ -648,7 +648,7 @@ func (c *cmd) xdsAddress(httpCfg *api.Config) (GRPC, error) { // enabled. port = 8502 } - addr = fmt.Sprintf("localhost:%v", port) + addr = fmt.Sprintf("%vlocalhost:%v", protocol, port) } // TODO: parse addr as a url instead of strings.HasPrefix/TrimPrefix @@ -697,39 +697,48 @@ func (c *cmd) xdsAddress(httpCfg *api.Config) (GRPC, error) { return g, nil } -func (c *cmd) lookupXDSPort() (int, error) { +func (c *cmd) lookupXDSPort() (int, string, error) { self, err := c.client.Agent().Self() if err != nil { - return 0, err + return 0, "", err } type response struct { XDS struct { - Port int + Ports struct { + Plaintext int + TLS int + } } } var resp response - if err := mapstructure.Decode(self, &resp); err == nil && resp.XDS.Port != 0 { - return resp.XDS.Port, nil + if err := mapstructure.Decode(self, &resp); err == nil { + if resp.XDS.Ports.TLS > 0 { + return resp.XDS.Ports.TLS, "https://", nil + } + if resp.XDS.Ports.Plaintext > 0 { + return resp.XDS.Ports.Plaintext, "http://", nil + } } // Fallback to old API for the case where a new consul CLI is being used with // an older API version. cfg, ok := self["DebugConfig"] if !ok { - return 0, fmt.Errorf("unexpected agent response: no debug config") + return 0, "", fmt.Errorf("unexpected agent response: no debug config") } + // TODO what does this mean? What did the old API look like? How does this affect compatibility? port, ok := cfg["GRPCPort"] if !ok { - return 0, fmt.Errorf("agent does not have grpc port enabled") + return 0, "", fmt.Errorf("agent does not have grpc port enabled") } portN, ok := port.(float64) if !ok { - return 0, fmt.Errorf("invalid grpc port in agent response") + return 0, "", fmt.Errorf("invalid grpc port in agent response") } - return int(portN), nil + return int(portN), "", nil } func (c *cmd) Synopsis() string { diff --git a/command/connect/envoy/envoy_test.go b/command/connect/envoy/envoy_test.go index 4eedd16d4..5236ee673 100644 --- a/command/connect/envoy/envoy_test.go +++ b/command/connect/envoy/envoy_test.go @@ -117,8 +117,8 @@ type generateConfigTestCase struct { Files map[string]string ProxyConfig map[string]interface{} NamespacesEnabled bool - XDSPort int // only used for testing custom-configured grpc port - AgentSelf110 bool // fake the agent API from versions v1.10 and earlier + XDSPorts agent.GRPCPorts // only used for testing custom-configured grpc port + AgentSelf110 bool // fake the agent API from versions v1.10 and earlier WantArgs BootstrapTplArgs WantErr string } @@ -447,9 +447,9 @@ func TestGenerateConfig(t *testing.T) { }, }, { - Name: "xds-addr-config", - Flags: []string{"-proxy-id", "test-proxy"}, - XDSPort: 9999, + Name: "xds-addr-config", + Flags: []string{"-proxy-id", "test-proxy"}, + XDSPorts: agent.GRPCPorts{Plaintext: 9999, TLS: 0}, WantArgs: BootstrapTplArgs{ ProxyCluster: "test-proxy", ProxyID: "test-proxy", @@ -470,10 +470,36 @@ func TestGenerateConfig(t *testing.T) { PrometheusScrapePath: "/metrics", }, }, + { + Name: "grpc-tls-addr-config", + Flags: []string{"-proxy-id", "test-proxy"}, + XDSPorts: agent.GRPCPorts{Plaintext: 9997, TLS: 9998}, + AgentSelf110: false, + WantArgs: BootstrapTplArgs{ + ProxyCluster: "test-proxy", + ProxyID: "test-proxy", + // We don't know this til after the lookup so it will be empty in the + // initial args call we are testing here. + ProxySourceService: "", + // Should resolve IP, note this might not resolve the same way + // everywhere which might make this test brittle but not sure what else + // to do. + GRPC: GRPC{ + AgentAddress: "127.0.0.1", + AgentPort: "9998", + AgentTLS: true, + }, + AdminAccessLogPath: "/dev/null", + AdminBindAddress: "127.0.0.1", + AdminBindPort: "19000", + LocalAgentClusterName: xds.LocalAgentClusterName, + PrometheusScrapePath: "/metrics", + }, + }, { Name: "deprecated-grpc-addr-config", Flags: []string{"-proxy-id", "test-proxy"}, - XDSPort: 9999, + XDSPorts: agent.GRPCPorts{Plaintext: 9999, TLS: 0}, AgentSelf110: true, WantArgs: BootstrapTplArgs{ ProxyCluster: "test-proxy", @@ -1138,7 +1164,7 @@ func testMockAgent(tc generateConfigTestCase) http.HandlerFunc { case strings.Contains(r.URL.Path, "/agent/service"): testMockAgentProxyConfig(tc.ProxyConfig, tc.NamespacesEnabled)(w, r) case strings.Contains(r.URL.Path, "/agent/self"): - testMockAgentSelf(tc.XDSPort, tc.AgentSelf110)(w, r) + testMockAgentSelf(tc.XDSPorts, tc.AgentSelf110)(w, r) case strings.Contains(r.URL.Path, "/catalog/node-services"): testMockCatalogNodeServiceList()(w, r) default: @@ -1378,7 +1404,7 @@ func TestEnvoyCommand_canBindInternal(t *testing.T) { // testMockAgentSelf returns an empty /v1/agent/self response except GRPC // port is filled in to match the given wantXDSPort argument. -func testMockAgentSelf(wantXDSPort int, agentSelf110 bool) http.HandlerFunc { +func testMockAgentSelf(wantXDSPorts agent.GRPCPorts, agentSelf110 bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { resp := agent.Self{ Config: map[string]interface{}{ @@ -1388,10 +1414,17 @@ func testMockAgentSelf(wantXDSPort int, agentSelf110 bool) http.HandlerFunc { if agentSelf110 { resp.DebugConfig = map[string]interface{}{ - "GRPCPort": wantXDSPort, + "GRPCPort": wantXDSPorts.Plaintext, } } else { - resp.XDS = &agent.XDSSelf{Port: wantXDSPort} + resp.XDS = &agent.XDSSelf{ + // The deprecated Port field should default to TLS if it's available. + Port: wantXDSPorts.TLS, + Ports: wantXDSPorts, + } + if wantXDSPorts.TLS <= 0 { + resp.XDS.Port = wantXDSPorts.Plaintext + } } selfJSON, err := json.Marshal(resp) diff --git a/command/connect/envoy/testdata/grpc-tls-addr-config.golden b/command/connect/envoy/testdata/grpc-tls-addr-config.golden new file mode 100644 index 000000000..e79cf0455 --- /dev/null +++ b/command/connect/envoy/testdata/grpc-tls-addr-config.golden @@ -0,0 +1,223 @@ +{ + "admin": { + "access_log_path": "/dev/null", + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 19000 + } + } + }, + "node": { + "cluster": "test", + "id": "test-proxy", + "metadata": { + "namespace": "default", + "partition": "default" + } + }, + "layered_runtime": { + "layers": [ + { + "name": "base", + "static_layer": { + "re2.max_program_size.error_level": 1048576 + } + } + ] + }, + "static_resources": { + "clusters": [ + { + "name": "local_agent", + "ignore_health_on_host_removal": false, + "connect_timeout": "1s", + "type": "STATIC", + "transport_socket": { + "name": "tls", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "common_tls_context": { + "validation_context": { + "trusted_ca": { + "inline_string": "" + } + } + } + } + }, + "http2_protocol_options": {}, + "loadAssignment": { + "clusterName": "local_agent", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socket_address": { + "address": "127.0.0.1", + "port_value": 9998 + } + } + } + } + ] + } + ] + } + } + ] + }, + "stats_config": { + "stats_tags": [ + { + "regex": "^cluster\\.(?:passthrough~)?((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.custom_hash" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service_subset" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.service" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.namespace" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:([^.]+)\\.)?[^.]+\\.internal[^.]*\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.partition" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.internal[^.]*\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.datacenter" + }, + { + "regex": "^cluster\\.([^.]+\\.(?:[^.]+\\.)?([^.]+)\\.external\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.peer" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.routing_type" + }, + { + "regex": "^cluster\\.(?:passthrough~)?((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.destination.trust_domain" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.destination.target" + }, + { + "regex": "^cluster\\.(?:passthrough~)?(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.destination.full_target" + }, + { + "regex": "^(?:tcp|http)\\.upstream(?:_peered)?\\.(([^.]+)(?:\\.[^.]+)?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.service" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.[^.]+)?\\.([^.]+)\\.)", + "tag_name": "consul.upstream.datacenter" + }, + { + "regex": "^(?:tcp|http)\\.upstream_peered\\.([^.]+(?:\\.[^.]+)?\\.([^.]+)\\.)", + "tag_name": "consul.upstream.peer" + }, + { + "regex": "^(?:tcp|http)\\.upstream(?:_peered)?\\.([^.]+(?:\\.([^.]+))?(?:\\.[^.]+)?\\.[^.]+\\.)", + "tag_name": "consul.upstream.namespace" + }, + { + "regex": "^(?:tcp|http)\\.upstream\\.([^.]+(?:\\.[^.]+)?(?:\\.([^.]+))?\\.[^.]+\\.)", + "tag_name": "consul.upstream.partition" + }, + { + "regex": "^cluster\\.((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.custom_hash" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service_subset" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.service" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.namespace" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?([^.]+)\\.internal[^.]*\\.[^.]+\\.consul\\.)", + "tag_name": "consul.datacenter" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)", + "tag_name": "consul.routing_type" + }, + { + "regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)", + "tag_name": "consul.trust_domain" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)", + "tag_name": "consul.target" + }, + { + "regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)", + "tag_name": "consul.full_target" + }, + { + "tag_name": "local_cluster", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.service", + "fixed_value": "test" + }, + { + "tag_name": "consul.source.namespace", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.partition", + "fixed_value": "default" + }, + { + "tag_name": "consul.source.datacenter", + "fixed_value": "dc1" + } + ], + "use_all_default_tags": true + }, + "dynamic_resources": { + "lds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "cds_config": { + "ads": {}, + "resource_api_version": "V3" + }, + "ads_config": { + "api_type": "DELTA_GRPC", + "transport_api_version": "V3", + "grpc_services": { + "initial_metadata": [ + { + "key": "x-consul-token", + "value": "" + } + ], + "envoy_grpc": { + "cluster_name": "local_agent" + } + } + } + } +} + diff --git a/command/connect/proxy/proxy.go b/command/connect/proxy/proxy.go index d2d0b90cf..a0477a6a1 100644 --- a/command/connect/proxy/proxy.go +++ b/command/connect/proxy/proxy.go @@ -232,7 +232,7 @@ func LookupProxyIDForSidecar(client *api.Client, sidecarFor string) (string, err var proxyIDs []string for _, svc := range svcs { if svc.Kind == api.ServiceKindConnectProxy && svc.Proxy != nil && - strings.ToLower(svc.Proxy.DestinationServiceID) == sidecarFor { + strings.EqualFold(svc.Proxy.DestinationServiceID, sidecarFor) { proxyIDs = append(proxyIDs, svc.ID) } } diff --git a/command/connect/proxy/proxy_test.go b/command/connect/proxy/proxy_test.go index ae7b1cdfb..28d5a9da2 100644 --- a/command/connect/proxy/proxy_test.go +++ b/command/connect/proxy/proxy_test.go @@ -110,6 +110,17 @@ func TestCommandConfigWatcher(t *testing.T) { require.Equal(t, 9999, cfg.PublicListener.BindPort) }, }, + + { + Name: "-sidecar-for, one sidecar case-insensitive", + Flags: []string{ + "-sidecar-for", "One-SideCar", + }, + Test: func(t *testing.T, cfg *proxy.Config) { + // Sanity check we got the right instance. + require.Equal(t, 9999, cfg.PublicListener.BindPort) + }, + }, } for _, tc := range cases { diff --git a/command/flags/http.go b/command/flags/http.go index 139ab7ed0..e82e024fb 100644 --- a/command/flags/http.go +++ b/command/flags/http.go @@ -98,6 +98,10 @@ func (f *HTTPFlags) Datacenter() string { return f.datacenter.String() } +func (f *HTTPFlags) Partition() string { + return f.partition.String() +} + func (f *HTTPFlags) Stale() bool { if f.stale.v == nil { return false diff --git a/command/kv/get/kv_get.go b/command/kv/get/kv_get.go index 099aedb9f..aa93ef963 100644 --- a/command/kv/get/kv_get.go +++ b/command/kv/get/kv_get.go @@ -99,6 +99,32 @@ func (c *cmd) Run(args []string) int { } switch { + case c.keys && c.recurse: + pairs, _, err := client.KV().List(key, &api.QueryOptions{ + AllowStale: c.http.Stale(), + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + for i, pair := range pairs { + if c.detailed { + var b bytes.Buffer + if err := prettyKVPair(&b, pair, false, true); err != nil { + c.UI.Error(fmt.Sprintf("Error rendering KV key: %s", err)) + return 1 + } + c.UI.Info(b.String()) + + if i < len(pairs)-1 { + c.UI.Info("") + } + } else { + c.UI.Info(fmt.Sprintf("%s", pair.Key)) + } + } + return 0 case c.keys: keys, _, err := client.KV().Keys(key, c.separator, &api.QueryOptions{ AllowStale: c.http.Stale(), @@ -125,7 +151,7 @@ func (c *cmd) Run(args []string) int { for i, pair := range pairs { if c.detailed { var b bytes.Buffer - if err := prettyKVPair(&b, pair, c.base64encode); err != nil { + if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil { c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) return 1 } @@ -161,7 +187,7 @@ func (c *cmd) Run(args []string) int { if c.detailed { var b bytes.Buffer - if err := prettyKVPair(&b, pair, c.base64encode); err != nil { + if err := prettyKVPair(&b, pair, c.base64encode, false); err != nil { c.UI.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) return 1 } @@ -187,7 +213,7 @@ func (c *cmd) Help() string { return c.help } -func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool) error { +func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool, keysOnly bool) error { tw := tabwriter.NewWriter(w, 0, 2, 6, ' ', 0) fmt.Fprintf(tw, "CreateIndex\t%d\n", pair.CreateIndex) fmt.Fprintf(tw, "Flags\t%d\n", pair.Flags) @@ -205,9 +231,9 @@ func prettyKVPair(w io.Writer, pair *api.KVPair, base64EncodeValue bool) error { if pair.Namespace != "" { fmt.Fprintf(tw, "Namespace\t%s\n", pair.Namespace) } - if base64EncodeValue { + if !keysOnly && base64EncodeValue { fmt.Fprintf(tw, "Value\t%s", base64.StdEncoding.EncodeToString(pair.Value)) - } else { + } else if !keysOnly { fmt.Fprintf(tw, "Value\t%s", pair.Value) } return tw.Flush() diff --git a/command/kv/get/kv_get_test.go b/command/kv/get/kv_get_test.go index 3a7b12d8a..5143391ef 100644 --- a/command/kv/get/kv_get_test.go +++ b/command/kv/get/kv_get_test.go @@ -418,3 +418,102 @@ func TestKVGetCommand_DetailedBase64(t *testing.T) { t.Fatalf("bad %#v, value is not base64 encoded", output) } } + +func TestKVGetCommand_KeysRecurse(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := agent.NewTestAgent(t, ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + keys := map[string]string{ + "foo/": "", + "foo/a": "Hello World 2", + "foo1/a": "Hello World 1", + } + for k, v := range keys { + var pair *api.KVPair + switch v { + case "": + pair = &api.KVPair{Key: k, Value: nil} + default: + pair = &api.KVPair{Key: k, Value: []byte(v)} + } + if _, err := client.KV().Put(pair, nil); err != nil { + t.Fatalf("err: %#v", err) + } + } + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-recurse", + "-keys", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := ui.OutputWriter.String() + for key, value := range keys { + if !strings.Contains(output, key) { + t.Fatalf("bad %#v missing %q", output, key) + } + if strings.Contains(output, key+":"+value) { + t.Fatalf("bad %#v expected no values for keys %q but received %q", output, key, value) + } + } +} +func TestKVGetCommand_DetailedKeysRecurse(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + a := agent.NewTestAgent(t, ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + keys := map[string]string{ + "foo/": "", + "foo/a": "Hello World 2", + "foo1/a": "Hello World 1", + } + for k, v := range keys { + var pair *api.KVPair + switch v { + case "": + pair = &api.KVPair{Key: k, Value: nil} + default: + pair = &api.KVPair{Key: k, Value: []byte(v)} + } + if _, err := client.KV().Put(pair, nil); err != nil { + t.Fatalf("err: %#v", err) + } + } + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-recurse", + "-keys", + "-detailed", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + output := ui.OutputWriter.String() + for key, value := range keys { + if value != "" && strings.Contains(output, value) { + t.Fatalf("bad %#v expected no values for keys %q but received %q", output, key, value) + } + } +} diff --git a/command/peering/delete/delete.go b/command/peering/delete/delete.go new file mode 100644 index 000000000..cb9818900 --- /dev/null +++ b/command/peering/delete/delete.go @@ -0,0 +1,91 @@ +package delete + +import ( + "context" + "flag" + "fmt" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + _, err = peerings.Delete(context.Background(), c.name, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error deleting peering for %s: %v", c.name, err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully submitted peering connection, %s, for deletion", c.name)) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Delete a peering connection" + help = ` +Usage: consul peering delete [options] -name + + Delete a peering connection. Consul deletes all data imported from the peer + in the background. The peering connection is removed after all associated + data has been deleted. Operators can still read the peering connections + while the data is being removed. A 'DeletedAt' field will be populated with + the timestamp of when the peering was marked for deletion. + + Example: + + $ consul peering delete -name west-dc +` +) diff --git a/command/peering/delete/delete_test.go b/command/peering/delete/delete_test.go new file mode 100644 index 000000000..984e773f5 --- /dev/null +++ b/command/peering/delete/delete_test.go @@ -0,0 +1,70 @@ +package delete + +import ( + "context" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestDeleteCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestDeleteCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("delete connection", func(t *testing.T) { + + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + }) +} diff --git a/command/peering/establish/establish.go b/command/peering/establish/establish.go new file mode 100644 index 000000000..14cd0e310 --- /dev/null +++ b/command/peering/establish/establish.go @@ -0,0 +1,109 @@ +package establish + +import ( + "context" + "flag" + "fmt" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + peeringToken string + meta map[string]string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.StringVar(&c.peeringToken, "peering-token", "", "(Required) The peering token from the accepting cluster.") + + c.flags.Var((*flags.FlagMapValue)(&c.meta), "meta", + "Metadata to associate with the peering, formatted as key=value. This flag "+ + "may be specified multiple times to set multiple meta fields.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if c.peeringToken == "" { + c.UI.Error("Missing the required -peering-token flag") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + req := api.PeeringEstablishRequest{ + PeerName: c.name, + PeeringToken: c.peeringToken, + Partition: c.http.Partition(), + Meta: c.meta, + } + + _, _, err = peerings.Establish(context.Background(), req, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error establishing peering for %s: %v", req.PeerName, err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully established peering connection with %s", req.PeerName)) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Establish a peering connection" + help = ` +Usage: consul peering establish [options] -name -peering-token + + Establish a peering connection. The name provided will be used locally by + this cluster to refer to the peering connection. The peering token can + only be used once to establish the connection. + + Example: + + $ consul peering establish -name west-dc -peering-token +` +) diff --git a/command/peering/establish/establish_test.go b/command/peering/establish/establish_test.go new file mode 100644 index 000000000..4d1d83e36 --- /dev/null +++ b/command/peering/establish/establish_test.go @@ -0,0 +1,127 @@ +package establish + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestEstablishCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestEstablishCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + dialer := agent.NewTestAgent(t, `datacenter = "dc2"`) + t.Cleanup(func() { _ = dialer.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + testrpc.WaitForTestAgent(t, dialer.RPC, "dc2") + + acceptingClient := acceptor.Client() + dialingClient := dialer.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-peering-token=1234abcde", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("peering token is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -peering-token flag") + }) + + t.Run("establish connection", func(t *testing.T) { + // Grab the token from the acceptor + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + fmt.Sprintf("-peering-token=%s", res.PeeringToken), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + }) + + t.Run("establish connection with options", func(t *testing.T) { + // Grab the token from the acceptor + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + fmt.Sprintf("-peering-token=%s", res.PeeringToken), + "-meta=env=production", + "-meta=region=us-west-1", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + + //Meta + peering, _, err := dialingClient.Peerings().Read(context.Background(), "bar", &api.QueryOptions{}) + require.NoError(t, err) + + actual, ok := peering.Meta["env"] + require.True(t, ok) + require.Equal(t, "production", actual) + + actual, ok = peering.Meta["region"] + require.True(t, ok) + require.Equal(t, "us-west-1", actual) + }) +} diff --git a/command/peering/generate/generate.go b/command/peering/generate/generate.go new file mode 100644 index 000000000..cbbb23009 --- /dev/null +++ b/command/peering/generate/generate.go @@ -0,0 +1,139 @@ +package generate + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + externalAddresses []string + meta map[string]string + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.Var((*flags.FlagMapValue)(&c.meta), "meta", + "Metadata to associate with the peering, formatted as key=value. This flag "+ + "may be specified multiple times to set multiple metadata fields.") + + c.flags.Var((*flags.AppendSliceValue)(&c.externalAddresses), "server-external-addresses", + "A list of addresses to put into the generated token, formatted as a comma-separate list. "+ + "Addresses are the form of :port. "+ + "This could be used to specify load balancer(s) or external IPs to reach the servers from "+ + "the dialing side, and will override any server addresses obtained from the \"consul\" service.") + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + req := api.PeeringGenerateTokenRequest{ + PeerName: c.name, + Partition: c.http.Partition(), + Meta: c.meta, + ServerExternalAddresses: c.externalAddresses, + } + + res, _, err := peerings.GenerateToken(context.Background(), req, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error generating peering token for %s: %v", req.PeerName, err)) + return 1 + } + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(res) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + c.UI.Info(res.PeeringToken) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Generate a peering token" + help = ` +Usage: consul peering generate-token [options] -name + + Generate a peering token. The name provided will be used locally by + this cluster to refer to the peering connection. Re-generating a token + for a given name will not interrupt any active connection, but will + invalidate any unused token for that name. + + Example: + + $ consul peering generate-token -name west-dc + + Example using a load balancer in front of Consul servers: + + $ consul peering generate-token -name west-dc -server-external-addresses load-balancer.elb.us-west-1.amazonaws.com:8502 +` +) diff --git a/command/peering/generate/generate_test.go b/command/peering/generate/generate_test.go new file mode 100644 index 000000000..c74597610 --- /dev/null +++ b/command/peering/generate/generate_test.go @@ -0,0 +1,141 @@ +package generate + +import ( + "context" + "encoding/base64" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestGenerateCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestGenerateCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = a.Shutdown() }) + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + client := a.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=foo", + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("generate token", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + token, err := base64.StdEncoding.DecodeString(ui.OutputWriter.String()) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + }) + + t.Run("generate token with options", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=bar", + "-server-external-addresses=1.2.3.4,5.6.7.8", + "-meta=env=production", + "-meta=region=us-east-1", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + token, err := base64.StdEncoding.DecodeString(ui.OutputWriter.String()) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + + //ServerExternalAddresses + require.Contains(t, string(token), "1.2.3.4") + require.Contains(t, string(token), "5.6.7.8") + + //Meta + peering, _, err := client.Peerings().Read(context.Background(), "bar", &api.QueryOptions{}) + require.NoError(t, err) + + actual, ok := peering.Meta["env"] + require.True(t, ok) + require.Equal(t, "production", actual) + + actual, ok = peering.Meta["region"] + require.True(t, ok) + require.Equal(t, "us-east-1", actual) + }) + + t.Run("read with json", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=baz", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputRes api.PeeringGenerateTokenResponse + require.NoError(t, json.Unmarshal(output, &outputRes)) + + token, err := base64.StdEncoding.DecodeString(outputRes.PeeringToken) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + }) +} diff --git a/command/peering/list/list.go b/command/peering/list/list.go new file mode 100644 index 000000000..c445e3d57 --- /dev/null +++ b/command/peering/list/list.go @@ -0,0 +1,139 @@ +package list + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "sort" + "strings" + + "github.com/mitchellh/cli" + "github.com/ryanuber/columnize" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + res, _, err := peerings.List(context.Background(), &api.QueryOptions{}) + if err != nil { + c.UI.Error("Error listing peerings") + return 1 + } + + list := peeringList(res) + sort.Sort(list) + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(list) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + if len(res) == 0 { + c.UI.Info(fmt.Sprintf("There are no peering connections.")) + return 0 + } + + result := make([]string, 0, len(list)) + header := "Name\x1fState\x1fImported Svcs\x1fExported Svcs\x1fMeta" + result = append(result, header) + for _, peer := range list { + metaPairs := make([]string, 0, len(peer.Meta)) + for k, v := range peer.Meta { + metaPairs = append(metaPairs, fmt.Sprintf("%s=%s", k, v)) + } + meta := strings.Join(metaPairs, ",") + line := fmt.Sprintf("%s\x1f%s\x1f%d\x1f%d\x1f%s", + peer.Name, peer.State, peer.ImportedServiceCount, peer.ExportedServiceCount, meta) + result = append(result, line) + } + + output := columnize.Format(result, &columnize.Config{Delim: string([]byte{0x1f})}) + c.UI.Output(output) + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "List peering connections" + help = ` +Usage: consul peering list [options] + + List all peering connections. The results will be filtered according + to ACL policy configuration. + + Example: + + $ consul peering list +` +) + +// peeringList applies sort.Interface to a list of peering connections for sorting by name. +type peeringList []*api.Peering + +func (d peeringList) Len() int { return len(d) } +func (d peeringList) Less(i, j int) bool { return d[i].Name < d[j].Name } +func (d peeringList) Swap(i, j int) { d[i], d[j] = d[j], d[i] } diff --git a/command/peering/list/list_test.go b/command/peering/list/list_test.go new file mode 100644 index 000000000..c015fc8e8 --- /dev/null +++ b/command/peering/list/list_test.go @@ -0,0 +1,133 @@ +package list + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestListCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestListCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("no results - pretty", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "no peering connections") + }) + + t.Run("no results - json", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "[]") + }) + + t.Run("two results for pretty print", func(t *testing.T) { + + generateReq := api.PeeringGenerateTokenRequest{PeerName: "foo"} + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") + + generateReq = api.PeeringGenerateTokenRequest{PeerName: "bar"} + _, _, err = acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"bar\"") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Equal(t, 3, strings.Count(output, "\n")) // There should be three lines including the header + + lines := strings.Split(output, "\n") + + require.Contains(t, lines[0], "Name") + require.Contains(t, lines[1], "bar") + require.Contains(t, lines[2], "foo") + }) + + t.Run("two results for JSON print", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputList []*api.Peering + require.NoError(t, json.Unmarshal(output, &outputList)) + + require.Len(t, outputList, 2) + require.Equal(t, "bar", outputList[0].Name) + require.Equal(t, "foo", outputList[1].Name) + }) +} diff --git a/command/peering/peering.go b/command/peering/peering.go new file mode 100644 index 000000000..1872f3738 --- /dev/null +++ b/command/peering/peering.go @@ -0,0 +1,69 @@ +package peering + +import ( + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/command/flags" +) + +const ( + PeeringFormatJSON = "json" + PeeringFormatPretty = "pretty" +) + +func GetSupportedFormats() []string { + return []string{PeeringFormatJSON, PeeringFormatPretty} +} + +func FormatIsValid(f string) bool { + return f == PeeringFormatPretty || f == PeeringFormatJSON +} + +func New() *cmd { + return &cmd{} +} + +type cmd struct{} + +func (c *cmd) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(help, nil) +} + +const synopsis = "Create and manage peering connections between Consul clusters" +const help = ` +Usage: consul peering [options] [args] + + This command has subcommands for interacting with Cluster Peering + connections. Here are some simple examples, and more detailed + examples are available in the subcommands or the documentation. + + Generate a peering token: + + $ consul peering generate-token -name west-dc + + Establish a peering connection: + + $ consul peering establish -name east-dc -peering-token + + List all the local peering connections: + + $ consul peering list + + Print the status of a peering connection: + + $ consul peering read -name west-dc + + Delete and close a peering connection: + + $ consul peering delete -name west-dc + + For more examples, ask for subcommand help or view the documentation. +` diff --git a/command/peering/read/read.go b/command/peering/read/read.go new file mode 100644 index 000000000..c8340e19b --- /dev/null +++ b/command/peering/read/read.go @@ -0,0 +1,164 @@ +package read + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "strings" + "time" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + res, _, err := peerings.Read(context.Background(), c.name, &api.QueryOptions{}) + if err != nil { + c.UI.Error("Error reading peerings") + return 1 + } + + if res == nil { + c.UI.Error(fmt.Sprintf("No peering with name %s found.", c.name)) + return 1 + } + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(res) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + c.UI.Output(formatPeering(res)) + + return 0 +} + +func formatPeering(peering *api.Peering) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("Name: %s\n", peering.Name)) + buffer.WriteString(fmt.Sprintf("ID: %s\n", peering.ID)) + if peering.Partition != "" { + buffer.WriteString(fmt.Sprintf("Partition: %s\n", peering.Partition)) + } + if peering.DeletedAt != nil { + buffer.WriteString(fmt.Sprintf("DeletedAt: %s\n", peering.DeletedAt.Format(time.RFC3339))) + } + buffer.WriteString(fmt.Sprintf("State: %s\n", peering.State)) + if peering.Meta != nil && len(peering.Meta) > 0 { + buffer.WriteString("Meta:\n") + for k, v := range peering.Meta { + buffer.WriteString(fmt.Sprintf(" %s=%s\n", k, v)) + } + } + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Peer ID: %s\n", peering.PeerID)) + buffer.WriteString(fmt.Sprintf("Peer Server Name: %s\n", peering.PeerServerName)) + buffer.WriteString(fmt.Sprintf("Peer CA Pems: %d\n", len(peering.PeerCAPems))) + if peering.PeerServerAddresses != nil && len(peering.PeerServerAddresses) > 0 { + buffer.WriteString("Peer Server Addresses:\n") + for _, v := range peering.PeerServerAddresses { + buffer.WriteString(fmt.Sprintf(" %s", v)) + } + } + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Imported Services: %d\n", peering.ImportedServiceCount)) + buffer.WriteString(fmt.Sprintf("Exported Services: %d\n", peering.ExportedServiceCount)) + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", peering.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", peering.ModifyIndex)) + + return buffer.String() +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Read a peering connection" + help = ` +Usage: consul peering read [options] -name + + Read a peering connection with the provided name. If one is not found, + the command will exit with a non-zero code. The result will be filtered according + to ACL policy configuration. + + Example: + + $ consul peering read -name west-dc +` +) diff --git a/command/peering/read/read_test.go b/command/peering/read/read_test.go new file mode 100644 index 000000000..fe19e1100 --- /dev/null +++ b/command/peering/read/read_test.go @@ -0,0 +1,135 @@ +package read + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestReadCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestReadCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("no name flag", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("peering does not exist", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "No peering with name") + }) + + t.Run("read with pretty print", func(t *testing.T) { + + generateReq := api.PeeringGenerateTokenRequest{ + PeerName: "foo", + Meta: map[string]string{ + "env": "production", + }, + } + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Greater(t, strings.Count(output, "\n"), 0) // Checking for some kind of empty output + + // Spot check some fields and values + require.Contains(t, output, "foo") + require.Contains(t, output, api.PeeringStatePending) + require.Contains(t, output, "env=production") + require.Contains(t, output, "Imported Services") + require.Contains(t, output, "Exported Services") + }) + + t.Run("read with json", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputPeering api.Peering + require.NoError(t, json.Unmarshal(output, &outputPeering)) + + require.Equal(t, "foo", outputPeering.Name) + require.Equal(t, "production", outputPeering.Meta["env"]) + }) +} diff --git a/command/registry.go b/command/registry.go index 28e441e87..b35ac2e42 100644 --- a/command/registry.go +++ b/command/registry.go @@ -96,6 +96,12 @@ import ( operraft "github.com/hashicorp/consul/command/operator/raft" operraftlist "github.com/hashicorp/consul/command/operator/raft/listpeers" operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer" + "github.com/hashicorp/consul/command/peering" + peerdelete "github.com/hashicorp/consul/command/peering/delete" + peerestablish "github.com/hashicorp/consul/command/peering/establish" + peergenerate "github.com/hashicorp/consul/command/peering/generate" + peerlist "github.com/hashicorp/consul/command/peering/list" + peerread "github.com/hashicorp/consul/command/peering/read" "github.com/hashicorp/consul/command/reload" "github.com/hashicorp/consul/command/rtt" "github.com/hashicorp/consul/command/services" @@ -214,6 +220,12 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory { entry{"operator raft", func(cli.Ui) (cli.Command, error) { return operraft.New(), nil }}, entry{"operator raft list-peers", func(ui cli.Ui) (cli.Command, error) { return operraftlist.New(ui), nil }}, entry{"operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil }}, + entry{"peering", func(cli.Ui) (cli.Command, error) { return peering.New(), nil }}, + entry{"peering delete", func(ui cli.Ui) (cli.Command, error) { return peerdelete.New(ui), nil }}, + entry{"peering generate-token", func(ui cli.Ui) (cli.Command, error) { return peergenerate.New(ui), nil }}, + entry{"peering establish", func(ui cli.Ui) (cli.Command, error) { return peerestablish.New(ui), nil }}, + entry{"peering list", func(ui cli.Ui) (cli.Command, error) { return peerlist.New(ui), nil }}, + entry{"peering read", func(ui cli.Ui) (cli.Command, error) { return peerread.New(ui), nil }}, entry{"reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }}, entry{"rtt", func(ui cli.Ui) (cli.Command, error) { return rtt.New(ui), nil }}, entry{"services", func(cli.Ui) (cli.Command, error) { return services.New(), nil }}, diff --git a/docs/README.md b/docs/README.md index 8c1148ec2..0c24ff8f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,10 @@ be found in the public [user documentation]. Also see the [FAQ](./faq.md). +## Other Docs + +1. [Integration Tests](../test/integration/connect/envoy/README.md) + ## Important Directories Most top level directories contain Go source code. The directories listed below diff --git a/go.mod b/go.mod index cb048763d..3c518b17b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ replace github.com/hashicorp/consul/api => ./api replace github.com/hashicorp/consul/sdk => ./sdk +replace github.com/hashicorp/consul/proto-public => ./proto-public + replace launchpad.net/gocheck => github.com/go-check/check v0.0.0-20140225173054-eb6ee6f84d0a require ( @@ -28,6 +30,7 @@ require ( github.com/hashicorp/consul-awsauth v0.0.0-20220713182709-05ac1c5c2706 github.com/hashicorp/consul-net-rpc v0.0.0-20220307172752-3602954411b4 github.com/hashicorp/consul/api v1.13.1 + github.com/hashicorp/consul/proto-public v0.1.0 github.com/hashicorp/consul/sdk v0.10.0 github.com/hashicorp/go-bexpr v0.1.2 github.com/hashicorp/go-checkpoint v0.5.0 @@ -45,15 +48,15 @@ require ( github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hil v0.0.0-20200423225030-a18a1cd20038 - github.com/hashicorp/memberlist v0.3.1 + github.com/hashicorp/memberlist v0.4.0 github.com/hashicorp/raft v1.3.9 github.com/hashicorp/raft-autopilot v0.1.6 github.com/hashicorp/raft-boltdb/v2 v2.2.2 - github.com/hashicorp/serf v0.9.8 + github.com/hashicorp/serf v0.10.0 github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 github.com/hashicorp/yamux v0.0.0-20210826001029-26ff87cf9493 - github.com/imdario/mergo v0.3.6 + github.com/imdario/mergo v0.3.13 github.com/kr/text v0.2.0 github.com/miekg/dns v1.1.41 github.com/mitchellh/cli v1.1.0 @@ -77,7 +80,7 @@ require ( golang.org/x/net v0.0.0-20211216030914-fe4d6282115f golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/genproto v0.0.0-20200623002339-fbb79eadd5eb google.golang.org/grpc v1.37.1 @@ -183,7 +186,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/resty.v1 v1.12.0 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect k8s.io/klog v1.0.0 // indirect k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 // indirect sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect diff --git a/go.sum b/go.sum index 8f2afaa45..cf7f2afc3 100644 --- a/go.sum +++ b/go.sum @@ -364,8 +364,9 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/memberlist v0.3.1 h1:MXgUXLqva1QvpVEDQW1IQLG0wivQAtmFlHRQ+1vWZfM= github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.4.0 h1:k3uda5gZcltmafuFF+UFqNEl5PrH+yPZ4zkjp1f/H/8= +github.com/hashicorp/memberlist v0.4.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= github.com/hashicorp/raft v1.1.1/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= github.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= @@ -380,8 +381,8 @@ github.com/hashicorp/raft-boltdb v0.0.0-20211202195631-7d34b9fb3f42/go.mod h1:wc github.com/hashicorp/raft-boltdb/v2 v2.2.2 h1:rlkPtOllgIcKLxVT4nutqlTH2NRFn+tO1wwZk/4Dxqw= github.com/hashicorp/raft-boltdb/v2 v2.2.2/go.mod h1:N8YgaZgNJLpZC+h+by7vDu5rzsRgONThTEeUS3zWbfY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/serf v0.9.8 h1:JGklO/2Drf1QGa312EieQN3zhxQ+aJg6pG+aC3MFaVo= -github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.10.0 h1:89qvvpfMQnz6c2y4pv7j2vUUmeT1+5TSZMexuTbtsPs= +github.com/hashicorp/serf v0.10.0/go.mod h1:bXN03oZc5xlH46k/K1qTrpXb9ERKyY1/i/N5mxvgrZw= github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086 h1:OKsyxKi2sNmqm1Gv93adf2AID2FOBFdCbbZn9fGtIdg= github.com/hashicorp/vault/api v1.0.5-0.20200717191844-f687267c8086/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 h1:e1ok06zGrWJW91rzRroyl5nRNqraaBe4d5hiKcVZuHM= @@ -395,8 +396,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= @@ -793,8 +795,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -967,8 +970,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/logging/names.go b/logging/names.go index 7f5e2bf60..5d1f60c9b 100644 --- a/logging/names.go +++ b/logging/names.go @@ -1,66 +1,67 @@ package logging const ( - ACL string = "acl" - Agent string = "agent" - AntiEntropy string = "anti_entropy" - AutoEncrypt string = "auto_encrypt" - AutoConfig string = "auto_config" - Autopilot string = "autopilot" - AWS string = "aws" - Azure string = "azure" - CA string = "ca" - Catalog string = "catalog" - CentralConfig string = "central_config" - ConfigEntry string = "config_entry" - Connect string = "connect" - Consul string = "consul" - ConsulClient string = "client" - ConsulServer string = "server" - Coordinate string = "coordinate" - DNS string = "dns" - Envoy string = "envoy" - FederationState string = "federation_state" - FSM string = "fsm" - GatewayLocator string = "gateway_locator" - HTTP string = "http" - IngressGateway string = "ingress_gateway" - Intentions string = "intentions" - Internal string = "internal" - KV string = "kvs" - LAN string = "lan" - Leader string = "leader" - Legacy string = "legacy" - License string = "license" - Manager string = "manager" - Memberlist string = "memberlist" - MeshGateway string = "mesh_gateway" - Namespace string = "namespace" - NetworkAreas string = "network_areas" - Operator string = "operator" - PreparedQuery string = "prepared_query" - Proxy string = "proxy" - ProxyConfig string = "proxycfg" - Raft string = "raft" - Replication string = "replication" - Router string = "router" - RPC string = "rpc" - Serf string = "serf" - Session string = "session" - Sentinel string = "sentinel" - Snapshot string = "snapshot" - Partition string = "partition" - Peering string = "peering" - PeeringMetrics string = "peering_metrics" - TerminatingGateway string = "terminating_gateway" - TLSUtil string = "tlsutil" - Transaction string = "txn" - UsageMetrics string = "usage_metrics" - UIServer string = "ui_server" - UIMetricsProxy string = "ui_metrics_proxy" - WAN string = "wan" - Watch string = "watch" - XDS string = "xds" - Vault string = "vault" - Health string = "health" + ACL string = "acl" + Agent string = "agent" + AntiEntropy string = "anti_entropy" + AutoEncrypt string = "auto_encrypt" + AutoConfig string = "auto_config" + Autopilot string = "autopilot" + AWS string = "aws" + Azure string = "azure" + CA string = "ca" + Catalog string = "catalog" + CentralConfig string = "central_config" + ConfigEntry string = "config_entry" + Connect string = "connect" + Consul string = "consul" + ConsulClient string = "client" + ConsulServer string = "server" + Coordinate string = "coordinate" + DNS string = "dns" + Envoy string = "envoy" + FederationState string = "federation_state" + FSM string = "fsm" + GatewayLocator string = "gateway_locator" + HTTP string = "http" + IngressGateway string = "ingress_gateway" + Intentions string = "intentions" + Internal string = "internal" + KV string = "kvs" + LAN string = "lan" + Leader string = "leader" + Legacy string = "legacy" + License string = "license" + Manager string = "manager" + Memberlist string = "memberlist" + MeshGateway string = "mesh_gateway" + Namespace string = "namespace" + NetworkAreas string = "network_areas" + Operator string = "operator" + PreparedQuery string = "prepared_query" + Proxy string = "proxy" + ProxyConfig string = "proxycfg" + Raft string = "raft" + Replication string = "replication" + Router string = "router" + RPC string = "rpc" + Serf string = "serf" + Session string = "session" + Sentinel string = "sentinel" + Snapshot string = "snapshot" + Partition string = "partition" + Peering string = "peering" + PeeringMetrics string = "peering_metrics" + TerminatingGateway string = "terminating_gateway" + TLSUtil string = "tlsutil" + Transaction string = "txn" + UsageMetrics string = "usage_metrics" + UIServer string = "ui_server" + UIMetricsProxy string = "ui_metrics_proxy" + WAN string = "wan" + Watch string = "watch" + XDS string = "xds" + XDSCapacityController string = "xds_capacity_controller" + Vault string = "vault" + Health string = "health" ) diff --git a/proto-public/go.mod b/proto-public/go.mod new file mode 100644 index 000000000..9870dccfb --- /dev/null +++ b/proto-public/go.mod @@ -0,0 +1,16 @@ +module github.com/hashicorp/consul/proto-public + +go 1.19 + +require ( + github.com/golang/protobuf v1.5.0 + google.golang.org/grpc v1.37.1 + google.golang.org/protobuf v1.27.1 +) + +require ( + golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect + golang.org/x/text v0.3.0 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect +) diff --git a/proto-public/go.sum b/proto-public/go.sum new file mode 100644 index 000000000..59e6e6727 --- /dev/null +++ b/proto-public/go.sum @@ -0,0 +1,88 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.37.1 h1:ARnQJNWxGyYJpdf/JXscNlQr/uv607ZPU9Z7ogHi+iI= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/proto-public/pbdataplane/dataplane.pb.go b/proto-public/pbdataplane/dataplane.pb.go index 1da1eea15..8e8a1000f 100644 --- a/proto-public/pbdataplane/dataplane.pb.go +++ b/proto-public/pbdataplane/dataplane.pb.go @@ -401,12 +401,17 @@ type GetEnvoyBootstrapParamsResponse struct { unknownFields protoimpl.UnknownFields ServiceKind ServiceKind `protobuf:"varint,1,opt,name=service_kind,json=serviceKind,proto3,enum=hashicorp.consul.dataplane.ServiceKind" json:"service_kind,omitempty"` - // The destination service name + // service is be used to identify the service (as the local cluster name and + // in metric tags). If the service is a connect proxy it will be the name of + // the proxy's destination service, for gateways it will be the gateway + // service's name. Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` Namespace string `protobuf:"bytes,3,opt,name=namespace,proto3" json:"namespace,omitempty"` Partition string `protobuf:"bytes,4,opt,name=partition,proto3" json:"partition,omitempty"` Datacenter string `protobuf:"bytes,5,opt,name=datacenter,proto3" json:"datacenter,omitempty"` Config *structpb.Struct `protobuf:"bytes,6,opt,name=config,proto3" json:"config,omitempty"` + NodeId string `protobuf:"bytes,7,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + NodeName string `protobuf:"bytes,8,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` } func (x *GetEnvoyBootstrapParamsResponse) Reset() { @@ -483,6 +488,20 @@ func (x *GetEnvoyBootstrapParamsResponse) GetConfig() *structpb.Struct { return nil } +func (x *GetEnvoyBootstrapParamsResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *GetEnvoyBootstrapParamsResponse) GetNodeName() string { + if x != nil { + return x.NodeName + } + return "" +} + var File_proto_public_pbdataplane_dataplane_proto protoreflect.FileDescriptor var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ @@ -525,7 +544,7 @@ var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x22, 0x94, + 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x22, 0xca, 0x02, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6b, 0x69, @@ -543,69 +562,73 @@ var file_proto_public_pbdataplane_dataplane_proto_rawDesc = []byte{ 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2a, 0xc7, 0x01, 0x0a, 0x11, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, - 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x44, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x1b, + 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x2a, 0xc7, 0x01, 0x0a, 0x11, + 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, + 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, + 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x57, 0x41, 0x54, 0x43, + 0x48, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x32, 0x0a, 0x2e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, - 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x24, 0x0a, 0x20, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, - 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x57, 0x41, 0x54, 0x43, 0x48, 0x5f, 0x53, 0x45, 0x52, 0x56, - 0x45, 0x52, 0x53, 0x10, 0x01, 0x12, 0x32, 0x0a, 0x2e, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, - 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x45, 0x44, 0x47, 0x45, - 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, - 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x34, 0x0a, 0x30, 0x44, 0x41, 0x54, - 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, - 0x45, 0x4e, 0x56, 0x4f, 0x59, 0x5f, 0x42, 0x4f, 0x4f, 0x54, 0x53, 0x54, 0x52, 0x41, 0x50, 0x5f, - 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, - 0xcc, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, - 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, - 0x14, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x59, - 0x50, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x45, 0x52, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, - 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x45, 0x52, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x45, 0x53, 0x48, 0x5f, 0x47, 0x41, 0x54, - 0x45, 0x57, 0x41, 0x59, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x49, - 0x4e, 0x47, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x04, 0x12, 0x20, 0x0a, 0x1c, - 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x49, 0x4e, 0x47, - 0x52, 0x45, 0x53, 0x53, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x05, 0x32, 0xd2, - 0x02, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, - 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, - 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, - 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, - 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, - 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x94, 0x01, 0x0a, - 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, - 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, - 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, - 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, - 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0xf0, 0x01, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, - 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x42, 0x0e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x2f, 0x70, 0x62, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xa2, 0x02, - 0x03, 0x48, 0x43, 0x44, 0xaa, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, - 0x65, 0xca, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xe2, 0x02, - 0x26, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1c, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x44, 0x61, 0x74, - 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x53, 0x5f, 0x45, 0x44, 0x47, 0x45, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, + 0x54, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, + 0x34, 0x0a, 0x30, 0x44, 0x41, 0x54, 0x41, 0x50, 0x4c, 0x41, 0x4e, 0x45, 0x5f, 0x46, 0x45, 0x41, + 0x54, 0x55, 0x52, 0x45, 0x53, 0x5f, 0x45, 0x4e, 0x56, 0x4f, 0x59, 0x5f, 0x42, 0x4f, 0x4f, 0x54, + 0x53, 0x54, 0x52, 0x41, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, 0xcc, 0x01, 0x0a, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, + 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, + 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x59, 0x50, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x1e, 0x0a, + 0x1a, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x43, 0x4f, + 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x5f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x02, 0x12, 0x1d, 0x0a, + 0x19, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x45, + 0x53, 0x48, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, + 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x54, 0x45, 0x52, + 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x49, 0x4e, 0x47, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, 0x41, 0x59, + 0x10, 0x04, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4b, 0x49, + 0x4e, 0x44, 0x5f, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x47, 0x41, 0x54, 0x45, 0x57, + 0x41, 0x59, 0x10, 0x05, 0x32, 0xd2, 0x02, 0x0a, 0x10, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, + 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa6, 0x01, 0x0a, 0x1d, 0x47, 0x65, + 0x74, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x40, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, + 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x41, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x75, + 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x94, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, 0x42, + 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x3a, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, + 0x6e, 0x76, 0x6f, 0x79, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3b, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x64, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x76, 0x6f, 0x79, + 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xf0, 0x01, 0x0a, 0x1e, 0x63, 0x6f, + 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x42, 0x0e, 0x44, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x34, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x64, 0x61, 0x74, 0x61, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0xa2, 0x02, 0x03, 0x48, 0x43, 0x44, 0xaa, 0x02, 0x1a, 0x48, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x44, 0x61, + 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0xca, 0x02, 0x1a, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, + 0x6c, 0x61, 0x6e, 0x65, 0xe2, 0x02, 0x26, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1c, + 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x3a, 0x3a, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto-public/pbdataplane/dataplane.proto b/proto-public/pbdataplane/dataplane.proto index 0502dcd70..cc95f3a51 100644 --- a/proto-public/pbdataplane/dataplane.proto +++ b/proto-public/pbdataplane/dataplane.proto @@ -68,12 +68,17 @@ enum ServiceKind { message GetEnvoyBootstrapParamsResponse { ServiceKind service_kind = 1; - // The destination service name + // service is be used to identify the service (as the local cluster name and + // in metric tags). If the service is a connect proxy it will be the name of + // the proxy's destination service, for gateways it will be the gateway + // service's name. string service = 2; string namespace = 3; string partition = 4; string datacenter = 5; google.protobuf.Struct config = 6; + string node_id = 7; + string node_name = 8; } service DataplaneService { diff --git a/proto/pbconfigentry/config_entry.gen.go b/proto/pbconfigentry/config_entry.gen.go index 9f4d9fa75..540c95622 100644 --- a/proto/pbconfigentry/config_entry.gen.go +++ b/proto/pbconfigentry/config_entry.gen.go @@ -415,6 +415,11 @@ func MeshConfigToStructs(s *MeshConfig, t *structs.MeshConfigEntry) { MeshHTTPConfigToStructs(s.HTTP, &x) t.HTTP = &x } + if s.Peering != nil { + var x structs.PeeringMeshConfig + PeeringMeshConfigToStructs(s.Peering, &x) + t.Peering = &x + } t.Meta = s.Meta } func MeshConfigFromStructs(t *structs.MeshConfigEntry, s *MeshConfig) { @@ -436,6 +441,11 @@ func MeshConfigFromStructs(t *structs.MeshConfigEntry, s *MeshConfig) { MeshHTTPConfigFromStructs(t.HTTP, &x) s.HTTP = &x } + if t.Peering != nil { + var x PeeringMeshConfig + PeeringMeshConfigFromStructs(t.Peering, &x) + s.Peering = &x + } s.Meta = t.Meta } func MeshDirectionalTLSConfigToStructs(s *MeshDirectionalTLSConfig, t *structs.MeshDirectionalTLSConfig) { @@ -496,6 +506,18 @@ func MeshTLSConfigFromStructs(t *structs.MeshTLSConfig, s *MeshTLSConfig) { s.Outgoing = &x } } +func PeeringMeshConfigToStructs(s *PeeringMeshConfig, t *structs.PeeringMeshConfig) { + if s == nil { + return + } + t.PeerThroughMeshGateways = s.PeerThroughMeshGateways +} +func PeeringMeshConfigFromStructs(t *structs.PeeringMeshConfig, s *PeeringMeshConfig) { + if s == nil { + return + } + s.PeerThroughMeshGateways = t.PeerThroughMeshGateways +} func RingHashConfigToStructs(s *RingHashConfig, t *structs.RingHashConfig) { if s == nil { return @@ -630,6 +652,14 @@ func ServiceResolverFailoverToStructs(s *ServiceResolverFailover, t *structs.Ser t.ServiceSubset = s.ServiceSubset t.Namespace = s.Namespace t.Datacenters = s.Datacenters + { + t.Targets = make([]structs.ServiceResolverFailoverTarget, len(s.Targets)) + for i := range s.Targets { + if s.Targets[i] != nil { + ServiceResolverFailoverTargetToStructs(s.Targets[i], &t.Targets[i]) + } + } + } } func ServiceResolverFailoverFromStructs(t *structs.ServiceResolverFailover, s *ServiceResolverFailover) { if s == nil { @@ -639,6 +669,38 @@ func ServiceResolverFailoverFromStructs(t *structs.ServiceResolverFailover, s *S s.ServiceSubset = t.ServiceSubset s.Namespace = t.Namespace s.Datacenters = t.Datacenters + { + s.Targets = make([]*ServiceResolverFailoverTarget, len(t.Targets)) + for i := range t.Targets { + { + var x ServiceResolverFailoverTarget + ServiceResolverFailoverTargetFromStructs(&t.Targets[i], &x) + s.Targets[i] = &x + } + } + } +} +func ServiceResolverFailoverTargetToStructs(s *ServiceResolverFailoverTarget, t *structs.ServiceResolverFailoverTarget) { + if s == nil { + return + } + t.Service = s.Service + t.ServiceSubset = s.ServiceSubset + t.Partition = s.Partition + t.Namespace = s.Namespace + t.Datacenter = s.Datacenter + t.Peer = s.Peer +} +func ServiceResolverFailoverTargetFromStructs(t *structs.ServiceResolverFailoverTarget, s *ServiceResolverFailoverTarget) { + if s == nil { + return + } + s.Service = t.Service + s.ServiceSubset = t.ServiceSubset + s.Partition = t.Partition + s.Namespace = t.Namespace + s.Datacenter = t.Datacenter + s.Peer = t.Peer } func ServiceResolverRedirectToStructs(s *ServiceResolverRedirect, t *structs.ServiceResolverRedirect) { if s == nil { @@ -649,6 +711,7 @@ func ServiceResolverRedirectToStructs(s *ServiceResolverRedirect, t *structs.Ser t.Namespace = s.Namespace t.Partition = s.Partition t.Datacenter = s.Datacenter + t.Peer = s.Peer } func ServiceResolverRedirectFromStructs(t *structs.ServiceResolverRedirect, s *ServiceResolverRedirect) { if s == nil { @@ -659,6 +722,7 @@ func ServiceResolverRedirectFromStructs(t *structs.ServiceResolverRedirect, s *S s.Namespace = t.Namespace s.Partition = t.Partition s.Datacenter = t.Datacenter + s.Peer = t.Peer } func ServiceResolverSubsetToStructs(s *ServiceResolverSubset, t *structs.ServiceResolverSubset) { if s == nil { diff --git a/proto/pbconfigentry/config_entry.pb.binary.go b/proto/pbconfigentry/config_entry.pb.binary.go index 5ea1657a5..1fccef2b9 100644 --- a/proto/pbconfigentry/config_entry.pb.binary.go +++ b/proto/pbconfigentry/config_entry.pb.binary.go @@ -67,6 +67,16 @@ func (msg *MeshHTTPConfig) UnmarshalBinary(b []byte) error { return proto.Unmarshal(b, msg) } +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *PeeringMeshConfig) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *PeeringMeshConfig) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + // MarshalBinary implements encoding.BinaryMarshaler func (msg *ServiceResolver) MarshalBinary() ([]byte, error) { return proto.Marshal(msg) @@ -107,6 +117,16 @@ func (msg *ServiceResolverFailover) UnmarshalBinary(b []byte) error { return proto.Unmarshal(b, msg) } +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *ServiceResolverFailoverTarget) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *ServiceResolverFailoverTarget) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + // MarshalBinary implements encoding.BinaryMarshaler func (msg *LoadBalancer) MarshalBinary() ([]byte, error) { return proto.Marshal(msg) diff --git a/proto/pbconfigentry/config_entry.pb.go b/proto/pbconfigentry/config_entry.pb.go index 9a34e8248..b58b29be4 100644 --- a/proto/pbconfigentry/config_entry.pb.go +++ b/proto/pbconfigentry/config_entry.pb.go @@ -322,6 +322,7 @@ type MeshConfig struct { TLS *MeshTLSConfig `protobuf:"bytes,2,opt,name=TLS,proto3" json:"TLS,omitempty"` HTTP *MeshHTTPConfig `protobuf:"bytes,3,opt,name=HTTP,proto3" json:"HTTP,omitempty"` Meta map[string]string `protobuf:"bytes,4,rep,name=Meta,proto3" json:"Meta,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Peering *PeeringMeshConfig `protobuf:"bytes,5,opt,name=Peering,proto3" json:"Peering,omitempty"` } func (x *MeshConfig) Reset() { @@ -384,6 +385,13 @@ func (x *MeshConfig) GetMeta() map[string]string { return nil } +func (x *MeshConfig) GetPeering() *PeeringMeshConfig { + if x != nil { + return x.Peering + } + return nil +} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.TransparentProxyMeshConfig @@ -619,6 +627,58 @@ func (x *MeshHTTPConfig) GetSanitizeXForwardedClientCert() bool { return false } +// mog annotation: +// +// target=github.com/hashicorp/consul/agent/structs.PeeringMeshConfig +// output=config_entry.gen.go +// name=Structs +type PeeringMeshConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PeerThroughMeshGateways bool `protobuf:"varint,1,opt,name=PeerThroughMeshGateways,proto3" json:"PeerThroughMeshGateways,omitempty"` +} + +func (x *PeeringMeshConfig) Reset() { + *x = PeeringMeshConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeeringMeshConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeeringMeshConfig) ProtoMessage() {} + +func (x *PeeringMeshConfig) ProtoReflect() protoreflect.Message { + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeeringMeshConfig.ProtoReflect.Descriptor instead. +func (*PeeringMeshConfig) Descriptor() ([]byte, []int) { + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{6} +} + +func (x *PeeringMeshConfig) GetPeerThroughMeshGateways() bool { + if x != nil { + return x.PeerThroughMeshGateways + } + return false +} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.ServiceResolverConfigEntry @@ -643,7 +703,7 @@ type ServiceResolver struct { func (x *ServiceResolver) Reset() { *x = ServiceResolver{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[6] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -656,7 +716,7 @@ func (x *ServiceResolver) String() string { func (*ServiceResolver) ProtoMessage() {} func (x *ServiceResolver) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[6] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -669,7 +729,7 @@ func (x *ServiceResolver) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceResolver.ProtoReflect.Descriptor instead. func (*ServiceResolver) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{6} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{7} } func (x *ServiceResolver) GetDefaultSubset() string { @@ -738,7 +798,7 @@ type ServiceResolverSubset struct { func (x *ServiceResolverSubset) Reset() { *x = ServiceResolverSubset{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[7] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -751,7 +811,7 @@ func (x *ServiceResolverSubset) String() string { func (*ServiceResolverSubset) ProtoMessage() {} func (x *ServiceResolverSubset) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[7] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -764,7 +824,7 @@ func (x *ServiceResolverSubset) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceResolverSubset.ProtoReflect.Descriptor instead. func (*ServiceResolverSubset) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{7} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{8} } func (x *ServiceResolverSubset) GetFilter() string { @@ -796,12 +856,13 @@ type ServiceResolverRedirect struct { Namespace string `protobuf:"bytes,3,opt,name=Namespace,proto3" json:"Namespace,omitempty"` Partition string `protobuf:"bytes,4,opt,name=Partition,proto3" json:"Partition,omitempty"` Datacenter string `protobuf:"bytes,5,opt,name=Datacenter,proto3" json:"Datacenter,omitempty"` + Peer string `protobuf:"bytes,6,opt,name=Peer,proto3" json:"Peer,omitempty"` } func (x *ServiceResolverRedirect) Reset() { *x = ServiceResolverRedirect{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[8] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -814,7 +875,7 @@ func (x *ServiceResolverRedirect) String() string { func (*ServiceResolverRedirect) ProtoMessage() {} func (x *ServiceResolverRedirect) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[8] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -827,7 +888,7 @@ func (x *ServiceResolverRedirect) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceResolverRedirect.ProtoReflect.Descriptor instead. func (*ServiceResolverRedirect) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{8} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{9} } func (x *ServiceResolverRedirect) GetService() string { @@ -865,6 +926,13 @@ func (x *ServiceResolverRedirect) GetDatacenter() string { return "" } +func (x *ServiceResolverRedirect) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.ServiceResolverFailover @@ -875,16 +943,17 @@ type ServiceResolverFailover struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Service string `protobuf:"bytes,1,opt,name=Service,proto3" json:"Service,omitempty"` - ServiceSubset string `protobuf:"bytes,2,opt,name=ServiceSubset,proto3" json:"ServiceSubset,omitempty"` - Namespace string `protobuf:"bytes,3,opt,name=Namespace,proto3" json:"Namespace,omitempty"` - Datacenters []string `protobuf:"bytes,4,rep,name=Datacenters,proto3" json:"Datacenters,omitempty"` + Service string `protobuf:"bytes,1,opt,name=Service,proto3" json:"Service,omitempty"` + ServiceSubset string `protobuf:"bytes,2,opt,name=ServiceSubset,proto3" json:"ServiceSubset,omitempty"` + Namespace string `protobuf:"bytes,3,opt,name=Namespace,proto3" json:"Namespace,omitempty"` + Datacenters []string `protobuf:"bytes,4,rep,name=Datacenters,proto3" json:"Datacenters,omitempty"` + Targets []*ServiceResolverFailoverTarget `protobuf:"bytes,5,rep,name=Targets,proto3" json:"Targets,omitempty"` } func (x *ServiceResolverFailover) Reset() { *x = ServiceResolverFailover{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[9] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -897,7 +966,7 @@ func (x *ServiceResolverFailover) String() string { func (*ServiceResolverFailover) ProtoMessage() {} func (x *ServiceResolverFailover) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[9] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -910,7 +979,7 @@ func (x *ServiceResolverFailover) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceResolverFailover.ProtoReflect.Descriptor instead. func (*ServiceResolverFailover) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{9} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{10} } func (x *ServiceResolverFailover) GetService() string { @@ -941,6 +1010,105 @@ func (x *ServiceResolverFailover) GetDatacenters() []string { return nil } +func (x *ServiceResolverFailover) GetTargets() []*ServiceResolverFailoverTarget { + if x != nil { + return x.Targets + } + return nil +} + +// mog annotation: +// +// target=github.com/hashicorp/consul/agent/structs.ServiceResolverFailoverTarget +// output=config_entry.gen.go +// name=Structs +type ServiceResolverFailoverTarget struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service string `protobuf:"bytes,1,opt,name=Service,proto3" json:"Service,omitempty"` + ServiceSubset string `protobuf:"bytes,2,opt,name=ServiceSubset,proto3" json:"ServiceSubset,omitempty"` + Partition string `protobuf:"bytes,3,opt,name=Partition,proto3" json:"Partition,omitempty"` + Namespace string `protobuf:"bytes,4,opt,name=Namespace,proto3" json:"Namespace,omitempty"` + Datacenter string `protobuf:"bytes,5,opt,name=Datacenter,proto3" json:"Datacenter,omitempty"` + Peer string `protobuf:"bytes,6,opt,name=Peer,proto3" json:"Peer,omitempty"` +} + +func (x *ServiceResolverFailoverTarget) Reset() { + *x = ServiceResolverFailoverTarget{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServiceResolverFailoverTarget) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceResolverFailoverTarget) ProtoMessage() {} + +func (x *ServiceResolverFailoverTarget) ProtoReflect() protoreflect.Message { + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceResolverFailoverTarget.ProtoReflect.Descriptor instead. +func (*ServiceResolverFailoverTarget) Descriptor() ([]byte, []int) { + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{11} +} + +func (x *ServiceResolverFailoverTarget) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *ServiceResolverFailoverTarget) GetServiceSubset() string { + if x != nil { + return x.ServiceSubset + } + return "" +} + +func (x *ServiceResolverFailoverTarget) GetPartition() string { + if x != nil { + return x.Partition + } + return "" +} + +func (x *ServiceResolverFailoverTarget) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ServiceResolverFailoverTarget) GetDatacenter() string { + if x != nil { + return x.Datacenter + } + return "" +} + +func (x *ServiceResolverFailoverTarget) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.LoadBalancer @@ -960,7 +1128,7 @@ type LoadBalancer struct { func (x *LoadBalancer) Reset() { *x = LoadBalancer{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[10] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -973,7 +1141,7 @@ func (x *LoadBalancer) String() string { func (*LoadBalancer) ProtoMessage() {} func (x *LoadBalancer) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[10] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -986,7 +1154,7 @@ func (x *LoadBalancer) ProtoReflect() protoreflect.Message { // Deprecated: Use LoadBalancer.ProtoReflect.Descriptor instead. func (*LoadBalancer) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{10} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{12} } func (x *LoadBalancer) GetPolicy() string { @@ -1034,7 +1202,7 @@ type RingHashConfig struct { func (x *RingHashConfig) Reset() { *x = RingHashConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[11] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1047,7 +1215,7 @@ func (x *RingHashConfig) String() string { func (*RingHashConfig) ProtoMessage() {} func (x *RingHashConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[11] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1060,7 +1228,7 @@ func (x *RingHashConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use RingHashConfig.ProtoReflect.Descriptor instead. func (*RingHashConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{11} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{13} } func (x *RingHashConfig) GetMinimumRingSize() uint64 { @@ -1093,7 +1261,7 @@ type LeastRequestConfig struct { func (x *LeastRequestConfig) Reset() { *x = LeastRequestConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[12] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1106,7 +1274,7 @@ func (x *LeastRequestConfig) String() string { func (*LeastRequestConfig) ProtoMessage() {} func (x *LeastRequestConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[12] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1119,7 +1287,7 @@ func (x *LeastRequestConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use LeastRequestConfig.ProtoReflect.Descriptor instead. func (*LeastRequestConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{12} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{14} } func (x *LeastRequestConfig) GetChoiceCount() uint32 { @@ -1149,7 +1317,7 @@ type HashPolicy struct { func (x *HashPolicy) Reset() { *x = HashPolicy{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[13] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1162,7 +1330,7 @@ func (x *HashPolicy) String() string { func (*HashPolicy) ProtoMessage() {} func (x *HashPolicy) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[13] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1175,7 +1343,7 @@ func (x *HashPolicy) ProtoReflect() protoreflect.Message { // Deprecated: Use HashPolicy.ProtoReflect.Descriptor instead. func (*HashPolicy) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{13} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{15} } func (x *HashPolicy) GetField() string { @@ -1232,7 +1400,7 @@ type CookieConfig struct { func (x *CookieConfig) Reset() { *x = CookieConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[14] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1245,7 +1413,7 @@ func (x *CookieConfig) String() string { func (*CookieConfig) ProtoMessage() {} func (x *CookieConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[14] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1258,7 +1426,7 @@ func (x *CookieConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CookieConfig.ProtoReflect.Descriptor instead. func (*CookieConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{14} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{16} } func (x *CookieConfig) GetSession() bool { @@ -1301,7 +1469,7 @@ type IngressGateway struct { func (x *IngressGateway) Reset() { *x = IngressGateway{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[15] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1314,7 +1482,7 @@ func (x *IngressGateway) String() string { func (*IngressGateway) ProtoMessage() {} func (x *IngressGateway) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[15] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1327,7 +1495,7 @@ func (x *IngressGateway) ProtoReflect() protoreflect.Message { // Deprecated: Use IngressGateway.ProtoReflect.Descriptor instead. func (*IngressGateway) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{15} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{17} } func (x *IngressGateway) GetTLS() *GatewayTLSConfig { @@ -1374,7 +1542,7 @@ type GatewayTLSConfig struct { func (x *GatewayTLSConfig) Reset() { *x = GatewayTLSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[16] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1387,7 +1555,7 @@ func (x *GatewayTLSConfig) String() string { func (*GatewayTLSConfig) ProtoMessage() {} func (x *GatewayTLSConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[16] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1400,7 +1568,7 @@ func (x *GatewayTLSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayTLSConfig.ProtoReflect.Descriptor instead. func (*GatewayTLSConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{16} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{18} } func (x *GatewayTLSConfig) GetEnabled() bool { @@ -1455,7 +1623,7 @@ type GatewayTLSSDSConfig struct { func (x *GatewayTLSSDSConfig) Reset() { *x = GatewayTLSSDSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[17] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1468,7 +1636,7 @@ func (x *GatewayTLSSDSConfig) String() string { func (*GatewayTLSSDSConfig) ProtoMessage() {} func (x *GatewayTLSSDSConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[17] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1481,7 +1649,7 @@ func (x *GatewayTLSSDSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayTLSSDSConfig.ProtoReflect.Descriptor instead. func (*GatewayTLSSDSConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{17} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{19} } func (x *GatewayTLSSDSConfig) GetClusterName() string { @@ -1518,7 +1686,7 @@ type IngressListener struct { func (x *IngressListener) Reset() { *x = IngressListener{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[18] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1531,7 +1699,7 @@ func (x *IngressListener) String() string { func (*IngressListener) ProtoMessage() {} func (x *IngressListener) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[18] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1544,7 +1712,7 @@ func (x *IngressListener) ProtoReflect() protoreflect.Message { // Deprecated: Use IngressListener.ProtoReflect.Descriptor instead. func (*IngressListener) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{18} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{20} } func (x *IngressListener) GetPort() int32 { @@ -1598,7 +1766,7 @@ type IngressService struct { func (x *IngressService) Reset() { *x = IngressService{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[19] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1611,7 +1779,7 @@ func (x *IngressService) String() string { func (*IngressService) ProtoMessage() {} func (x *IngressService) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[19] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1624,7 +1792,7 @@ func (x *IngressService) ProtoReflect() protoreflect.Message { // Deprecated: Use IngressService.ProtoReflect.Descriptor instead. func (*IngressService) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{19} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{21} } func (x *IngressService) GetName() string { @@ -1692,7 +1860,7 @@ type GatewayServiceTLSConfig struct { func (x *GatewayServiceTLSConfig) Reset() { *x = GatewayServiceTLSConfig{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[20] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1705,7 +1873,7 @@ func (x *GatewayServiceTLSConfig) String() string { func (*GatewayServiceTLSConfig) ProtoMessage() {} func (x *GatewayServiceTLSConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[20] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1718,7 +1886,7 @@ func (x *GatewayServiceTLSConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayServiceTLSConfig.ProtoReflect.Descriptor instead. func (*GatewayServiceTLSConfig) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{20} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{22} } func (x *GatewayServiceTLSConfig) GetSDS() *GatewayTLSSDSConfig { @@ -1746,7 +1914,7 @@ type HTTPHeaderModifiers struct { func (x *HTTPHeaderModifiers) Reset() { *x = HTTPHeaderModifiers{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[21] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1759,7 +1927,7 @@ func (x *HTTPHeaderModifiers) String() string { func (*HTTPHeaderModifiers) ProtoMessage() {} func (x *HTTPHeaderModifiers) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[21] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1772,7 +1940,7 @@ func (x *HTTPHeaderModifiers) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPHeaderModifiers.ProtoReflect.Descriptor instead. func (*HTTPHeaderModifiers) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{21} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{23} } func (x *HTTPHeaderModifiers) GetAdd() map[string]string { @@ -1814,7 +1982,7 @@ type ServiceIntentions struct { func (x *ServiceIntentions) Reset() { *x = ServiceIntentions{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[22] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1827,7 +1995,7 @@ func (x *ServiceIntentions) String() string { func (*ServiceIntentions) ProtoMessage() {} func (x *ServiceIntentions) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[22] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1840,7 +2008,7 @@ func (x *ServiceIntentions) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceIntentions.ProtoReflect.Descriptor instead. func (*ServiceIntentions) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{22} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{24} } func (x *ServiceIntentions) GetSources() []*SourceIntention { @@ -1890,7 +2058,7 @@ type SourceIntention struct { func (x *SourceIntention) Reset() { *x = SourceIntention{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[23] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1903,7 +2071,7 @@ func (x *SourceIntention) String() string { func (*SourceIntention) ProtoMessage() {} func (x *SourceIntention) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[23] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1916,7 +2084,7 @@ func (x *SourceIntention) ProtoReflect() protoreflect.Message { // Deprecated: Use SourceIntention.ProtoReflect.Descriptor instead. func (*SourceIntention) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{23} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{25} } func (x *SourceIntention) GetName() string { @@ -2021,7 +2189,7 @@ type IntentionPermission struct { func (x *IntentionPermission) Reset() { *x = IntentionPermission{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[24] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2034,7 +2202,7 @@ func (x *IntentionPermission) String() string { func (*IntentionPermission) ProtoMessage() {} func (x *IntentionPermission) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[24] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2047,7 +2215,7 @@ func (x *IntentionPermission) ProtoReflect() protoreflect.Message { // Deprecated: Use IntentionPermission.ProtoReflect.Descriptor instead. func (*IntentionPermission) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{24} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{26} } func (x *IntentionPermission) GetAction() IntentionAction { @@ -2084,7 +2252,7 @@ type IntentionHTTPPermission struct { func (x *IntentionHTTPPermission) Reset() { *x = IntentionHTTPPermission{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[25] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2097,7 +2265,7 @@ func (x *IntentionHTTPPermission) String() string { func (*IntentionHTTPPermission) ProtoMessage() {} func (x *IntentionHTTPPermission) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[25] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2110,7 +2278,7 @@ func (x *IntentionHTTPPermission) ProtoReflect() protoreflect.Message { // Deprecated: Use IntentionHTTPPermission.ProtoReflect.Descriptor instead. func (*IntentionHTTPPermission) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{25} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{27} } func (x *IntentionHTTPPermission) GetPathExact() string { @@ -2170,7 +2338,7 @@ type IntentionHTTPHeaderPermission struct { func (x *IntentionHTTPHeaderPermission) Reset() { *x = IntentionHTTPHeaderPermission{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[26] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2183,7 +2351,7 @@ func (x *IntentionHTTPHeaderPermission) String() string { func (*IntentionHTTPHeaderPermission) ProtoMessage() {} func (x *IntentionHTTPHeaderPermission) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[26] + mi := &file_proto_pbconfigentry_config_entry_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2196,7 +2364,7 @@ func (x *IntentionHTTPHeaderPermission) ProtoReflect() protoreflect.Message { // Deprecated: Use IntentionHTTPHeaderPermission.ProtoReflect.Descriptor instead. func (*IntentionHTTPHeaderPermission) Descriptor() ([]byte, []int) { - return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{26} + return file_proto_pbconfigentry_config_entry_proto_rawDescGZIP(), []int{28} } func (x *IntentionHTTPHeaderPermission) GetName() string { @@ -2303,7 +2471,7 @@ var file_proto_pbconfigentry_config_entry_proto_rawDesc = []byte{ 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x11, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x22, 0x98, 0x03, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, + 0x6e, 0x74, 0x72, 0x79, 0x22, 0xec, 0x03, 0x0a, 0x0a, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x6d, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, @@ -2325,430 +2493,460 @@ var file_proto_pbconfigentry_config_entry_proto_rawDesc = []byte{ 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x50, 0x0a, 0x1a, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x50, 0x72, - 0x6f, 0x78, 0x79, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x32, 0x0a, - 0x14, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x4d, 0x65, 0x73, - 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4f, 0x6e, 0x6c, - 0x79, 0x22, 0xc9, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x5b, 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, - 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, - 0x12, 0x5b, 0x0a, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x44, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x22, 0x8a, 0x01, - 0x0a, 0x18, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, - 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, - 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, - 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x54, 0x0a, 0x0e, 0x4d, 0x65, - 0x73, 0x68, 0x48, 0x54, 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x42, 0x0a, 0x1c, - 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x1c, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, - 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, - 0x22, 0xf6, 0x06, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x6c, 0x76, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, - 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x44, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x5d, 0x0a, 0x07, 0x53, 0x75, - 0x62, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x72, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x12, 0x5a, 0x0a, 0x08, 0x52, 0x65, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x52, 0x08, 0x52, 0x65, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x60, 0x0a, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x52, 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, - 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x46, - 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, - 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x4c, 0x6f, - 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x72, 0x52, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x72, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x78, 0x0a, 0x0c, 0x53, 0x75, 0x62, - 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x52, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x68, 0x61, 0x73, + 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x50, 0x0a, 0x1a, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x32, 0x0a, 0x14, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x14, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0xc9, 0x01, 0x0a, 0x0d, 0x4d, 0x65, 0x73, 0x68, 0x54, 0x4c, + 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5b, 0x0a, 0x08, 0x49, 0x6e, 0x63, 0x6f, 0x6d, + 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, + 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x49, 0x6e, 0x63, 0x6f, + 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x5b, 0x0a, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4d, + 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, + 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x08, 0x4f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, + 0x67, 0x22, 0x8a, 0x01, 0x0a, 0x18, 0x4d, 0x65, 0x73, 0x68, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, + 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, + 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, + 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x54, + 0x0a, 0x0e, 0x4d, 0x65, 0x73, 0x68, 0x48, 0x54, 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x42, 0x0a, 0x1c, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x58, 0x46, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1c, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, + 0x58, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x64, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x43, 0x65, 0x72, 0x74, 0x22, 0x4d, 0x0a, 0x11, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4d, + 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a, 0x17, 0x50, 0x65, 0x65, + 0x72, 0x54, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x50, 0x65, 0x65, 0x72, + 0x54, 0x68, 0x72, 0x6f, 0x75, 0x67, 0x68, 0x4d, 0x65, 0x73, 0x68, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x73, 0x22, 0xf6, 0x06, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x44, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x5d, 0x0a, + 0x07, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x07, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x12, 0x5a, 0x0a, 0x08, + 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x52, 0x08, + 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x60, 0x0a, 0x08, 0x46, 0x61, 0x69, 0x6c, + 0x6f, 0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x72, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x1a, 0x7b, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x54, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, - 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x15, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, 0x73, - 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x6e, - 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0b, 0x4f, 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x22, 0xb5, 0x01, 0x0a, - 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, - 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, - 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, - 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, - 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, - 0x6e, 0x74, 0x65, 0x72, 0x22, 0x99, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, - 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, - 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, - 0x22, 0xc7, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x72, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x5d, 0x0a, 0x0e, 0x52, 0x69, 0x6e, - 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, - 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, - 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x69, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x65, 0x61, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0c, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0c, 0x48, 0x61, - 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x69, - 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x0f, - 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, - 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, - 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, - 0x22, 0x36, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, 0x65, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x43, 0x68, 0x6f, - 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xd3, 0x01, 0x0a, 0x0a, 0x48, 0x61, 0x73, - 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1e, 0x0a, - 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x57, 0x0a, - 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, + 0x65, 0x72, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x08, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x0e, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x57, 0x0a, + 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6f, 0x6b, - 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x50, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x69, - 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, - 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x22, 0xbf, 0x02, 0x0a, 0x0e, 0x49, 0x6e, - 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x49, 0x0a, 0x03, - 0x54, 0x4c, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x54, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, - 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x6f, 0x61, 0x64, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x52, 0x0c, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x78, 0x0a, 0x0c, + 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x52, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3c, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x7b, 0x0a, 0x0d, 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, + 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x54, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, + 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x15, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x53, + 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x20, 0x0a, + 0x0b, 0x4f, 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x4f, 0x6e, 0x6c, 0x79, 0x50, 0x61, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x22, + 0xc9, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, + 0x76, 0x65, 0x72, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, + 0x65, 0x6e, 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, + 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, 0xf9, 0x01, 0x0a, 0x17, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, + 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, + 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x61, 0x74, 0x61, + 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x12, 0x5e, 0x0a, 0x07, 0x54, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, + 0x46, 0x61, 0x69, 0x6c, 0x6f, 0x76, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x07, + 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x22, 0xcf, 0x01, 0x0a, 0x1d, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x46, 0x61, 0x69, 0x6c, 0x6f, + 0x76, 0x65, 0x72, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x75, + 0x62, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, + 0x74, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x63, + 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x22, 0xc7, 0x02, 0x0a, 0x0c, 0x4c, 0x6f, + 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x12, 0x5d, 0x0a, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, - 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, - 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x53, 0x0a, - 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, - 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, - 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x01, 0x0a, 0x10, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x18, 0x0a, 0x07, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x07, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x03, 0x53, 0x44, - 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x03, 0x53, 0x44, 0x53, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, - 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, - 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, - 0x69, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, - 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x20, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x51, 0x0a, 0x08, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x49, 0x0a, 0x03, - 0x54, 0x4c, 0x53, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, - 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x22, 0xbe, 0x04, 0x0a, 0x0e, 0x49, 0x6e, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x48, - 0x6f, 0x73, 0x74, 0x73, 0x12, 0x50, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, - 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, 0x62, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x0f, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, - 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, - 0x12, 0x53, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, + 0x72, 0x79, 0x2e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x69, 0x0a, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, - 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x1a, - 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x67, 0x0a, 0x17, 0x47, 0x61, 0x74, 0x65, - 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x4c, 0x0a, 0x03, 0x53, 0x44, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x12, 0x4c, 0x65, 0x61, 0x73, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x55, 0x0a, 0x0c, + 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0c, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x69, 0x65, 0x73, 0x22, 0x64, 0x0a, 0x0e, 0x52, 0x69, 0x6e, 0x67, 0x48, 0x61, 0x73, 0x68, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, + 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, + 0x4d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x12, + 0x28, 0x0a, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, + 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x4d, 0x61, 0x78, 0x69, 0x6d, 0x75, + 0x6d, 0x52, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x36, 0x0a, 0x12, 0x4c, 0x65, 0x61, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x20, 0x0a, 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x43, 0x68, 0x6f, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x22, 0xd3, 0x01, 0x0a, 0x0a, 0x48, 0x61, 0x73, 0x68, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x1a, 0x0a, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x08, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x54, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x54, + 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x69, 0x0a, 0x0c, 0x43, 0x6f, 0x6f, 0x6b, 0x69, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x2b, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x12, + 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, + 0x74, 0x68, 0x22, 0xbf, 0x02, 0x0a, 0x0e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, + 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x49, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, + 0x12, 0x54, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x52, 0x09, 0x4c, 0x69, 0x73, + 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x53, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x4d, 0x65, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, + 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x01, 0x0a, 0x10, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x45, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x4c, 0x0a, 0x03, 0x53, 0x44, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x53, 0x44, - 0x53, 0x22, 0xcb, 0x02, 0x0a, 0x13, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x12, 0x55, 0x0a, 0x03, 0x41, 0x64, 0x64, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, - 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x41, 0x64, 0x64, - 0x12, 0x55, 0x0a, 0x03, 0x53, 0x65, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x03, 0x53, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x1a, - 0x36, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x36, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0xf6, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, 0x0a, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, - 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa6, 0x06, 0x0a, 0x0f, 0x53, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, - 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x5c, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, - 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0a, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, - 0x0a, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x49, 0x44, 0x12, 0x4e, 0x0a, 0x04, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, - 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x66, 0x0a, 0x0a, - 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x46, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, - 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x4d, 0x65, 0x74, 0x61, 0x12, 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, 0x61, - 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, - 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x54, 0x69, 0x6d, 0x65, 0x12, 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, - 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, + 0x53, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x69, 0x6e, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x4c, 0x53, 0x4d, 0x61, + 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x54, 0x4c, 0x53, 0x4d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, + 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65, + 0x73, 0x22, 0x5b, 0x0a, 0x13, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, + 0x44, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x65, + 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xdf, + 0x01, 0x0a, 0x0f, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x51, 0x0a, 0x08, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x49, 0x0a, 0x03, 0x54, 0x4c, 0x53, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, + 0x22, 0xbe, 0x04, 0x0a, 0x0e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x50, 0x0a, + 0x03, 0x54, 0x4c, 0x53, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, + 0x72, 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x54, 0x4c, 0x53, 0x12, + 0x62, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x73, 0x52, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, - 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, - 0x65, 0x72, 0x1a, 0x3d, 0x0a, 0x0f, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, + 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x52, 0x0f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x53, 0x0a, 0x04, 0x4d, 0x65, 0x74, + 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, + 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x58, + 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0xb9, 0x01, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x01, 0x22, 0x67, 0x0a, 0x17, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x54, 0x4c, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4c, 0x0a, 0x03, + 0x53, 0x44, 0x53, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x54, 0x4c, 0x53, 0x53, 0x44, 0x53, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x03, 0x53, 0x44, 0x53, 0x22, 0xcb, 0x02, 0x0a, 0x13, 0x48, + 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x73, 0x12, 0x55, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x41, 0x64, 0x64, 0x12, 0x55, 0x0a, 0x03, 0x53, 0x65, 0x74, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x48, + 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x53, 0x65, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x1a, 0x36, 0x0a, 0x08, 0x41, 0x64, 0x64, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x1a, 0x36, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf6, 0x01, 0x0a, 0x11, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x50, + 0x0a, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x12, 0x56, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xa6, 0x06, 0x0a, 0x0f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x04, 0x48, 0x54, 0x54, - 0x50, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, - 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, - 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x48, 0x54, 0x54, 0x50, 0x22, 0xed, 0x01, - 0x0a, 0x17, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, - 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, - 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x50, 0x61, 0x74, - 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, - 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, - 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x5c, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5c, 0x0a, 0x0b, 0x50, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x50, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x72, 0x65, 0x63, 0x65, + 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x50, 0x72, 0x65, + 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x49, 0x44, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x4c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x49, 0x44, 0x12, 0x4e, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x66, 0x0a, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, + 0x65, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x46, 0x2e, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0a, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x46, 0x0a, + 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, + 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x10, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x4c, 0x65, 0x67, + 0x61, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x58, 0x0a, + 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x48, 0x65, 0x61, - 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0xc1, 0x01, - 0x0a, 0x1d, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x48, - 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x78, - 0x61, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x53, - 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x75, 0x66, - 0x66, 0x69, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x6e, 0x76, - 0x65, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, - 0x74, 0x2a, 0x77, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x4b, 0x69, 0x6e, - 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4b, 0x69, - 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x10, 0x01, 0x12, 0x17, - 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, - 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x69, 0x6e, 0x64, 0x49, - 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x10, 0x03, 0x12, - 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x10, 0x04, 0x2a, 0x26, 0x0a, 0x0f, 0x49, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, - 0x04, 0x44, 0x65, 0x6e, 0x79, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x77, - 0x10, 0x01, 0x2a, 0x21, 0x0a, 0x13, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x10, 0x00, 0x42, 0xa6, 0x02, 0x0a, 0x29, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, - 0x74, 0x72, 0x79, 0x42, 0x10, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, - 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x43, 0xaa, - 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, - 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xca, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xe2, - 0x02, 0x31, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, - 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x28, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, - 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x1a, 0x3d, 0x0a, 0x0f, 0x4c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xb9, 0x01, 0x0a, 0x13, 0x49, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x52, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x3e, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x04, 0x48, 0x54, 0x54, 0x50, 0x22, 0xed, 0x01, 0x0a, 0x17, 0x49, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, 0x45, 0x78, 0x61, 0x63, 0x74, + 0x12, 0x1e, 0x0a, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x50, 0x61, 0x74, 0x68, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, + 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x5c, + 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x22, 0xc1, 0x01, 0x0a, 0x1d, 0x49, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x54, 0x54, 0x50, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x50, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, + 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x14, 0x0a, 0x05, + 0x52, 0x65, 0x67, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x65, 0x67, + 0x65, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x49, 0x6e, 0x76, 0x65, 0x72, 0x74, 0x2a, 0x77, 0x0a, 0x04, 0x4b, 0x69, + 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x4b, 0x69, 0x6e, 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, + 0x6e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x4b, 0x69, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x68, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x4b, 0x69, 0x6e, 0x64, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x10, 0x02, + 0x12, 0x16, 0x0a, 0x12, 0x4b, 0x69, 0x6e, 0x64, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x47, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x4b, 0x69, 0x6e, 0x64, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x10, 0x04, 0x2a, 0x26, 0x0a, 0x0f, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, + 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x65, 0x6e, 0x79, 0x10, 0x00, + 0x12, 0x09, 0x0a, 0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x10, 0x01, 0x2a, 0x21, 0x0a, 0x13, 0x49, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x10, 0x00, 0x42, 0xa6, + 0x02, 0x0a, 0x29, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x10, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x43, 0xaa, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0xca, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0xe2, 0x02, 0x31, 0x48, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x28, 0x48, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2764,7 +2962,7 @@ func file_proto_pbconfigentry_config_entry_proto_rawDescGZIP() []byte { } var file_proto_pbconfigentry_config_entry_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_proto_pbconfigentry_config_entry_proto_msgTypes = make([]protoimpl.MessageInfo, 37) +var file_proto_pbconfigentry_config_entry_proto_msgTypes = make([]protoimpl.MessageInfo, 39) var file_proto_pbconfigentry_config_entry_proto_goTypes = []interface{}{ (Kind)(0), // 0: hashicorp.consul.internal.configentry.Kind (IntentionAction)(0), // 1: hashicorp.consul.internal.configentry.IntentionAction @@ -2775,100 +2973,104 @@ var file_proto_pbconfigentry_config_entry_proto_goTypes = []interface{}{ (*MeshTLSConfig)(nil), // 6: hashicorp.consul.internal.configentry.MeshTLSConfig (*MeshDirectionalTLSConfig)(nil), // 7: hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig (*MeshHTTPConfig)(nil), // 8: hashicorp.consul.internal.configentry.MeshHTTPConfig - (*ServiceResolver)(nil), // 9: hashicorp.consul.internal.configentry.ServiceResolver - (*ServiceResolverSubset)(nil), // 10: hashicorp.consul.internal.configentry.ServiceResolverSubset - (*ServiceResolverRedirect)(nil), // 11: hashicorp.consul.internal.configentry.ServiceResolverRedirect - (*ServiceResolverFailover)(nil), // 12: hashicorp.consul.internal.configentry.ServiceResolverFailover - (*LoadBalancer)(nil), // 13: hashicorp.consul.internal.configentry.LoadBalancer - (*RingHashConfig)(nil), // 14: hashicorp.consul.internal.configentry.RingHashConfig - (*LeastRequestConfig)(nil), // 15: hashicorp.consul.internal.configentry.LeastRequestConfig - (*HashPolicy)(nil), // 16: hashicorp.consul.internal.configentry.HashPolicy - (*CookieConfig)(nil), // 17: hashicorp.consul.internal.configentry.CookieConfig - (*IngressGateway)(nil), // 18: hashicorp.consul.internal.configentry.IngressGateway - (*GatewayTLSConfig)(nil), // 19: hashicorp.consul.internal.configentry.GatewayTLSConfig - (*GatewayTLSSDSConfig)(nil), // 20: hashicorp.consul.internal.configentry.GatewayTLSSDSConfig - (*IngressListener)(nil), // 21: hashicorp.consul.internal.configentry.IngressListener - (*IngressService)(nil), // 22: hashicorp.consul.internal.configentry.IngressService - (*GatewayServiceTLSConfig)(nil), // 23: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig - (*HTTPHeaderModifiers)(nil), // 24: hashicorp.consul.internal.configentry.HTTPHeaderModifiers - (*ServiceIntentions)(nil), // 25: hashicorp.consul.internal.configentry.ServiceIntentions - (*SourceIntention)(nil), // 26: hashicorp.consul.internal.configentry.SourceIntention - (*IntentionPermission)(nil), // 27: hashicorp.consul.internal.configentry.IntentionPermission - (*IntentionHTTPPermission)(nil), // 28: hashicorp.consul.internal.configentry.IntentionHTTPPermission - (*IntentionHTTPHeaderPermission)(nil), // 29: hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission - nil, // 30: hashicorp.consul.internal.configentry.MeshConfig.MetaEntry - nil, // 31: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry - nil, // 32: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry - nil, // 33: hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry - nil, // 34: hashicorp.consul.internal.configentry.IngressGateway.MetaEntry - nil, // 35: hashicorp.consul.internal.configentry.IngressService.MetaEntry - nil, // 36: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry - nil, // 37: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry - nil, // 38: hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry - nil, // 39: hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry - (*pbcommon.EnterpriseMeta)(nil), // 40: hashicorp.consul.internal.common.EnterpriseMeta - (*pbcommon.RaftIndex)(nil), // 41: hashicorp.consul.internal.common.RaftIndex - (*durationpb.Duration)(nil), // 42: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp + (*PeeringMeshConfig)(nil), // 9: hashicorp.consul.internal.configentry.PeeringMeshConfig + (*ServiceResolver)(nil), // 10: hashicorp.consul.internal.configentry.ServiceResolver + (*ServiceResolverSubset)(nil), // 11: hashicorp.consul.internal.configentry.ServiceResolverSubset + (*ServiceResolverRedirect)(nil), // 12: hashicorp.consul.internal.configentry.ServiceResolverRedirect + (*ServiceResolverFailover)(nil), // 13: hashicorp.consul.internal.configentry.ServiceResolverFailover + (*ServiceResolverFailoverTarget)(nil), // 14: hashicorp.consul.internal.configentry.ServiceResolverFailoverTarget + (*LoadBalancer)(nil), // 15: hashicorp.consul.internal.configentry.LoadBalancer + (*RingHashConfig)(nil), // 16: hashicorp.consul.internal.configentry.RingHashConfig + (*LeastRequestConfig)(nil), // 17: hashicorp.consul.internal.configentry.LeastRequestConfig + (*HashPolicy)(nil), // 18: hashicorp.consul.internal.configentry.HashPolicy + (*CookieConfig)(nil), // 19: hashicorp.consul.internal.configentry.CookieConfig + (*IngressGateway)(nil), // 20: hashicorp.consul.internal.configentry.IngressGateway + (*GatewayTLSConfig)(nil), // 21: hashicorp.consul.internal.configentry.GatewayTLSConfig + (*GatewayTLSSDSConfig)(nil), // 22: hashicorp.consul.internal.configentry.GatewayTLSSDSConfig + (*IngressListener)(nil), // 23: hashicorp.consul.internal.configentry.IngressListener + (*IngressService)(nil), // 24: hashicorp.consul.internal.configentry.IngressService + (*GatewayServiceTLSConfig)(nil), // 25: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig + (*HTTPHeaderModifiers)(nil), // 26: hashicorp.consul.internal.configentry.HTTPHeaderModifiers + (*ServiceIntentions)(nil), // 27: hashicorp.consul.internal.configentry.ServiceIntentions + (*SourceIntention)(nil), // 28: hashicorp.consul.internal.configentry.SourceIntention + (*IntentionPermission)(nil), // 29: hashicorp.consul.internal.configentry.IntentionPermission + (*IntentionHTTPPermission)(nil), // 30: hashicorp.consul.internal.configentry.IntentionHTTPPermission + (*IntentionHTTPHeaderPermission)(nil), // 31: hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission + nil, // 32: hashicorp.consul.internal.configentry.MeshConfig.MetaEntry + nil, // 33: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry + nil, // 34: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry + nil, // 35: hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry + nil, // 36: hashicorp.consul.internal.configentry.IngressGateway.MetaEntry + nil, // 37: hashicorp.consul.internal.configentry.IngressService.MetaEntry + nil, // 38: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry + nil, // 39: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry + nil, // 40: hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry + nil, // 41: hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry + (*pbcommon.EnterpriseMeta)(nil), // 42: hashicorp.consul.internal.common.EnterpriseMeta + (*pbcommon.RaftIndex)(nil), // 43: hashicorp.consul.internal.common.RaftIndex + (*durationpb.Duration)(nil), // 44: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 45: google.protobuf.Timestamp } var file_proto_pbconfigentry_config_entry_proto_depIdxs = []int32{ 0, // 0: hashicorp.consul.internal.configentry.ConfigEntry.Kind:type_name -> hashicorp.consul.internal.configentry.Kind - 40, // 1: hashicorp.consul.internal.configentry.ConfigEntry.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 41, // 2: hashicorp.consul.internal.configentry.ConfigEntry.RaftIndex:type_name -> hashicorp.consul.internal.common.RaftIndex + 42, // 1: hashicorp.consul.internal.configentry.ConfigEntry.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 43, // 2: hashicorp.consul.internal.configentry.ConfigEntry.RaftIndex:type_name -> hashicorp.consul.internal.common.RaftIndex 4, // 3: hashicorp.consul.internal.configentry.ConfigEntry.MeshConfig:type_name -> hashicorp.consul.internal.configentry.MeshConfig - 9, // 4: hashicorp.consul.internal.configentry.ConfigEntry.ServiceResolver:type_name -> hashicorp.consul.internal.configentry.ServiceResolver - 18, // 5: hashicorp.consul.internal.configentry.ConfigEntry.IngressGateway:type_name -> hashicorp.consul.internal.configentry.IngressGateway - 25, // 6: hashicorp.consul.internal.configentry.ConfigEntry.ServiceIntentions:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions + 10, // 4: hashicorp.consul.internal.configentry.ConfigEntry.ServiceResolver:type_name -> hashicorp.consul.internal.configentry.ServiceResolver + 20, // 5: hashicorp.consul.internal.configentry.ConfigEntry.IngressGateway:type_name -> hashicorp.consul.internal.configentry.IngressGateway + 27, // 6: hashicorp.consul.internal.configentry.ConfigEntry.ServiceIntentions:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions 5, // 7: hashicorp.consul.internal.configentry.MeshConfig.TransparentProxy:type_name -> hashicorp.consul.internal.configentry.TransparentProxyMeshConfig 6, // 8: hashicorp.consul.internal.configentry.MeshConfig.TLS:type_name -> hashicorp.consul.internal.configentry.MeshTLSConfig 8, // 9: hashicorp.consul.internal.configentry.MeshConfig.HTTP:type_name -> hashicorp.consul.internal.configentry.MeshHTTPConfig - 30, // 10: hashicorp.consul.internal.configentry.MeshConfig.Meta:type_name -> hashicorp.consul.internal.configentry.MeshConfig.MetaEntry - 7, // 11: hashicorp.consul.internal.configentry.MeshTLSConfig.Incoming:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig - 7, // 12: hashicorp.consul.internal.configentry.MeshTLSConfig.Outgoing:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig - 31, // 13: hashicorp.consul.internal.configentry.ServiceResolver.Subsets:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry - 11, // 14: hashicorp.consul.internal.configentry.ServiceResolver.Redirect:type_name -> hashicorp.consul.internal.configentry.ServiceResolverRedirect - 32, // 15: hashicorp.consul.internal.configentry.ServiceResolver.Failover:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry - 42, // 16: hashicorp.consul.internal.configentry.ServiceResolver.ConnectTimeout:type_name -> google.protobuf.Duration - 13, // 17: hashicorp.consul.internal.configentry.ServiceResolver.LoadBalancer:type_name -> hashicorp.consul.internal.configentry.LoadBalancer - 33, // 18: hashicorp.consul.internal.configentry.ServiceResolver.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry - 14, // 19: hashicorp.consul.internal.configentry.LoadBalancer.RingHashConfig:type_name -> hashicorp.consul.internal.configentry.RingHashConfig - 15, // 20: hashicorp.consul.internal.configentry.LoadBalancer.LeastRequestConfig:type_name -> hashicorp.consul.internal.configentry.LeastRequestConfig - 16, // 21: hashicorp.consul.internal.configentry.LoadBalancer.HashPolicies:type_name -> hashicorp.consul.internal.configentry.HashPolicy - 17, // 22: hashicorp.consul.internal.configentry.HashPolicy.CookieConfig:type_name -> hashicorp.consul.internal.configentry.CookieConfig - 42, // 23: hashicorp.consul.internal.configentry.CookieConfig.TTL:type_name -> google.protobuf.Duration - 19, // 24: hashicorp.consul.internal.configentry.IngressGateway.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig - 21, // 25: hashicorp.consul.internal.configentry.IngressGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.IngressListener - 34, // 26: hashicorp.consul.internal.configentry.IngressGateway.Meta:type_name -> hashicorp.consul.internal.configentry.IngressGateway.MetaEntry - 20, // 27: hashicorp.consul.internal.configentry.GatewayTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig - 22, // 28: hashicorp.consul.internal.configentry.IngressListener.Services:type_name -> hashicorp.consul.internal.configentry.IngressService - 19, // 29: hashicorp.consul.internal.configentry.IngressListener.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig - 23, // 30: hashicorp.consul.internal.configentry.IngressService.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayServiceTLSConfig - 24, // 31: hashicorp.consul.internal.configentry.IngressService.RequestHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers - 24, // 32: hashicorp.consul.internal.configentry.IngressService.ResponseHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers - 35, // 33: hashicorp.consul.internal.configentry.IngressService.Meta:type_name -> hashicorp.consul.internal.configentry.IngressService.MetaEntry - 40, // 34: hashicorp.consul.internal.configentry.IngressService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 20, // 35: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig - 36, // 36: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry - 37, // 37: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry - 26, // 38: hashicorp.consul.internal.configentry.ServiceIntentions.Sources:type_name -> hashicorp.consul.internal.configentry.SourceIntention - 38, // 39: hashicorp.consul.internal.configentry.ServiceIntentions.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry - 1, // 40: hashicorp.consul.internal.configentry.SourceIntention.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction - 27, // 41: hashicorp.consul.internal.configentry.SourceIntention.Permissions:type_name -> hashicorp.consul.internal.configentry.IntentionPermission - 2, // 42: hashicorp.consul.internal.configentry.SourceIntention.Type:type_name -> hashicorp.consul.internal.configentry.IntentionSourceType - 39, // 43: hashicorp.consul.internal.configentry.SourceIntention.LegacyMeta:type_name -> hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry - 43, // 44: hashicorp.consul.internal.configentry.SourceIntention.LegacyCreateTime:type_name -> google.protobuf.Timestamp - 43, // 45: hashicorp.consul.internal.configentry.SourceIntention.LegacyUpdateTime:type_name -> google.protobuf.Timestamp - 40, // 46: hashicorp.consul.internal.configentry.SourceIntention.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta - 1, // 47: hashicorp.consul.internal.configentry.IntentionPermission.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction - 28, // 48: hashicorp.consul.internal.configentry.IntentionPermission.HTTP:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPPermission - 29, // 49: hashicorp.consul.internal.configentry.IntentionHTTPPermission.Header:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission - 10, // 50: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverSubset - 12, // 51: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailover - 52, // [52:52] is the sub-list for method output_type - 52, // [52:52] is the sub-list for method input_type - 52, // [52:52] is the sub-list for extension type_name - 52, // [52:52] is the sub-list for extension extendee - 0, // [0:52] is the sub-list for field type_name + 32, // 10: hashicorp.consul.internal.configentry.MeshConfig.Meta:type_name -> hashicorp.consul.internal.configentry.MeshConfig.MetaEntry + 9, // 11: hashicorp.consul.internal.configentry.MeshConfig.Peering:type_name -> hashicorp.consul.internal.configentry.PeeringMeshConfig + 7, // 12: hashicorp.consul.internal.configentry.MeshTLSConfig.Incoming:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig + 7, // 13: hashicorp.consul.internal.configentry.MeshTLSConfig.Outgoing:type_name -> hashicorp.consul.internal.configentry.MeshDirectionalTLSConfig + 33, // 14: hashicorp.consul.internal.configentry.ServiceResolver.Subsets:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry + 12, // 15: hashicorp.consul.internal.configentry.ServiceResolver.Redirect:type_name -> hashicorp.consul.internal.configentry.ServiceResolverRedirect + 34, // 16: hashicorp.consul.internal.configentry.ServiceResolver.Failover:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry + 44, // 17: hashicorp.consul.internal.configentry.ServiceResolver.ConnectTimeout:type_name -> google.protobuf.Duration + 15, // 18: hashicorp.consul.internal.configentry.ServiceResolver.LoadBalancer:type_name -> hashicorp.consul.internal.configentry.LoadBalancer + 35, // 19: hashicorp.consul.internal.configentry.ServiceResolver.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceResolver.MetaEntry + 14, // 20: hashicorp.consul.internal.configentry.ServiceResolverFailover.Targets:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailoverTarget + 16, // 21: hashicorp.consul.internal.configentry.LoadBalancer.RingHashConfig:type_name -> hashicorp.consul.internal.configentry.RingHashConfig + 17, // 22: hashicorp.consul.internal.configentry.LoadBalancer.LeastRequestConfig:type_name -> hashicorp.consul.internal.configentry.LeastRequestConfig + 18, // 23: hashicorp.consul.internal.configentry.LoadBalancer.HashPolicies:type_name -> hashicorp.consul.internal.configentry.HashPolicy + 19, // 24: hashicorp.consul.internal.configentry.HashPolicy.CookieConfig:type_name -> hashicorp.consul.internal.configentry.CookieConfig + 44, // 25: hashicorp.consul.internal.configentry.CookieConfig.TTL:type_name -> google.protobuf.Duration + 21, // 26: hashicorp.consul.internal.configentry.IngressGateway.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig + 23, // 27: hashicorp.consul.internal.configentry.IngressGateway.Listeners:type_name -> hashicorp.consul.internal.configentry.IngressListener + 36, // 28: hashicorp.consul.internal.configentry.IngressGateway.Meta:type_name -> hashicorp.consul.internal.configentry.IngressGateway.MetaEntry + 22, // 29: hashicorp.consul.internal.configentry.GatewayTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig + 24, // 30: hashicorp.consul.internal.configentry.IngressListener.Services:type_name -> hashicorp.consul.internal.configentry.IngressService + 21, // 31: hashicorp.consul.internal.configentry.IngressListener.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSConfig + 25, // 32: hashicorp.consul.internal.configentry.IngressService.TLS:type_name -> hashicorp.consul.internal.configentry.GatewayServiceTLSConfig + 26, // 33: hashicorp.consul.internal.configentry.IngressService.RequestHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers + 26, // 34: hashicorp.consul.internal.configentry.IngressService.ResponseHeaders:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers + 37, // 35: hashicorp.consul.internal.configentry.IngressService.Meta:type_name -> hashicorp.consul.internal.configentry.IngressService.MetaEntry + 42, // 36: hashicorp.consul.internal.configentry.IngressService.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 22, // 37: hashicorp.consul.internal.configentry.GatewayServiceTLSConfig.SDS:type_name -> hashicorp.consul.internal.configentry.GatewayTLSSDSConfig + 38, // 38: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Add:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.AddEntry + 39, // 39: hashicorp.consul.internal.configentry.HTTPHeaderModifiers.Set:type_name -> hashicorp.consul.internal.configentry.HTTPHeaderModifiers.SetEntry + 28, // 40: hashicorp.consul.internal.configentry.ServiceIntentions.Sources:type_name -> hashicorp.consul.internal.configentry.SourceIntention + 40, // 41: hashicorp.consul.internal.configentry.ServiceIntentions.Meta:type_name -> hashicorp.consul.internal.configentry.ServiceIntentions.MetaEntry + 1, // 42: hashicorp.consul.internal.configentry.SourceIntention.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction + 29, // 43: hashicorp.consul.internal.configentry.SourceIntention.Permissions:type_name -> hashicorp.consul.internal.configentry.IntentionPermission + 2, // 44: hashicorp.consul.internal.configentry.SourceIntention.Type:type_name -> hashicorp.consul.internal.configentry.IntentionSourceType + 41, // 45: hashicorp.consul.internal.configentry.SourceIntention.LegacyMeta:type_name -> hashicorp.consul.internal.configentry.SourceIntention.LegacyMetaEntry + 45, // 46: hashicorp.consul.internal.configentry.SourceIntention.LegacyCreateTime:type_name -> google.protobuf.Timestamp + 45, // 47: hashicorp.consul.internal.configentry.SourceIntention.LegacyUpdateTime:type_name -> google.protobuf.Timestamp + 42, // 48: hashicorp.consul.internal.configentry.SourceIntention.EnterpriseMeta:type_name -> hashicorp.consul.internal.common.EnterpriseMeta + 1, // 49: hashicorp.consul.internal.configentry.IntentionPermission.Action:type_name -> hashicorp.consul.internal.configentry.IntentionAction + 30, // 50: hashicorp.consul.internal.configentry.IntentionPermission.HTTP:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPPermission + 31, // 51: hashicorp.consul.internal.configentry.IntentionHTTPPermission.Header:type_name -> hashicorp.consul.internal.configentry.IntentionHTTPHeaderPermission + 11, // 52: hashicorp.consul.internal.configentry.ServiceResolver.SubsetsEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverSubset + 13, // 53: hashicorp.consul.internal.configentry.ServiceResolver.FailoverEntry.value:type_name -> hashicorp.consul.internal.configentry.ServiceResolverFailover + 54, // [54:54] is the sub-list for method output_type + 54, // [54:54] is the sub-list for method input_type + 54, // [54:54] is the sub-list for extension type_name + 54, // [54:54] is the sub-list for extension extendee + 0, // [0:54] is the sub-list for field type_name } func init() { file_proto_pbconfigentry_config_entry_proto_init() } @@ -2950,7 +3152,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServiceResolver); i { + switch v := v.(*PeeringMeshConfig); i { case 0: return &v.state case 1: @@ -2962,7 +3164,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServiceResolverSubset); i { + switch v := v.(*ServiceResolver); i { case 0: return &v.state case 1: @@ -2974,7 +3176,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServiceResolverRedirect); i { + switch v := v.(*ServiceResolverSubset); i { case 0: return &v.state case 1: @@ -2986,7 +3188,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServiceResolverFailover); i { + switch v := v.(*ServiceResolverRedirect); i { case 0: return &v.state case 1: @@ -2998,7 +3200,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoadBalancer); i { + switch v := v.(*ServiceResolverFailover); i { case 0: return &v.state case 1: @@ -3010,7 +3212,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RingHashConfig); i { + switch v := v.(*ServiceResolverFailoverTarget); i { case 0: return &v.state case 1: @@ -3022,7 +3224,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LeastRequestConfig); i { + switch v := v.(*LoadBalancer); i { case 0: return &v.state case 1: @@ -3034,7 +3236,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HashPolicy); i { + switch v := v.(*RingHashConfig); i { case 0: return &v.state case 1: @@ -3046,7 +3248,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CookieConfig); i { + switch v := v.(*LeastRequestConfig); i { case 0: return &v.state case 1: @@ -3058,7 +3260,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IngressGateway); i { + switch v := v.(*HashPolicy); i { case 0: return &v.state case 1: @@ -3070,7 +3272,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GatewayTLSConfig); i { + switch v := v.(*CookieConfig); i { case 0: return &v.state case 1: @@ -3082,7 +3284,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GatewayTLSSDSConfig); i { + switch v := v.(*IngressGateway); i { case 0: return &v.state case 1: @@ -3094,7 +3296,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IngressListener); i { + switch v := v.(*GatewayTLSConfig); i { case 0: return &v.state case 1: @@ -3106,7 +3308,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IngressService); i { + switch v := v.(*GatewayTLSSDSConfig); i { case 0: return &v.state case 1: @@ -3118,7 +3320,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GatewayServiceTLSConfig); i { + switch v := v.(*IngressListener); i { case 0: return &v.state case 1: @@ -3130,7 +3332,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HTTPHeaderModifiers); i { + switch v := v.(*IngressService); i { case 0: return &v.state case 1: @@ -3142,7 +3344,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ServiceIntentions); i { + switch v := v.(*GatewayServiceTLSConfig); i { case 0: return &v.state case 1: @@ -3154,7 +3356,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SourceIntention); i { + switch v := v.(*HTTPHeaderModifiers); i { case 0: return &v.state case 1: @@ -3166,7 +3368,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IntentionPermission); i { + switch v := v.(*ServiceIntentions); i { case 0: return &v.state case 1: @@ -3178,7 +3380,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IntentionHTTPPermission); i { + switch v := v.(*SourceIntention); i { case 0: return &v.state case 1: @@ -3190,6 +3392,30 @@ func file_proto_pbconfigentry_config_entry_proto_init() { } } file_proto_pbconfigentry_config_entry_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IntentionPermission); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_pbconfigentry_config_entry_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IntentionHTTPPermission); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_pbconfigentry_config_entry_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IntentionHTTPHeaderPermission); i { case 0: return &v.state @@ -3214,7 +3440,7 @@ func file_proto_pbconfigentry_config_entry_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_pbconfigentry_config_entry_proto_rawDesc, NumEnums: 3, - NumMessages: 37, + NumMessages: 39, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/pbconfigentry/config_entry.proto b/proto/pbconfigentry/config_entry.proto index 0a35f4ef1..dd4dc049e 100644 --- a/proto/pbconfigentry/config_entry.proto +++ b/proto/pbconfigentry/config_entry.proto @@ -40,6 +40,7 @@ message MeshConfig { MeshTLSConfig TLS = 2; MeshHTTPConfig HTTP = 3; map Meta = 4; + PeeringMeshConfig Peering = 5; } // mog annotation: @@ -84,6 +85,15 @@ message MeshHTTPConfig { bool SanitizeXForwardedClientCert = 1; } +// mog annotation: +// +// target=github.com/hashicorp/consul/agent/structs.PeeringMeshConfig +// output=config_entry.gen.go +// name=Structs +message PeeringMeshConfig { + bool PeerThroughMeshGateways = 1; +} + // mog annotation: // // target=github.com/hashicorp/consul/agent/structs.ServiceResolverConfigEntry @@ -122,6 +132,7 @@ message ServiceResolverRedirect { string Namespace = 3; string Partition = 4; string Datacenter = 5; + string Peer = 6; } // mog annotation: @@ -134,6 +145,21 @@ message ServiceResolverFailover { string ServiceSubset = 2; string Namespace = 3; repeated string Datacenters = 4; + repeated ServiceResolverFailoverTarget Targets = 5; +} + +// mog annotation: +// +// target=github.com/hashicorp/consul/agent/structs.ServiceResolverFailoverTarget +// output=config_entry.gen.go +// name=Structs +message ServiceResolverFailoverTarget { + string Service = 1; + string ServiceSubset = 2; + string Partition = 3; + string Namespace = 4; + string Datacenter = 5; + string Peer = 6; } // mog annotation: diff --git a/proto/pbconnect/connect.gen.go b/proto/pbconnect/connect.gen.go index 701400d80..abecd9b4c 100644 --- a/proto/pbconnect/connect.gen.go +++ b/proto/pbconnect/connect.gen.go @@ -93,6 +93,7 @@ func IssuedCertToStructsIssuedCert(s *IssuedCert, t *structs.IssuedCert) { t.ServiceURI = s.ServiceURI t.Agent = s.Agent t.AgentURI = s.AgentURI + t.ServerURI = s.ServerURI t.Kind = structs.ServiceKind(s.Kind) t.KindURI = s.KindURI t.ValidAfter = structs.TimeFromProto(s.ValidAfter) @@ -111,6 +112,7 @@ func IssuedCertFromStructsIssuedCert(t *structs.IssuedCert, s *IssuedCert) { s.ServiceURI = t.ServiceURI s.Agent = t.Agent s.AgentURI = t.AgentURI + s.ServerURI = t.ServerURI s.Kind = string(t.Kind) s.KindURI = t.KindURI s.ValidAfter = structs.TimeToProto(t.ValidAfter) diff --git a/proto/pbconnect/connect.pb.go b/proto/pbconnect/connect.pb.go index 5e6adf740..89100474f 100644 --- a/proto/pbconnect/connect.pb.go +++ b/proto/pbconnect/connect.pb.go @@ -377,6 +377,9 @@ type IssuedCert struct { Kind string `protobuf:"bytes,12,opt,name=Kind,proto3" json:"Kind,omitempty"` // KindURI is the cert URI value. KindURI string `protobuf:"bytes,13,opt,name=KindURI,proto3" json:"KindURI,omitempty"` + // ServerURI is the URI value of a cert issued for a server agent. + // The same URI is shared by all servers in a Consul datacenter. + ServerURI string `protobuf:"bytes,14,opt,name=ServerURI,proto3" json:"ServerURI,omitempty"` // ValidAfter and ValidBefore are the validity periods for the // certificate. // mog: func-to=structs.TimeFromProto func-from=structs.TimeToProto @@ -485,6 +488,13 @@ func (x *IssuedCert) GetKindURI() string { return "" } +func (x *IssuedCert) GetServerURI() string { + if x != nil { + return x.ServerURI + } + return "" +} + func (x *IssuedCert) GetValidAfter() *timestamppb.Timestamp { if x != nil { return x.ValidAfter @@ -579,7 +589,7 @@ var file_proto_pbconnect_connect_proto_rawDesc = []byte{ 0x32, 0x2b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x61, 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x09, 0x52, - 0x61, 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0xa9, 0x04, 0x0a, 0x0a, 0x49, 0x73, 0x73, + 0x61, 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0xc7, 0x04, 0x0a, 0x0a, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x43, @@ -596,42 +606,44 @@ var file_proto_pbconnect_connect_proto_rawDesc = []byte{ 0x67, 0x65, 0x6e, 0x74, 0x55, 0x52, 0x49, 0x12, 0x12, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x4b, 0x69, 0x6e, 0x64, 0x55, 0x52, 0x49, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4b, 0x69, - 0x6e, 0x64, 0x55, 0x52, 0x49, 0x12, 0x3a, 0x0a, 0x0a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x66, - 0x74, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x66, 0x74, 0x65, - 0x72, 0x12, 0x3c, 0x0a, 0x0b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x0b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, - 0x58, 0x0a, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x49, 0x0a, 0x09, 0x52, 0x61, 0x66, - 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x52, 0x61, 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x09, 0x52, 0x61, 0x66, 0x74, 0x49, - 0x6e, 0x64, 0x65, 0x78, 0x42, 0x8a, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, + 0x6e, 0x64, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, + 0x52, 0x49, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x55, 0x52, 0x49, 0x12, 0x3a, 0x0a, 0x0a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x66, 0x74, 0x65, + 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, + 0x3c, 0x0a, 0x0b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x58, 0x0a, + 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x0e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x49, 0x0a, 0x09, 0x52, 0x61, 0x66, 0x74, 0x49, + 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x0c, - 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x70, 0x62, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0xa2, 0x02, 0x04, 0x48, 0x43, - 0x49, 0x43, 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0xca, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x47, - 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x24, 0x48, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, - 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x61, + 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x09, 0x52, 0x61, 0x66, 0x74, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x42, 0x8a, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42, 0x0c, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x70, 0x62, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x43, + 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0xca, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x5c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x24, 0x48, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/pbconnect/connect.proto b/proto/pbconnect/connect.proto index 640987316..071f76dea 100644 --- a/proto/pbconnect/connect.proto +++ b/proto/pbconnect/connect.proto @@ -165,6 +165,10 @@ message IssuedCert { // KindURI is the cert URI value. string KindURI = 13; + // ServerURI is the URI value of a cert issued for a server agent. + // The same URI is shared by all servers in a Consul datacenter. + string ServerURI = 14; + // ValidAfter and ValidBefore are the validity periods for the // certificate. // mog: func-to=structs.TimeFromProto func-from=structs.TimeToProto diff --git a/proto/pbpeering/peering.go b/proto/pbpeering/peering.go index d31328b58..74f5a52f0 100644 --- a/proto/pbpeering/peering.go +++ b/proto/pbpeering/peering.go @@ -143,10 +143,10 @@ func PeeringStateFromAPI(t api.PeeringState) PeeringState { } func (p *Peering) IsActive() bool { - if p != nil && p.State == PeeringState_TERMINATED { + if p == nil || p.State == PeeringState_TERMINATED { return false } - if p == nil || p.DeletedAt == nil { + if p.DeletedAt == nil { return true } diff --git a/proto/pbpeering/peering.pb.binary.go b/proto/pbpeering/peering.pb.binary.go index 2e9d5c71c..499e31226 100644 --- a/proto/pbpeering/peering.pb.binary.go +++ b/proto/pbpeering/peering.pb.binary.go @@ -107,6 +107,16 @@ func (msg *PeeringTrustBundle) UnmarshalBinary(b []byte) error { return proto.Unmarshal(b, msg) } +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *PeeringServerAddresses) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *PeeringServerAddresses) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + // MarshalBinary implements encoding.BinaryMarshaler func (msg *PeeringReadRequest) MarshalBinary() ([]byte, error) { return proto.Marshal(msg) diff --git a/proto/pbpeering/peering.pb.go b/proto/pbpeering/peering.pb.go index abd0ea186..8fdff0246 100644 --- a/proto/pbpeering/peering.pb.go +++ b/proto/pbpeering/peering.pb.go @@ -568,6 +568,55 @@ func (x *PeeringTrustBundle) GetModifyIndex() uint64 { return 0 } +// PeeringServerAddresses contains the latest snapshot of all known +// server addresses for a peer. +type PeeringServerAddresses struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Addresses []string `protobuf:"bytes,1,rep,name=Addresses,proto3" json:"Addresses,omitempty"` +} + +func (x *PeeringServerAddresses) Reset() { + *x = PeeringServerAddresses{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_pbpeering_peering_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeeringServerAddresses) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeeringServerAddresses) ProtoMessage() {} + +func (x *PeeringServerAddresses) ProtoReflect() protoreflect.Message { + mi := &file_proto_pbpeering_peering_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeeringServerAddresses.ProtoReflect.Descriptor instead. +func (*PeeringServerAddresses) Descriptor() ([]byte, []int) { + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{4} +} + +func (x *PeeringServerAddresses) GetAddresses() []string { + if x != nil { + return x.Addresses + } + return nil +} + // @consul-rpc-glue: LeaderReadTODO type PeeringReadRequest struct { state protoimpl.MessageState @@ -581,7 +630,7 @@ type PeeringReadRequest struct { func (x *PeeringReadRequest) Reset() { *x = PeeringReadRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[4] + mi := &file_proto_pbpeering_peering_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -594,7 +643,7 @@ func (x *PeeringReadRequest) String() string { func (*PeeringReadRequest) ProtoMessage() {} func (x *PeeringReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[4] + mi := &file_proto_pbpeering_peering_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -607,7 +656,7 @@ func (x *PeeringReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringReadRequest.ProtoReflect.Descriptor instead. func (*PeeringReadRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{4} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{5} } func (x *PeeringReadRequest) GetName() string { @@ -635,7 +684,7 @@ type PeeringReadResponse struct { func (x *PeeringReadResponse) Reset() { *x = PeeringReadResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[5] + mi := &file_proto_pbpeering_peering_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -648,7 +697,7 @@ func (x *PeeringReadResponse) String() string { func (*PeeringReadResponse) ProtoMessage() {} func (x *PeeringReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[5] + mi := &file_proto_pbpeering_peering_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -661,7 +710,7 @@ func (x *PeeringReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringReadResponse.ProtoReflect.Descriptor instead. func (*PeeringReadResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{5} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{6} } func (x *PeeringReadResponse) GetPeering() *Peering { @@ -683,7 +732,7 @@ type PeeringListRequest struct { func (x *PeeringListRequest) Reset() { *x = PeeringListRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[6] + mi := &file_proto_pbpeering_peering_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -696,7 +745,7 @@ func (x *PeeringListRequest) String() string { func (*PeeringListRequest) ProtoMessage() {} func (x *PeeringListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[6] + mi := &file_proto_pbpeering_peering_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -709,7 +758,7 @@ func (x *PeeringListRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringListRequest.ProtoReflect.Descriptor instead. func (*PeeringListRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{6} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{7} } func (x *PeeringListRequest) GetPartition() string { @@ -730,7 +779,7 @@ type PeeringListResponse struct { func (x *PeeringListResponse) Reset() { *x = PeeringListResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[7] + mi := &file_proto_pbpeering_peering_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -743,7 +792,7 @@ func (x *PeeringListResponse) String() string { func (*PeeringListResponse) ProtoMessage() {} func (x *PeeringListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[7] + mi := &file_proto_pbpeering_peering_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -756,7 +805,7 @@ func (x *PeeringListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringListResponse.ProtoReflect.Descriptor instead. func (*PeeringListResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{7} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{8} } func (x *PeeringListResponse) GetPeerings() []*Peering { @@ -783,7 +832,7 @@ type PeeringWriteRequest struct { func (x *PeeringWriteRequest) Reset() { *x = PeeringWriteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[8] + mi := &file_proto_pbpeering_peering_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -796,7 +845,7 @@ func (x *PeeringWriteRequest) String() string { func (*PeeringWriteRequest) ProtoMessage() {} func (x *PeeringWriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[8] + mi := &file_proto_pbpeering_peering_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -809,7 +858,7 @@ func (x *PeeringWriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringWriteRequest.ProtoReflect.Descriptor instead. func (*PeeringWriteRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{8} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{9} } func (x *PeeringWriteRequest) GetPeering() *Peering { @@ -843,7 +892,7 @@ type PeeringWriteResponse struct { func (x *PeeringWriteResponse) Reset() { *x = PeeringWriteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[9] + mi := &file_proto_pbpeering_peering_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -856,7 +905,7 @@ func (x *PeeringWriteResponse) String() string { func (*PeeringWriteResponse) ProtoMessage() {} func (x *PeeringWriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[9] + mi := &file_proto_pbpeering_peering_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -869,7 +918,7 @@ func (x *PeeringWriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringWriteResponse.ProtoReflect.Descriptor instead. func (*PeeringWriteResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{9} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{10} } type PeeringDeleteRequest struct { @@ -884,7 +933,7 @@ type PeeringDeleteRequest struct { func (x *PeeringDeleteRequest) Reset() { *x = PeeringDeleteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[10] + mi := &file_proto_pbpeering_peering_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -897,7 +946,7 @@ func (x *PeeringDeleteRequest) String() string { func (*PeeringDeleteRequest) ProtoMessage() {} func (x *PeeringDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[10] + mi := &file_proto_pbpeering_peering_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -910,7 +959,7 @@ func (x *PeeringDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringDeleteRequest.ProtoReflect.Descriptor instead. func (*PeeringDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{10} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{11} } func (x *PeeringDeleteRequest) GetName() string { @@ -936,7 +985,7 @@ type PeeringDeleteResponse struct { func (x *PeeringDeleteResponse) Reset() { *x = PeeringDeleteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[11] + mi := &file_proto_pbpeering_peering_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -949,7 +998,7 @@ func (x *PeeringDeleteResponse) String() string { func (*PeeringDeleteResponse) ProtoMessage() {} func (x *PeeringDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[11] + mi := &file_proto_pbpeering_peering_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -962,7 +1011,7 @@ func (x *PeeringDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringDeleteResponse.ProtoReflect.Descriptor instead. func (*PeeringDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{11} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{12} } type TrustBundleListByServiceRequest struct { @@ -979,7 +1028,7 @@ type TrustBundleListByServiceRequest struct { func (x *TrustBundleListByServiceRequest) Reset() { *x = TrustBundleListByServiceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[12] + mi := &file_proto_pbpeering_peering_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -992,7 +1041,7 @@ func (x *TrustBundleListByServiceRequest) String() string { func (*TrustBundleListByServiceRequest) ProtoMessage() {} func (x *TrustBundleListByServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[12] + mi := &file_proto_pbpeering_peering_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1005,7 +1054,7 @@ func (x *TrustBundleListByServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleListByServiceRequest.ProtoReflect.Descriptor instead. func (*TrustBundleListByServiceRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{12} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{13} } func (x *TrustBundleListByServiceRequest) GetServiceName() string { @@ -1048,7 +1097,7 @@ type TrustBundleListByServiceResponse struct { func (x *TrustBundleListByServiceResponse) Reset() { *x = TrustBundleListByServiceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[13] + mi := &file_proto_pbpeering_peering_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1061,7 +1110,7 @@ func (x *TrustBundleListByServiceResponse) String() string { func (*TrustBundleListByServiceResponse) ProtoMessage() {} func (x *TrustBundleListByServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[13] + mi := &file_proto_pbpeering_peering_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1074,7 +1123,7 @@ func (x *TrustBundleListByServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleListByServiceResponse.ProtoReflect.Descriptor instead. func (*TrustBundleListByServiceResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{13} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{14} } func (x *TrustBundleListByServiceResponse) GetIndex() uint64 { @@ -1103,7 +1152,7 @@ type TrustBundleReadRequest struct { func (x *TrustBundleReadRequest) Reset() { *x = TrustBundleReadRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[14] + mi := &file_proto_pbpeering_peering_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1116,7 +1165,7 @@ func (x *TrustBundleReadRequest) String() string { func (*TrustBundleReadRequest) ProtoMessage() {} func (x *TrustBundleReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[14] + mi := &file_proto_pbpeering_peering_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1129,7 +1178,7 @@ func (x *TrustBundleReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleReadRequest.ProtoReflect.Descriptor instead. func (*TrustBundleReadRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{14} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{15} } func (x *TrustBundleReadRequest) GetName() string { @@ -1158,7 +1207,7 @@ type TrustBundleReadResponse struct { func (x *TrustBundleReadResponse) Reset() { *x = TrustBundleReadResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[15] + mi := &file_proto_pbpeering_peering_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1171,7 +1220,7 @@ func (x *TrustBundleReadResponse) String() string { func (*TrustBundleReadResponse) ProtoMessage() {} func (x *TrustBundleReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[15] + mi := &file_proto_pbpeering_peering_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1184,7 +1233,7 @@ func (x *TrustBundleReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustBundleReadResponse.ProtoReflect.Descriptor instead. func (*TrustBundleReadResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{15} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{16} } func (x *TrustBundleReadResponse) GetIndex() uint64 { @@ -1213,7 +1262,7 @@ type PeeringTerminateByIDRequest struct { func (x *PeeringTerminateByIDRequest) Reset() { *x = PeeringTerminateByIDRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[16] + mi := &file_proto_pbpeering_peering_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1226,7 +1275,7 @@ func (x *PeeringTerminateByIDRequest) String() string { func (*PeeringTerminateByIDRequest) ProtoMessage() {} func (x *PeeringTerminateByIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[16] + mi := &file_proto_pbpeering_peering_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1239,7 +1288,7 @@ func (x *PeeringTerminateByIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTerminateByIDRequest.ProtoReflect.Descriptor instead. func (*PeeringTerminateByIDRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{16} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{17} } func (x *PeeringTerminateByIDRequest) GetID() string { @@ -1258,7 +1307,7 @@ type PeeringTerminateByIDResponse struct { func (x *PeeringTerminateByIDResponse) Reset() { *x = PeeringTerminateByIDResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[17] + mi := &file_proto_pbpeering_peering_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1271,7 +1320,7 @@ func (x *PeeringTerminateByIDResponse) String() string { func (*PeeringTerminateByIDResponse) ProtoMessage() {} func (x *PeeringTerminateByIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[17] + mi := &file_proto_pbpeering_peering_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1284,7 +1333,7 @@ func (x *PeeringTerminateByIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTerminateByIDResponse.ProtoReflect.Descriptor instead. func (*PeeringTerminateByIDResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{17} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{18} } type PeeringTrustBundleWriteRequest struct { @@ -1298,7 +1347,7 @@ type PeeringTrustBundleWriteRequest struct { func (x *PeeringTrustBundleWriteRequest) Reset() { *x = PeeringTrustBundleWriteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[18] + mi := &file_proto_pbpeering_peering_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1311,7 +1360,7 @@ func (x *PeeringTrustBundleWriteRequest) String() string { func (*PeeringTrustBundleWriteRequest) ProtoMessage() {} func (x *PeeringTrustBundleWriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[18] + mi := &file_proto_pbpeering_peering_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1324,7 +1373,7 @@ func (x *PeeringTrustBundleWriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTrustBundleWriteRequest.ProtoReflect.Descriptor instead. func (*PeeringTrustBundleWriteRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{18} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{19} } func (x *PeeringTrustBundleWriteRequest) GetPeeringTrustBundle() *PeeringTrustBundle { @@ -1343,7 +1392,7 @@ type PeeringTrustBundleWriteResponse struct { func (x *PeeringTrustBundleWriteResponse) Reset() { *x = PeeringTrustBundleWriteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[19] + mi := &file_proto_pbpeering_peering_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1356,7 +1405,7 @@ func (x *PeeringTrustBundleWriteResponse) String() string { func (*PeeringTrustBundleWriteResponse) ProtoMessage() {} func (x *PeeringTrustBundleWriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[19] + mi := &file_proto_pbpeering_peering_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1369,7 +1418,7 @@ func (x *PeeringTrustBundleWriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTrustBundleWriteResponse.ProtoReflect.Descriptor instead. func (*PeeringTrustBundleWriteResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{19} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{20} } type PeeringTrustBundleDeleteRequest struct { @@ -1384,7 +1433,7 @@ type PeeringTrustBundleDeleteRequest struct { func (x *PeeringTrustBundleDeleteRequest) Reset() { *x = PeeringTrustBundleDeleteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[20] + mi := &file_proto_pbpeering_peering_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1397,7 +1446,7 @@ func (x *PeeringTrustBundleDeleteRequest) String() string { func (*PeeringTrustBundleDeleteRequest) ProtoMessage() {} func (x *PeeringTrustBundleDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[20] + mi := &file_proto_pbpeering_peering_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1410,7 +1459,7 @@ func (x *PeeringTrustBundleDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTrustBundleDeleteRequest.ProtoReflect.Descriptor instead. func (*PeeringTrustBundleDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{20} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{21} } func (x *PeeringTrustBundleDeleteRequest) GetName() string { @@ -1436,7 +1485,7 @@ type PeeringTrustBundleDeleteResponse struct { func (x *PeeringTrustBundleDeleteResponse) Reset() { *x = PeeringTrustBundleDeleteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[21] + mi := &file_proto_pbpeering_peering_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1449,7 +1498,7 @@ func (x *PeeringTrustBundleDeleteResponse) String() string { func (*PeeringTrustBundleDeleteResponse) ProtoMessage() {} func (x *PeeringTrustBundleDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[21] + mi := &file_proto_pbpeering_peering_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1462,7 +1511,7 @@ func (x *PeeringTrustBundleDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeeringTrustBundleDeleteResponse.ProtoReflect.Descriptor instead. func (*PeeringTrustBundleDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{21} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{22} } // mog annotation: @@ -1490,7 +1539,7 @@ type GenerateTokenRequest struct { func (x *GenerateTokenRequest) Reset() { *x = GenerateTokenRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[22] + mi := &file_proto_pbpeering_peering_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1503,7 +1552,7 @@ func (x *GenerateTokenRequest) String() string { func (*GenerateTokenRequest) ProtoMessage() {} func (x *GenerateTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[22] + mi := &file_proto_pbpeering_peering_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1516,7 +1565,7 @@ func (x *GenerateTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateTokenRequest.ProtoReflect.Descriptor instead. func (*GenerateTokenRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{22} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{23} } func (x *GenerateTokenRequest) GetPeerName() string { @@ -1565,7 +1614,7 @@ type GenerateTokenResponse struct { func (x *GenerateTokenResponse) Reset() { *x = GenerateTokenResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[23] + mi := &file_proto_pbpeering_peering_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1578,7 +1627,7 @@ func (x *GenerateTokenResponse) String() string { func (*GenerateTokenResponse) ProtoMessage() {} func (x *GenerateTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[23] + mi := &file_proto_pbpeering_peering_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1591,7 +1640,7 @@ func (x *GenerateTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GenerateTokenResponse.ProtoReflect.Descriptor instead. func (*GenerateTokenResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{23} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{24} } func (x *GenerateTokenResponse) GetPeeringToken() string { @@ -1624,7 +1673,7 @@ type EstablishRequest struct { func (x *EstablishRequest) Reset() { *x = EstablishRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[24] + mi := &file_proto_pbpeering_peering_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1637,7 +1686,7 @@ func (x *EstablishRequest) String() string { func (*EstablishRequest) ProtoMessage() {} func (x *EstablishRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[24] + mi := &file_proto_pbpeering_peering_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1650,7 +1699,7 @@ func (x *EstablishRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EstablishRequest.ProtoReflect.Descriptor instead. func (*EstablishRequest) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{24} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{25} } func (x *EstablishRequest) GetPeerName() string { @@ -1695,7 +1744,7 @@ type EstablishResponse struct { func (x *EstablishResponse) Reset() { *x = EstablishResponse{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[25] + mi := &file_proto_pbpeering_peering_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1708,7 +1757,7 @@ func (x *EstablishResponse) String() string { func (*EstablishResponse) ProtoMessage() {} func (x *EstablishResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[25] + mi := &file_proto_pbpeering_peering_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1721,7 +1770,7 @@ func (x *EstablishResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EstablishResponse.ProtoReflect.Descriptor instead. func (*EstablishResponse) Descriptor() ([]byte, []int) { - return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{25} + return file_proto_pbpeering_peering_proto_rawDescGZIP(), []int{26} } // GenerateTokenRequest encodes a request to persist a peering establishment @@ -1739,7 +1788,7 @@ type SecretsWriteRequest_GenerateTokenRequest struct { func (x *SecretsWriteRequest_GenerateTokenRequest) Reset() { *x = SecretsWriteRequest_GenerateTokenRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[26] + mi := &file_proto_pbpeering_peering_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1752,7 +1801,7 @@ func (x *SecretsWriteRequest_GenerateTokenRequest) String() string { func (*SecretsWriteRequest_GenerateTokenRequest) ProtoMessage() {} func (x *SecretsWriteRequest_GenerateTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[26] + mi := &file_proto_pbpeering_peering_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1793,7 +1842,7 @@ type SecretsWriteRequest_ExchangeSecretRequest struct { func (x *SecretsWriteRequest_ExchangeSecretRequest) Reset() { *x = SecretsWriteRequest_ExchangeSecretRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[27] + mi := &file_proto_pbpeering_peering_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1806,7 +1855,7 @@ func (x *SecretsWriteRequest_ExchangeSecretRequest) String() string { func (*SecretsWriteRequest_ExchangeSecretRequest) ProtoMessage() {} func (x *SecretsWriteRequest_ExchangeSecretRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[27] + mi := &file_proto_pbpeering_peering_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1852,7 +1901,7 @@ type SecretsWriteRequest_PromotePendingRequest struct { func (x *SecretsWriteRequest_PromotePendingRequest) Reset() { *x = SecretsWriteRequest_PromotePendingRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[28] + mi := &file_proto_pbpeering_peering_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1865,7 +1914,7 @@ func (x *SecretsWriteRequest_PromotePendingRequest) String() string { func (*SecretsWriteRequest_PromotePendingRequest) ProtoMessage() {} func (x *SecretsWriteRequest_PromotePendingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[28] + mi := &file_proto_pbpeering_peering_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1904,7 +1953,7 @@ type SecretsWriteRequest_EstablishRequest struct { func (x *SecretsWriteRequest_EstablishRequest) Reset() { *x = SecretsWriteRequest_EstablishRequest{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[29] + mi := &file_proto_pbpeering_peering_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1917,7 +1966,7 @@ func (x *SecretsWriteRequest_EstablishRequest) String() string { func (*SecretsWriteRequest_EstablishRequest) ProtoMessage() {} func (x *SecretsWriteRequest_EstablishRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[29] + mi := &file_proto_pbpeering_peering_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1952,7 +2001,7 @@ type PeeringSecrets_Establishment struct { func (x *PeeringSecrets_Establishment) Reset() { *x = PeeringSecrets_Establishment{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[30] + mi := &file_proto_pbpeering_peering_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1965,7 +2014,7 @@ func (x *PeeringSecrets_Establishment) String() string { func (*PeeringSecrets_Establishment) ProtoMessage() {} func (x *PeeringSecrets_Establishment) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[30] + mi := &file_proto_pbpeering_peering_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2011,7 +2060,7 @@ type PeeringSecrets_Stream struct { func (x *PeeringSecrets_Stream) Reset() { *x = PeeringSecrets_Stream{} if protoimpl.UnsafeEnabled { - mi := &file_proto_pbpeering_peering_proto_msgTypes[31] + mi := &file_proto_pbpeering_peering_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2024,7 +2073,7 @@ func (x *PeeringSecrets_Stream) String() string { func (*PeeringSecrets_Stream) ProtoMessage() {} func (x *PeeringSecrets_Stream) ProtoReflect() protoreflect.Message { - mi := &file_proto_pbpeering_peering_proto_msgTypes[31] + mi := &file_proto_pbpeering_peering_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2197,241 +2246,244 @@ var file_proto_pbpeering_peering_proto_rawDesc = []byte{ 0x6e, 0x64, 0x65, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4d, 0x6f, - 0x64, 0x69, 0x66, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x46, 0x0a, 0x12, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0x5b, 0x0a, 0x13, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, - 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, - 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x32, - 0x0a, 0x12, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x08, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, - 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, - 0x73, 0x22, 0xca, 0x02, 0x0a, 0x13, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x44, 0x0a, 0x07, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x12, - 0x5e, 0x0a, 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x73, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, - 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x16, - 0x0a, 0x14, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x0a, 0x14, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x17, 0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x1f, 0x54, 0x72, - 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, - 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4b, - 0x69, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x22, - 0x89, 0x01, 0x0a, 0x20, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, - 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4f, 0x0a, 0x07, 0x42, 0x75, - 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, - 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x52, 0x07, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x16, 0x54, - 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, - 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, - 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7e, 0x0a, 0x17, 0x54, 0x72, 0x75, 0x73, 0x74, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4d, 0x0a, 0x06, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, - 0x06, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x2d, 0x0a, 0x1b, 0x50, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x42, 0x79, 0x49, 0x44, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x87, 0x01, 0x0a, 0x1e, 0x50, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x57, 0x72, 0x69, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x65, 0x0a, 0x12, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x64, 0x69, 0x66, 0x79, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x36, 0x0a, 0x16, 0x50, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, + 0x73, 0x22, 0x46, 0x0a, 0x12, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x5b, 0x0a, 0x13, 0x50, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x44, 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x50, + 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x22, 0x32, 0x0a, 0x12, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x50, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x46, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, + 0x08, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x22, 0xca, 0x02, 0x0a, 0x13, 0x50, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x44, 0x0a, 0x07, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, + 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x07, + 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x5e, 0x0a, 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x0e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x54, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x12, 0x50, 0x65, - 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, - 0x22, 0x21, 0x0a, 0x1f, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x53, 0x0a, 0x1f, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, - 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, + 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x16, 0x0a, 0x14, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, + 0x0a, 0x14, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, - 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x22, 0x0a, 0x20, 0x50, 0x65, 0x65, 0x72, - 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9a, 0x02, 0x0a, - 0x14, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x17, 0x0a, 0x15, 0x50, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x93, 0x01, 0x0a, 0x1f, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, + 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4e, 0x61, 0x6d, 0x65, + 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x22, 0x89, 0x01, 0x0a, 0x20, 0x54, 0x72, 0x75, 0x73, + 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x49, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x12, 0x4f, 0x0a, 0x07, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, + 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x07, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x16, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x55, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x7e, 0x0a, 0x17, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x49, 0x6e, + 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x12, 0x4d, 0x0a, 0x06, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, + 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x06, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x22, + 0x2d, 0x0a, 0x1b, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, + 0x61, 0x74, 0x65, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x22, 0x1e, + 0x0a, 0x1c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, + 0x74, 0x65, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x87, + 0x01, 0x0a, 0x1e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x65, 0x0a, 0x12, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, + 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x38, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, - 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, - 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x3b, 0x0a, 0x15, 0x47, 0x65, 0x6e, - 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xfc, 0x01, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x61, 0x62, - 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, - 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, - 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x50, - 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x50, - 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x51, 0x0a, 0x04, 0x4d, 0x65, 0x74, - 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, - 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x45, 0x73, 0x74, 0x61, - 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, - 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x13, 0x0a, 0x11, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, - 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x73, 0x0a, 0x0c, 0x50, 0x65, - 0x65, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, - 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, - 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x53, 0x54, 0x41, 0x42, 0x4c, - 0x49, 0x53, 0x48, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, - 0x56, 0x45, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x41, 0x49, 0x4c, 0x49, 0x4e, 0x47, 0x10, - 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, - 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x52, 0x4d, 0x49, 0x4e, 0x41, 0x54, 0x45, 0x44, 0x10, 0x06, 0x32, - 0xc0, 0x08, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x09, 0x45, 0x73, 0x74, 0x61, 0x62, - 0x6c, 0x69, 0x73, 0x68, 0x12, 0x33, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, - 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x45, 0x73, - 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x7c, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x12, 0x35, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, + 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x12, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, + 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x50, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x57, 0x72, + 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x53, 0x0a, 0x1f, 0x50, + 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, + 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x22, 0x0a, 0x20, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x72, 0x75, 0x73, 0x74, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9a, 0x02, 0x0a, 0x14, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, + 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x55, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, - 0x0b, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x35, 0x2e, 0x68, + 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, + 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x38, + 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x17, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x3b, 0x0a, 0x15, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xfc, + 0x01, 0x0a, 0x10, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x65, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x22, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x51, 0x0a, 0x04, 0x4d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x3d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x2e, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, + 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x4d, 0x65, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x13, 0x0a, + 0x11, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2a, 0x73, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x10, + 0x0a, 0x0c, 0x45, 0x53, 0x54, 0x41, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, + 0x46, 0x41, 0x49, 0x4c, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x45, 0x4c, + 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x45, 0x52, 0x4d, 0x49, + 0x4e, 0x41, 0x54, 0x45, 0x44, 0x10, 0x06, 0x32, 0xc0, 0x08, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x0d, 0x47, + 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, - 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, - 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x0d, - 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x37, 0x2e, - 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, - 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x76, 0x0a, 0x09, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x12, 0x33, 0x2e, 0x68, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, + 0x2e, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x34, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, + 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x12, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x7f, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x12, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0xa3, 0x01, 0x0a, 0x18, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x42, - 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, - 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, - 0x6e, 0x67, 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, - 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, - 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x0f, 0x54, 0x72, 0x75, 0x73, - 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x12, 0x39, 0x2e, 0x68, 0x61, + 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x0b, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x35, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, - 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, + 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x36, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x37, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x57, 0x72, 0x69, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0xa3, 0x01, 0x0a, 0x18, 0x54, 0x72, + 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x42, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x8a, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x42, 0x0c, 0x50, 0x65, - 0x65, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, - 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x70, 0x62, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x50, - 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x69, 0x6e, 0x67, 0xca, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, - 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x5c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x5c, 0x47, 0x50, 0x42, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x24, 0x48, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x43, 0x2e, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x54, + 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x79, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x88, 0x01, 0x0a, 0x0f, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, + 0x65, 0x61, 0x64, 0x12, 0x39, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, + 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, 0x72, 0x69, + 0x6e, 0x67, 0x2e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x8a, 0x02, 0x0a, 0x25, 0x63, + 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x42, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x70, 0x65, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x50, 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0xca, 0x02, 0x21, 0x48, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, + 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x50, 0x65, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x24, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x3a, 0x3a, + 0x50, 0x65, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2447,83 +2499,84 @@ func file_proto_pbpeering_peering_proto_rawDescGZIP() []byte { } var file_proto_pbpeering_peering_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_pbpeering_peering_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_proto_pbpeering_peering_proto_msgTypes = make([]protoimpl.MessageInfo, 37) var file_proto_pbpeering_peering_proto_goTypes = []interface{}{ (PeeringState)(0), // 0: hashicorp.consul.internal.peering.PeeringState (*SecretsWriteRequest)(nil), // 1: hashicorp.consul.internal.peering.SecretsWriteRequest (*PeeringSecrets)(nil), // 2: hashicorp.consul.internal.peering.PeeringSecrets (*Peering)(nil), // 3: hashicorp.consul.internal.peering.Peering (*PeeringTrustBundle)(nil), // 4: hashicorp.consul.internal.peering.PeeringTrustBundle - (*PeeringReadRequest)(nil), // 5: hashicorp.consul.internal.peering.PeeringReadRequest - (*PeeringReadResponse)(nil), // 6: hashicorp.consul.internal.peering.PeeringReadResponse - (*PeeringListRequest)(nil), // 7: hashicorp.consul.internal.peering.PeeringListRequest - (*PeeringListResponse)(nil), // 8: hashicorp.consul.internal.peering.PeeringListResponse - (*PeeringWriteRequest)(nil), // 9: hashicorp.consul.internal.peering.PeeringWriteRequest - (*PeeringWriteResponse)(nil), // 10: hashicorp.consul.internal.peering.PeeringWriteResponse - (*PeeringDeleteRequest)(nil), // 11: hashicorp.consul.internal.peering.PeeringDeleteRequest - (*PeeringDeleteResponse)(nil), // 12: hashicorp.consul.internal.peering.PeeringDeleteResponse - (*TrustBundleListByServiceRequest)(nil), // 13: hashicorp.consul.internal.peering.TrustBundleListByServiceRequest - (*TrustBundleListByServiceResponse)(nil), // 14: hashicorp.consul.internal.peering.TrustBundleListByServiceResponse - (*TrustBundleReadRequest)(nil), // 15: hashicorp.consul.internal.peering.TrustBundleReadRequest - (*TrustBundleReadResponse)(nil), // 16: hashicorp.consul.internal.peering.TrustBundleReadResponse - (*PeeringTerminateByIDRequest)(nil), // 17: hashicorp.consul.internal.peering.PeeringTerminateByIDRequest - (*PeeringTerminateByIDResponse)(nil), // 18: hashicorp.consul.internal.peering.PeeringTerminateByIDResponse - (*PeeringTrustBundleWriteRequest)(nil), // 19: hashicorp.consul.internal.peering.PeeringTrustBundleWriteRequest - (*PeeringTrustBundleWriteResponse)(nil), // 20: hashicorp.consul.internal.peering.PeeringTrustBundleWriteResponse - (*PeeringTrustBundleDeleteRequest)(nil), // 21: hashicorp.consul.internal.peering.PeeringTrustBundleDeleteRequest - (*PeeringTrustBundleDeleteResponse)(nil), // 22: hashicorp.consul.internal.peering.PeeringTrustBundleDeleteResponse - (*GenerateTokenRequest)(nil), // 23: hashicorp.consul.internal.peering.GenerateTokenRequest - (*GenerateTokenResponse)(nil), // 24: hashicorp.consul.internal.peering.GenerateTokenResponse - (*EstablishRequest)(nil), // 25: hashicorp.consul.internal.peering.EstablishRequest - (*EstablishResponse)(nil), // 26: hashicorp.consul.internal.peering.EstablishResponse - (*SecretsWriteRequest_GenerateTokenRequest)(nil), // 27: hashicorp.consul.internal.peering.SecretsWriteRequest.GenerateTokenRequest - (*SecretsWriteRequest_ExchangeSecretRequest)(nil), // 28: hashicorp.consul.internal.peering.SecretsWriteRequest.ExchangeSecretRequest - (*SecretsWriteRequest_PromotePendingRequest)(nil), // 29: hashicorp.consul.internal.peering.SecretsWriteRequest.PromotePendingRequest - (*SecretsWriteRequest_EstablishRequest)(nil), // 30: hashicorp.consul.internal.peering.SecretsWriteRequest.EstablishRequest - (*PeeringSecrets_Establishment)(nil), // 31: hashicorp.consul.internal.peering.PeeringSecrets.Establishment - (*PeeringSecrets_Stream)(nil), // 32: hashicorp.consul.internal.peering.PeeringSecrets.Stream - nil, // 33: hashicorp.consul.internal.peering.Peering.MetaEntry - nil, // 34: hashicorp.consul.internal.peering.PeeringWriteRequest.MetaEntry - nil, // 35: hashicorp.consul.internal.peering.GenerateTokenRequest.MetaEntry - nil, // 36: hashicorp.consul.internal.peering.EstablishRequest.MetaEntry - (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp + (*PeeringServerAddresses)(nil), // 5: hashicorp.consul.internal.peering.PeeringServerAddresses + (*PeeringReadRequest)(nil), // 6: hashicorp.consul.internal.peering.PeeringReadRequest + (*PeeringReadResponse)(nil), // 7: hashicorp.consul.internal.peering.PeeringReadResponse + (*PeeringListRequest)(nil), // 8: hashicorp.consul.internal.peering.PeeringListRequest + (*PeeringListResponse)(nil), // 9: hashicorp.consul.internal.peering.PeeringListResponse + (*PeeringWriteRequest)(nil), // 10: hashicorp.consul.internal.peering.PeeringWriteRequest + (*PeeringWriteResponse)(nil), // 11: hashicorp.consul.internal.peering.PeeringWriteResponse + (*PeeringDeleteRequest)(nil), // 12: hashicorp.consul.internal.peering.PeeringDeleteRequest + (*PeeringDeleteResponse)(nil), // 13: hashicorp.consul.internal.peering.PeeringDeleteResponse + (*TrustBundleListByServiceRequest)(nil), // 14: hashicorp.consul.internal.peering.TrustBundleListByServiceRequest + (*TrustBundleListByServiceResponse)(nil), // 15: hashicorp.consul.internal.peering.TrustBundleListByServiceResponse + (*TrustBundleReadRequest)(nil), // 16: hashicorp.consul.internal.peering.TrustBundleReadRequest + (*TrustBundleReadResponse)(nil), // 17: hashicorp.consul.internal.peering.TrustBundleReadResponse + (*PeeringTerminateByIDRequest)(nil), // 18: hashicorp.consul.internal.peering.PeeringTerminateByIDRequest + (*PeeringTerminateByIDResponse)(nil), // 19: hashicorp.consul.internal.peering.PeeringTerminateByIDResponse + (*PeeringTrustBundleWriteRequest)(nil), // 20: hashicorp.consul.internal.peering.PeeringTrustBundleWriteRequest + (*PeeringTrustBundleWriteResponse)(nil), // 21: hashicorp.consul.internal.peering.PeeringTrustBundleWriteResponse + (*PeeringTrustBundleDeleteRequest)(nil), // 22: hashicorp.consul.internal.peering.PeeringTrustBundleDeleteRequest + (*PeeringTrustBundleDeleteResponse)(nil), // 23: hashicorp.consul.internal.peering.PeeringTrustBundleDeleteResponse + (*GenerateTokenRequest)(nil), // 24: hashicorp.consul.internal.peering.GenerateTokenRequest + (*GenerateTokenResponse)(nil), // 25: hashicorp.consul.internal.peering.GenerateTokenResponse + (*EstablishRequest)(nil), // 26: hashicorp.consul.internal.peering.EstablishRequest + (*EstablishResponse)(nil), // 27: hashicorp.consul.internal.peering.EstablishResponse + (*SecretsWriteRequest_GenerateTokenRequest)(nil), // 28: hashicorp.consul.internal.peering.SecretsWriteRequest.GenerateTokenRequest + (*SecretsWriteRequest_ExchangeSecretRequest)(nil), // 29: hashicorp.consul.internal.peering.SecretsWriteRequest.ExchangeSecretRequest + (*SecretsWriteRequest_PromotePendingRequest)(nil), // 30: hashicorp.consul.internal.peering.SecretsWriteRequest.PromotePendingRequest + (*SecretsWriteRequest_EstablishRequest)(nil), // 31: hashicorp.consul.internal.peering.SecretsWriteRequest.EstablishRequest + (*PeeringSecrets_Establishment)(nil), // 32: hashicorp.consul.internal.peering.PeeringSecrets.Establishment + (*PeeringSecrets_Stream)(nil), // 33: hashicorp.consul.internal.peering.PeeringSecrets.Stream + nil, // 34: hashicorp.consul.internal.peering.Peering.MetaEntry + nil, // 35: hashicorp.consul.internal.peering.PeeringWriteRequest.MetaEntry + nil, // 36: hashicorp.consul.internal.peering.GenerateTokenRequest.MetaEntry + nil, // 37: hashicorp.consul.internal.peering.EstablishRequest.MetaEntry + (*timestamppb.Timestamp)(nil), // 38: google.protobuf.Timestamp } var file_proto_pbpeering_peering_proto_depIdxs = []int32{ - 27, // 0: hashicorp.consul.internal.peering.SecretsWriteRequest.generate_token:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.GenerateTokenRequest - 28, // 1: hashicorp.consul.internal.peering.SecretsWriteRequest.exchange_secret:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.ExchangeSecretRequest - 29, // 2: hashicorp.consul.internal.peering.SecretsWriteRequest.promote_pending:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.PromotePendingRequest - 30, // 3: hashicorp.consul.internal.peering.SecretsWriteRequest.establish:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.EstablishRequest - 31, // 4: hashicorp.consul.internal.peering.PeeringSecrets.establishment:type_name -> hashicorp.consul.internal.peering.PeeringSecrets.Establishment - 32, // 5: hashicorp.consul.internal.peering.PeeringSecrets.stream:type_name -> hashicorp.consul.internal.peering.PeeringSecrets.Stream - 37, // 6: hashicorp.consul.internal.peering.Peering.DeletedAt:type_name -> google.protobuf.Timestamp - 33, // 7: hashicorp.consul.internal.peering.Peering.Meta:type_name -> hashicorp.consul.internal.peering.Peering.MetaEntry + 28, // 0: hashicorp.consul.internal.peering.SecretsWriteRequest.generate_token:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.GenerateTokenRequest + 29, // 1: hashicorp.consul.internal.peering.SecretsWriteRequest.exchange_secret:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.ExchangeSecretRequest + 30, // 2: hashicorp.consul.internal.peering.SecretsWriteRequest.promote_pending:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.PromotePendingRequest + 31, // 3: hashicorp.consul.internal.peering.SecretsWriteRequest.establish:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest.EstablishRequest + 32, // 4: hashicorp.consul.internal.peering.PeeringSecrets.establishment:type_name -> hashicorp.consul.internal.peering.PeeringSecrets.Establishment + 33, // 5: hashicorp.consul.internal.peering.PeeringSecrets.stream:type_name -> hashicorp.consul.internal.peering.PeeringSecrets.Stream + 38, // 6: hashicorp.consul.internal.peering.Peering.DeletedAt:type_name -> google.protobuf.Timestamp + 34, // 7: hashicorp.consul.internal.peering.Peering.Meta:type_name -> hashicorp.consul.internal.peering.Peering.MetaEntry 0, // 8: hashicorp.consul.internal.peering.Peering.State:type_name -> hashicorp.consul.internal.peering.PeeringState 3, // 9: hashicorp.consul.internal.peering.PeeringReadResponse.Peering:type_name -> hashicorp.consul.internal.peering.Peering 3, // 10: hashicorp.consul.internal.peering.PeeringListResponse.Peerings:type_name -> hashicorp.consul.internal.peering.Peering 3, // 11: hashicorp.consul.internal.peering.PeeringWriteRequest.Peering:type_name -> hashicorp.consul.internal.peering.Peering 1, // 12: hashicorp.consul.internal.peering.PeeringWriteRequest.SecretsRequest:type_name -> hashicorp.consul.internal.peering.SecretsWriteRequest - 34, // 13: hashicorp.consul.internal.peering.PeeringWriteRequest.Meta:type_name -> hashicorp.consul.internal.peering.PeeringWriteRequest.MetaEntry + 35, // 13: hashicorp.consul.internal.peering.PeeringWriteRequest.Meta:type_name -> hashicorp.consul.internal.peering.PeeringWriteRequest.MetaEntry 4, // 14: hashicorp.consul.internal.peering.TrustBundleListByServiceResponse.Bundles:type_name -> hashicorp.consul.internal.peering.PeeringTrustBundle 4, // 15: hashicorp.consul.internal.peering.TrustBundleReadResponse.Bundle:type_name -> hashicorp.consul.internal.peering.PeeringTrustBundle 4, // 16: hashicorp.consul.internal.peering.PeeringTrustBundleWriteRequest.PeeringTrustBundle:type_name -> hashicorp.consul.internal.peering.PeeringTrustBundle - 35, // 17: hashicorp.consul.internal.peering.GenerateTokenRequest.Meta:type_name -> hashicorp.consul.internal.peering.GenerateTokenRequest.MetaEntry - 36, // 18: hashicorp.consul.internal.peering.EstablishRequest.Meta:type_name -> hashicorp.consul.internal.peering.EstablishRequest.MetaEntry - 23, // 19: hashicorp.consul.internal.peering.PeeringService.GenerateToken:input_type -> hashicorp.consul.internal.peering.GenerateTokenRequest - 25, // 20: hashicorp.consul.internal.peering.PeeringService.Establish:input_type -> hashicorp.consul.internal.peering.EstablishRequest - 5, // 21: hashicorp.consul.internal.peering.PeeringService.PeeringRead:input_type -> hashicorp.consul.internal.peering.PeeringReadRequest - 7, // 22: hashicorp.consul.internal.peering.PeeringService.PeeringList:input_type -> hashicorp.consul.internal.peering.PeeringListRequest - 11, // 23: hashicorp.consul.internal.peering.PeeringService.PeeringDelete:input_type -> hashicorp.consul.internal.peering.PeeringDeleteRequest - 9, // 24: hashicorp.consul.internal.peering.PeeringService.PeeringWrite:input_type -> hashicorp.consul.internal.peering.PeeringWriteRequest - 13, // 25: hashicorp.consul.internal.peering.PeeringService.TrustBundleListByService:input_type -> hashicorp.consul.internal.peering.TrustBundleListByServiceRequest - 15, // 26: hashicorp.consul.internal.peering.PeeringService.TrustBundleRead:input_type -> hashicorp.consul.internal.peering.TrustBundleReadRequest - 24, // 27: hashicorp.consul.internal.peering.PeeringService.GenerateToken:output_type -> hashicorp.consul.internal.peering.GenerateTokenResponse - 26, // 28: hashicorp.consul.internal.peering.PeeringService.Establish:output_type -> hashicorp.consul.internal.peering.EstablishResponse - 6, // 29: hashicorp.consul.internal.peering.PeeringService.PeeringRead:output_type -> hashicorp.consul.internal.peering.PeeringReadResponse - 8, // 30: hashicorp.consul.internal.peering.PeeringService.PeeringList:output_type -> hashicorp.consul.internal.peering.PeeringListResponse - 12, // 31: hashicorp.consul.internal.peering.PeeringService.PeeringDelete:output_type -> hashicorp.consul.internal.peering.PeeringDeleteResponse - 10, // 32: hashicorp.consul.internal.peering.PeeringService.PeeringWrite:output_type -> hashicorp.consul.internal.peering.PeeringWriteResponse - 14, // 33: hashicorp.consul.internal.peering.PeeringService.TrustBundleListByService:output_type -> hashicorp.consul.internal.peering.TrustBundleListByServiceResponse - 16, // 34: hashicorp.consul.internal.peering.PeeringService.TrustBundleRead:output_type -> hashicorp.consul.internal.peering.TrustBundleReadResponse + 36, // 17: hashicorp.consul.internal.peering.GenerateTokenRequest.Meta:type_name -> hashicorp.consul.internal.peering.GenerateTokenRequest.MetaEntry + 37, // 18: hashicorp.consul.internal.peering.EstablishRequest.Meta:type_name -> hashicorp.consul.internal.peering.EstablishRequest.MetaEntry + 24, // 19: hashicorp.consul.internal.peering.PeeringService.GenerateToken:input_type -> hashicorp.consul.internal.peering.GenerateTokenRequest + 26, // 20: hashicorp.consul.internal.peering.PeeringService.Establish:input_type -> hashicorp.consul.internal.peering.EstablishRequest + 6, // 21: hashicorp.consul.internal.peering.PeeringService.PeeringRead:input_type -> hashicorp.consul.internal.peering.PeeringReadRequest + 8, // 22: hashicorp.consul.internal.peering.PeeringService.PeeringList:input_type -> hashicorp.consul.internal.peering.PeeringListRequest + 12, // 23: hashicorp.consul.internal.peering.PeeringService.PeeringDelete:input_type -> hashicorp.consul.internal.peering.PeeringDeleteRequest + 10, // 24: hashicorp.consul.internal.peering.PeeringService.PeeringWrite:input_type -> hashicorp.consul.internal.peering.PeeringWriteRequest + 14, // 25: hashicorp.consul.internal.peering.PeeringService.TrustBundleListByService:input_type -> hashicorp.consul.internal.peering.TrustBundleListByServiceRequest + 16, // 26: hashicorp.consul.internal.peering.PeeringService.TrustBundleRead:input_type -> hashicorp.consul.internal.peering.TrustBundleReadRequest + 25, // 27: hashicorp.consul.internal.peering.PeeringService.GenerateToken:output_type -> hashicorp.consul.internal.peering.GenerateTokenResponse + 27, // 28: hashicorp.consul.internal.peering.PeeringService.Establish:output_type -> hashicorp.consul.internal.peering.EstablishResponse + 7, // 29: hashicorp.consul.internal.peering.PeeringService.PeeringRead:output_type -> hashicorp.consul.internal.peering.PeeringReadResponse + 9, // 30: hashicorp.consul.internal.peering.PeeringService.PeeringList:output_type -> hashicorp.consul.internal.peering.PeeringListResponse + 13, // 31: hashicorp.consul.internal.peering.PeeringService.PeeringDelete:output_type -> hashicorp.consul.internal.peering.PeeringDeleteResponse + 11, // 32: hashicorp.consul.internal.peering.PeeringService.PeeringWrite:output_type -> hashicorp.consul.internal.peering.PeeringWriteResponse + 15, // 33: hashicorp.consul.internal.peering.PeeringService.TrustBundleListByService:output_type -> hashicorp.consul.internal.peering.TrustBundleListByServiceResponse + 17, // 34: hashicorp.consul.internal.peering.PeeringService.TrustBundleRead:output_type -> hashicorp.consul.internal.peering.TrustBundleReadResponse 27, // [27:35] is the sub-list for method output_type 19, // [19:27] is the sub-list for method input_type 19, // [19:19] is the sub-list for extension type_name @@ -2586,7 +2639,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringReadRequest); i { + switch v := v.(*PeeringServerAddresses); i { case 0: return &v.state case 1: @@ -2598,7 +2651,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringReadResponse); i { + switch v := v.(*PeeringReadRequest); i { case 0: return &v.state case 1: @@ -2610,7 +2663,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringListRequest); i { + switch v := v.(*PeeringReadResponse); i { case 0: return &v.state case 1: @@ -2622,7 +2675,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringListResponse); i { + switch v := v.(*PeeringListRequest); i { case 0: return &v.state case 1: @@ -2634,7 +2687,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringWriteRequest); i { + switch v := v.(*PeeringListResponse); i { case 0: return &v.state case 1: @@ -2646,7 +2699,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringWriteResponse); i { + switch v := v.(*PeeringWriteRequest); i { case 0: return &v.state case 1: @@ -2658,7 +2711,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringDeleteRequest); i { + switch v := v.(*PeeringWriteResponse); i { case 0: return &v.state case 1: @@ -2670,7 +2723,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringDeleteResponse); i { + switch v := v.(*PeeringDeleteRequest); i { case 0: return &v.state case 1: @@ -2682,7 +2735,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustBundleListByServiceRequest); i { + switch v := v.(*PeeringDeleteResponse); i { case 0: return &v.state case 1: @@ -2694,7 +2747,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustBundleListByServiceResponse); i { + switch v := v.(*TrustBundleListByServiceRequest); i { case 0: return &v.state case 1: @@ -2706,7 +2759,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustBundleReadRequest); i { + switch v := v.(*TrustBundleListByServiceResponse); i { case 0: return &v.state case 1: @@ -2718,7 +2771,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TrustBundleReadResponse); i { + switch v := v.(*TrustBundleReadRequest); i { case 0: return &v.state case 1: @@ -2730,7 +2783,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTerminateByIDRequest); i { + switch v := v.(*TrustBundleReadResponse); i { case 0: return &v.state case 1: @@ -2742,7 +2795,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTerminateByIDResponse); i { + switch v := v.(*PeeringTerminateByIDRequest); i { case 0: return &v.state case 1: @@ -2754,7 +2807,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTrustBundleWriteRequest); i { + switch v := v.(*PeeringTerminateByIDResponse); i { case 0: return &v.state case 1: @@ -2766,7 +2819,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTrustBundleWriteResponse); i { + switch v := v.(*PeeringTrustBundleWriteRequest); i { case 0: return &v.state case 1: @@ -2778,7 +2831,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTrustBundleDeleteRequest); i { + switch v := v.(*PeeringTrustBundleWriteResponse); i { case 0: return &v.state case 1: @@ -2790,7 +2843,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringTrustBundleDeleteResponse); i { + switch v := v.(*PeeringTrustBundleDeleteRequest); i { case 0: return &v.state case 1: @@ -2802,7 +2855,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GenerateTokenRequest); i { + switch v := v.(*PeeringTrustBundleDeleteResponse); i { case 0: return &v.state case 1: @@ -2814,7 +2867,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GenerateTokenResponse); i { + switch v := v.(*GenerateTokenRequest); i { case 0: return &v.state case 1: @@ -2826,7 +2879,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EstablishRequest); i { + switch v := v.(*GenerateTokenResponse); i { case 0: return &v.state case 1: @@ -2838,7 +2891,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EstablishResponse); i { + switch v := v.(*EstablishRequest); i { case 0: return &v.state case 1: @@ -2850,7 +2903,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SecretsWriteRequest_GenerateTokenRequest); i { + switch v := v.(*EstablishResponse); i { case 0: return &v.state case 1: @@ -2862,7 +2915,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SecretsWriteRequest_ExchangeSecretRequest); i { + switch v := v.(*SecretsWriteRequest_GenerateTokenRequest); i { case 0: return &v.state case 1: @@ -2874,7 +2927,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SecretsWriteRequest_PromotePendingRequest); i { + switch v := v.(*SecretsWriteRequest_ExchangeSecretRequest); i { case 0: return &v.state case 1: @@ -2886,7 +2939,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SecretsWriteRequest_EstablishRequest); i { + switch v := v.(*SecretsWriteRequest_PromotePendingRequest); i { case 0: return &v.state case 1: @@ -2898,7 +2951,7 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeeringSecrets_Establishment); i { + switch v := v.(*SecretsWriteRequest_EstablishRequest); i { case 0: return &v.state case 1: @@ -2910,6 +2963,18 @@ func file_proto_pbpeering_peering_proto_init() { } } file_proto_pbpeering_peering_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeeringSecrets_Establishment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_pbpeering_peering_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PeeringSecrets_Stream); i { case 0: return &v.state @@ -2934,7 +2999,7 @@ func file_proto_pbpeering_peering_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_pbpeering_peering_proto_rawDesc, NumEnums: 1, - NumMessages: 36, + NumMessages: 37, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/pbpeering/peering.proto b/proto/pbpeering/peering.proto index 4283c3c32..cc37c6041 100644 --- a/proto/pbpeering/peering.proto +++ b/proto/pbpeering/peering.proto @@ -225,6 +225,12 @@ message PeeringTrustBundle { uint64 ModifyIndex = 7; } +// PeeringServerAddresses contains the latest snapshot of all known +// server addresses for a peer. +message PeeringServerAddresses { + repeated string Addresses = 1; +} + // @consul-rpc-glue: LeaderReadTODO message PeeringReadRequest { string Name = 1; diff --git a/proto/pbpeerstream/types.go b/proto/pbpeerstream/types.go index df300cccd..4bf114c0e 100644 --- a/proto/pbpeerstream/types.go +++ b/proto/pbpeerstream/types.go @@ -3,13 +3,14 @@ package pbpeerstream const ( apiTypePrefix = "type.googleapis.com/" - TypeURLExportedService = apiTypePrefix + "hashicorp.consul.internal.peerstream.ExportedService" - TypeURLPeeringTrustBundle = apiTypePrefix + "hashicorp.consul.internal.peering.PeeringTrustBundle" + TypeURLExportedService = apiTypePrefix + "hashicorp.consul.internal.peerstream.ExportedService" + TypeURLPeeringTrustBundle = apiTypePrefix + "hashicorp.consul.internal.peering.PeeringTrustBundle" + TypeURLPeeringServerAddresses = apiTypePrefix + "hashicorp.consul.internal.peering.PeeringServerAddresses" ) func KnownTypeURL(s string) bool { switch s { - case TypeURLExportedService, TypeURLPeeringTrustBundle: + case TypeURLExportedService, TypeURLPeeringTrustBundle, TypeURLPeeringServerAddresses: return true } return false diff --git a/proto/prototest/testing.go b/proto/prototest/testing.go index 275d8502b..bf25fb0a1 100644 --- a/proto/prototest/testing.go +++ b/proto/prototest/testing.go @@ -57,7 +57,7 @@ func AssertElementsMatch[V any]( } } - if len(outX) == len(outY) && len(outX) == len(listX) { + if len(outX) == len(outY) && len(listX) == len(listY) { return // matches } diff --git a/test/integration/connect/envoy/Dockerfile-consul-envoy b/test/integration/connect/envoy/Dockerfile-consul-envoy index b6d5b3e8e..41941a336 100644 --- a/test/integration/connect/envoy/Dockerfile-consul-envoy +++ b/test/integration/connect/envoy/Dockerfile-consul-envoy @@ -1,7 +1,7 @@ # Note this arg has to be before the first FROM ARG ENVOY_VERSION -FROM consul-dev as consul +FROM consul:local as consul FROM docker.mirror.hashicorp.services/envoyproxy/envoy:v${ENVOY_VERSION} COPY --from=consul /bin/consul /bin/consul diff --git a/test/integration/connect/envoy/README.md b/test/integration/connect/envoy/README.md new file mode 100644 index 000000000..a97acc710 --- /dev/null +++ b/test/integration/connect/envoy/README.md @@ -0,0 +1,68 @@ +# Envoy Integration Tests + +## Overview + +These tests validate that Consul is configuring Envoy correctly. They set up various scenarios using Docker containers and then run +[Bats](https://github.com/sstephenson/bats) (a Bash test framework) tests to validate the expected results. + +## Running Tests + +To run the tests locally, `cd` into the root of the repo and run: + +```console +make test-envoy-integ +``` + +To run a specific test, run: + +```console +make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/case-basic" +``` + +Where `case-basic` can be replaced by any directory name from this directory. + +## How Do These Tests Work + +1. The tests are all run through Go test via the `main_test.go` file. Each directory prefixed by `case-` is a subtest, for example, +`TestEnvoy/case-basic` and `TestEnvoy/case-wanfed-gw`. +2. The real framework for this test suite lives in `run-tests.sh`. Under the hood, `main_test.go` just runs `run-tests.sh` with + various arguments. +3. The tests use your local code by building a Docker image from your local directory just before executing. + _Note:_ this is implemented as the `docker-envoy-integ` Makefile target which is a prerequisite to the `test-envoy-integ` target, + so if you are running the tests by invoking `run-tests.sh` or `go test` manually, be sure to rebuild the Docker image to ensure + you are running your latest code. +4. The tests run Docker containers connected by a shared Docker network. All tests have at least one Consul server running and then + depending on the test case they will spin up additional services or gateways. Some tests run multiple Consul servers to test + multi-DC setups. See the [`case-wanfed-gateway` test](./case-wanfed-gw) for an example of this. +5. At a high level, tests are set up by executing the `setup.sh` script in each directory. This script uses helper functions + defined in `helpers.bash`. Once the test case is set up, the validations in `verify.bats` are run. +6. If there exists a `vars.sh` file in the top-level of the case directory, the test runner will source it prior to invoking + the `run_tests`, `test_teardown` and `capture_logs` phases of the test scenario. +7. If there exists a `capture.sh` file in the top-level of the case directory, it will be executed after the test is done, but prior to + the containers being removed. This is useful for capturing logs or Envoy snapshots for debugging test failures. +8. Any files matching the `*.hcl` glob will be copied to the container `$WORKDIR/$CLUSTER/consul` directory prior to running the tests. + This is useful for defining Consul configuration for each agent process to load on start up. +9. In CI, the tests are executed against different Envoy versions and with both `XDS_TARGET=client` and `XDS_TARGET=server`. + If set to `client`, a Consul server and client are run, and services are registered against the client. If set to `server`, + only a Consul server is run, and services are registered against the server. By default, `XDS_TARGET` is set to `server`. + See [this comment](https://github.com/hashicorp/consul/blob/70bb6a2abdbc5ed4a6e728e8da243c5394a631d1/test/integration/connect/envoy/run-tests.sh#L178-L212) for more information. + +## Investigating Test Failures + +* When tests fail in CI, logs and additional debugging data are available in the artifacts of the test run. +* You can re-run the tests locally by running `make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/"` where `` is + replaced with the name of the directory, e.g. `case-basic`. +* Locally, all the logs of the failed test will be available in `workdir` in this directory. +* You can run with `DEBUG=1` to print out all the commands being run, e.g. `DEBUG=1 make test-envoy-integ GO_TEST_FLAGS="-run TestEnvoy/case-basic"`. +* If you want to prevent the Docker containers from being spun down after test failure, add a `sleep 9999` to the `verify.bats` test case that's failing. + +## Creating a New Test + +Below is a rough outline for creating a new test. For the example, assume our test case will be called `my-feature`. +1. Create a new directory named `case-my-feature` +2. If the test involves multiple datacenters/clusters, create a separate subdirectory for each cluster (eg. `case-my-feature/{dc1,dc2}`) +3. Add any necessary configuration to `*.hcl` files in the respective cluster subdirectory (or the test case directory when using a single cluster). +4. Create a `setup.sh` file in the case directory +5. Create a `capture.sh` file in the case directory +6. Create a `verify.bats` file in the case directory +7. Populate the `setup.sh`, `capture.sh` and `verify.bats` files with the appropriate code for running your test, validating its state and capturing any logs or snapshots. diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/base.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/base.hcl new file mode 100644 index 000000000..f81ab0edd --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/base.hcl @@ -0,0 +1,5 @@ +primary_datacenter = "alpha" +log_level = "trace" +peering { + enabled = true +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/config_entries.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/config_entries.hcl new file mode 100644 index 000000000..64d011702 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/config_entries.hcl @@ -0,0 +1,26 @@ +config_entries { + bootstrap = [ + { + kind = "proxy-defaults" + name = "global" + + config { + protocol = "tcp" + } + }, + { + kind = "exported-services" + name = "default" + services = [ + { + name = "s2" + consumers = [ + { + peer_name = "alpha-to-primary" + } + ] + } + ] + } + ] +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_gateway.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_gateway.hcl new file mode 100644 index 000000000..bcdcb2e8b --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_gateway.hcl @@ -0,0 +1,5 @@ +services { + name = "mesh-gateway" + kind = "mesh-gateway" + port = 4432 +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s1.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s1.hcl new file mode 100644 index 000000000..e97ec2366 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s1.hcl @@ -0,0 +1 @@ +# We don't want an s1 service in this peer diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s2.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s2.hcl new file mode 100644 index 000000000..01d4505c6 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/service_s2.hcl @@ -0,0 +1,7 @@ +services { + name = "s2" + port = 8181 + connect { + sidecar_service {} + } +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/setup.sh b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/setup.sh new file mode 100644 index 000000000..820506ea9 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/setup.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +register_services alpha + +gen_envoy_bootstrap s2 19002 alpha +gen_envoy_bootstrap mesh-gateway 19003 alpha true + +wait_for_config_entry proxy-defaults global alpha +wait_for_config_entry exported-services default alpha diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/verify.bats b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/verify.bats new file mode 100644 index 000000000..d2229b297 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/alpha/verify.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats + +load helpers + +@test "s2 proxy is running correct version" { + assert_envoy_version 19002 +} + +@test "s2 proxy admin is up on :19002" { + retry_default curl -f -s localhost:19002/stats -o /dev/null +} + +@test "gateway-alpha proxy admin is up on :19003" { + retry_default curl -f -s localhost:19003/stats -o /dev/null +} + +@test "s2 proxy listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21000 s2 alpha +} + +@test "s2 proxy should be healthy" { + assert_service_has_healthy_instances s2 1 alpha +} + +@test "gateway-alpha should be up and listening" { + retry_long nc -z consul-alpha-client:4432 +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/bind.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/bind.hcl new file mode 100644 index 000000000..f54393f03 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/bind.hcl @@ -0,0 +1,2 @@ +bind_addr = "0.0.0.0" +advertise_addr = "{{ GetInterfaceIP \"eth0\" }}" \ No newline at end of file diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/capture.sh b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/capture.sh new file mode 100644 index 000000000..ab90eb425 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/capture.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:19000 s1 primary || true +snapshot_envoy_admin localhost:19001 s2 primary || true +snapshot_envoy_admin localhost:19002 s2 alpha || true +snapshot_envoy_admin localhost:19003 mesh-gateway alpha || true diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/base.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/base.hcl new file mode 100644 index 000000000..c1e134d5a --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/base.hcl @@ -0,0 +1,3 @@ +peering { + enabled = true +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/config_entries.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/config_entries.hcl new file mode 100644 index 000000000..65163b111 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/config_entries.hcl @@ -0,0 +1,31 @@ +config_entries { + bootstrap { + kind = "proxy-defaults" + name = "global" + + config { + protocol = "tcp" + } + } + + bootstrap { + kind = "service-resolver" + name = "s2" + + failover = { + "*" = { + targets = [{peer = "primary-to-alpha"}] + } + } + } + + bootstrap { + kind = "service-resolver" + name = "virtual-s2" + + redirect = { + service = "s2" + peer = "primary-to-alpha" + } + } +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s1.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s1.hcl new file mode 100644 index 000000000..d10535324 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s1.hcl @@ -0,0 +1,20 @@ +services { + name = "s1" + port = 8080 + connect { + sidecar_service { + proxy { + upstreams = [ + { + destination_name = "s2" + local_bind_port = 5000 + }, + { + destination_name = "virtual-s2" + local_bind_port = 5001 + } + ] + } + } + } +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s2.hcl b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s2.hcl new file mode 100644 index 000000000..01d4505c6 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/service_s2.hcl @@ -0,0 +1,7 @@ +services { + name = "s2" + port = 8181 + connect { + sidecar_service {} + } +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/setup.sh b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/setup.sh new file mode 100644 index 000000000..c65cc31e4 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/setup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +register_services primary + +gen_envoy_bootstrap s1 19000 primary +gen_envoy_bootstrap s2 19001 primary + +wait_for_config_entry proxy-defaults global diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/verify.bats b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/verify.bats new file mode 100644 index 000000000..5b059bece --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/primary/verify.bats @@ -0,0 +1,113 @@ +#!/usr/bin/env bats + +load helpers + +@test "s1 proxy is running correct version" { + assert_envoy_version 19000 +} + +@test "s1 proxy admin is up on :19000" { + retry_default curl -f -s localhost:19000/stats -o /dev/null +} + +@test "s2 proxy admin is up on :19001" { + retry_default curl -f -s localhost:19001/stats -o /dev/null +} + +@test "gateway-primary proxy admin is up on :19001" { + retry_default curl localhost:19000/config_dump +} + +@test "s1 proxy listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21000 s1 +} + +@test "s2 proxies should be healthy in primary" { + assert_service_has_healthy_instances s2 1 primary +} + +@test "s2 proxies should be healthy in alpha" { + assert_service_has_healthy_instances s2 1 alpha +} + +@test "gateway-alpha should be up and listening" { + retry_long nc -z consul-alpha-client:4432 +} + +@test "peer the two clusters together" { + create_peering primary alpha +} + +@test "s2 alpha proxies should be healthy in primary" { + assert_service_has_healthy_instances s2 1 primary "" "" primary-to-alpha +} + +# Failover + +@test "s1 upstream should have healthy endpoints for s2 in both primary and failover" { + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary.internal HEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary-to-alpha.external HEALTHY 1 +} + + +@test "s1 upstream should be able to connect to s2" { + run retry_default curl -s -f -d hello localhost:5000 + [ "$status" -eq 0 ] + [ "$output" = "hello" ] +} + +@test "s1 upstream made 1 connection" { + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.primary.internal.*cx_total" 1 +} + +@test "terminate instance of s2 primary envoy which should trigger failover to s2 alpha when the tcp check fails" { + kill_envoy s2 primary +} + +@test "s2 proxies should be unhealthy in primary" { + assert_service_has_healthy_instances s2 0 primary +} + + +@test "s1 upstream should have healthy endpoints for s2 in the failover cluster peer" { + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary.internal UNHEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary-to-alpha.external HEALTHY 1 +} + +@test "reset envoy statistics for failover" { + reset_envoy_metrics 127.0.0.1:19000 +} + +@test "gateway-alpha should have healthy endpoints for s2" { + assert_upstream_has_endpoints_in_status consul-alpha-client:19003 exported~s2.default.alpha HEALTHY 1 +} + +@test "s1 upstream should be able to connect to s2 in the failover cluster peer" { + run retry_default curl -s -f -d hello localhost:5000 + [ "$status" -eq 0 ] + [ "$output" = "hello" ] +} + +@test "s1 upstream made 1 connection to s2 through the cluster peer" { + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.primary-to-alpha.external.*cx_total" 1 +} + +# Redirect + +@test "reset envoy statistics for redirect" { + reset_envoy_metrics 127.0.0.1:19000 +} + +@test "s1 upstream should have healthy endpoints for s2 (virtual-s2) in the cluster peer" { + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary-to-alpha.external HEALTHY 1 +} + +@test "s1 upstream should be able to connect to s2 via virtual-s2" { + run retry_default curl -s -f -d hello localhost:5001 + [ "$status" -eq 0 ] + [ "$output" = "hello" ] +} + +@test "s1 upstream made 1 connection to s2 via virtual-s2 through the cluster peer" { + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.primary-to-alpha.external.*cx_total" 1 +} diff --git a/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/vars.sh b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/vars.sh new file mode 100644 index 000000000..8e9108a34 --- /dev/null +++ b/test/integration/connect/envoy/case-cfg-resolver-cluster-peering-failover/vars.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy s2-alpha s2-sidecar-proxy-alpha gateway-alpha tcpdump-primary tcpdump-alpha" +export REQUIRE_PEERS=1 diff --git a/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-none/primary/verify.bats b/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-none/primary/verify.bats index 85bb95e1a..a87c31261 100644 --- a/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-none/primary/verify.bats +++ b/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-none/primary/verify.bats @@ -40,7 +40,8 @@ load helpers # Note: when failover is configured the cluster is named for the original # service not any destination related to failover. @test "s1 upstream should have healthy endpoints for s2 in both primary and failover" { - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 2 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary HEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.secondary HEALTHY 1 } @test "s1 upstream should be able to connect to s2 via upstream s2 to start" { @@ -48,7 +49,7 @@ load helpers } @test "s1 upstream made 1 connection" { - assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.primary.*cx_total" 1 + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.primary.*cx_total" 1 } ################ @@ -63,8 +64,8 @@ load helpers } @test "s1 upstream should have healthy endpoints for s2 secondary and unhealthy endpoints for s2 primary" { - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1 - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary UNHEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary UNHEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.secondary HEALTHY 1 } @test "reset envoy statistics" { @@ -76,5 +77,5 @@ load helpers } @test "s1 upstream made 1 connection again" { - assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.primary.*cx_total" 1 + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.secondary.*cx_total" 1 } diff --git a/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-remote/primary/verify.bats b/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-remote/primary/verify.bats index f80ba7a19..d46c73640 100644 --- a/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-remote/primary/verify.bats +++ b/test/integration/connect/envoy/case-cfg-resolver-dc-failover-gateways-remote/primary/verify.bats @@ -34,10 +34,6 @@ load helpers retry_long nc -z consul-secondary-client:4432 } -@test "wait until the first cluster is configured" { - assert_envoy_dynamic_cluster_exists 127.0.0.1:19000 s2.default.primary s2.default.primary -} - ################ # PHASE 1: we show that by default requests are served from the primary @@ -45,8 +41,8 @@ load helpers # service not any destination related to failover. @test "s1 upstream should have healthy endpoints for s2 in both primary and failover" { # in mesh gateway remote or local mode only the current leg of failover manifests in the load assignments - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1 - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary UNHEALTHY 0 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary HEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary UNHEALTHY 0 } @test "s1 upstream should be able to connect to s2 via upstream s2 to start" { @@ -54,7 +50,7 @@ load helpers } @test "s1 upstream made 1 connection" { - assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.primary.*cx_total" 1 + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.primary.*cx_total" 1 } ################ @@ -68,14 +64,11 @@ load helpers assert_service_has_healthy_instances s2 0 primary } -@test "wait until the failover cluster is configured" { - assert_envoy_dynamic_cluster_exists 127.0.0.1:19000 s2.default.primary s2.default.secondary -} - @test "s1 upstream should have healthy endpoints for s2 secondary" { # in mesh gateway remote or local mode only the current leg of failover manifests in the load assignments - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1 - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary UNHEALTHY 0 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary HEALTHY 0 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.secondary HEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.secondary UNHEALTHY 0 } @test "reset envoy statistics" { @@ -87,5 +80,5 @@ load helpers } @test "s1 upstream made 1 connection again" { - assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.primary.*cx_total" 1 + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.failover-target~s2.default.secondary.*cx_total" 1 } diff --git a/test/integration/connect/envoy/case-cfg-resolver-svc-failover/verify.bats b/test/integration/connect/envoy/case-cfg-resolver-svc-failover/verify.bats index 85fb0aa2c..6f684e712 100644 --- a/test/integration/connect/envoy/case-cfg-resolver-svc-failover/verify.bats +++ b/test/integration/connect/envoy/case-cfg-resolver-svc-failover/verify.bats @@ -53,7 +53,7 @@ load helpers # Note: when failover is configured the cluster is named for the original # service not any destination related to failover. @test "s1 upstream should have healthy endpoints for s2 and s3 together" { - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 2 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary HEALTHY 1 } @test "s1 upstream should be able to connect to s2 via upstream s2 to start" { @@ -65,11 +65,10 @@ load helpers } @test "s1 upstream should have healthy endpoints for s3-v1 and unhealthy endpoints for s2" { - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1 - assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary UNHEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~v1.s3.default.primary HEALTHY 1 + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 failover-target~s2.default.primary UNHEALTHY 1 } @test "s1 upstream should be able to connect to s3-v1 now" { assert_expected_fortio_name s3-v1 } - diff --git a/test/integration/connect/envoy/case-wanfed-gw/global-setup.sh b/test/integration/connect/envoy/case-wanfed-gw/global-setup.sh index fee985add..3e6120445 100755 --- a/test/integration/connect/envoy/case-wanfed-gw/global-setup.sh +++ b/test/integration/connect/envoy/case-wanfed-gw/global-setup.sh @@ -17,7 +17,7 @@ consul tls cert create -dc=secondary -server -node=sec " docker rm -f "$container" &>/dev/null || true -docker run -i --net=none --name="$container" consul-dev:latest sh -c "${scriptlet}" +docker run -i --net=none --name="$container" consul:local sh -c "${scriptlet}" # primary for f in \ diff --git a/test/integration/connect/envoy/helpers.bash b/test/integration/connect/envoy/helpers.bash index 2fd9be7e3..d7fe0ae02 100755 --- a/test/integration/connect/envoy/helpers.bash +++ b/test/integration/connect/envoy/helpers.bash @@ -562,14 +562,14 @@ function assert_intention_denied { function docker_consul { local DC=$1 shift 1 - docker run -i --rm --network container:envoy_consul-${DC}_1 consul-dev "$@" + docker run -i --rm --network container:envoy_consul-${DC}_1 consul:local "$@" } function docker_consul_for_proxy_bootstrap { local DC=$1 shift 1 - docker run -i --rm --network container:envoy_consul-${DC}_1 consul-dev "$@" + docker run -i --rm --network container:envoy_consul-${DC}_1 consul:local "$@" 2> /dev/null } function docker_wget { @@ -581,7 +581,7 @@ function docker_wget { function docker_curl { local DC=$1 shift 1 - docker run --rm --network container:envoy_consul-${DC}_1 --entrypoint curl consul-dev "$@" + docker run --rm --network container:envoy_consul-${DC}_1 --entrypoint curl consul:local "$@" } function docker_exec { @@ -806,9 +806,16 @@ function delete_config_entry { function register_services { local DC=${1:-primary} + wait_for_leader "$DC" docker_consul_exec ${DC} sh -c "consul services register /workdir/${DC}/register/service_*.hcl" } +# wait_for_leader waits until a leader is elected. +# Its first argument must be the datacenter name. +function wait_for_leader { + retry_default docker_consul_exec "$1" sh -c '[[ $(curl --fail -sS http://127.0.0.1:8500/v1/status/leader) ]]' +} + function setup_upsert_l4_intention { local SOURCE=$1 local DESTINATION=$2 diff --git a/test/integration/connect/envoy/run-tests.sh b/test/integration/connect/envoy/run-tests.sh index c8c392c34..43e0925c6 100755 --- a/test/integration/connect/envoy/run-tests.sh +++ b/test/integration/connect/envoy/run-tests.sh @@ -12,10 +12,12 @@ DEBUG=${DEBUG:-} XDS_TARGET=${XDS_TARGET:-server} # ENVOY_VERSION to run each test against -ENVOY_VERSION=${ENVOY_VERSION:-"1.23.0"} +ENVOY_VERSION=${ENVOY_VERSION:-"1.23.1"} export ENVOY_VERSION export DOCKER_BUILDKIT=1 +# Always run tests on amd64 because that's what the CI environment uses. +export DOCKER_DEFAULT_PLATFORM="linux/amd64" if [ ! -z "$DEBUG" ] ; then set -x @@ -44,17 +46,20 @@ function network_snippet { } function aws_snippet { - local snippet="" + LAMBDA_TESTS_ENABLED=${LAMBDA_TESTS_ENABLED:-false} + if [ "$LAMBDA_TESTS_ENABLED" != false ]; then + local snippet="" - # The Lambda integration cases assume that a Lambda function exists in $AWS_REGION with an ARN of $AWS_LAMBDA_ARN. - # The AWS credentials must have permission to invoke the Lambda function. - [ -n "$(set | grep '^AWS_ACCESS_KEY_ID=')" ] && snippet="${snippet} -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" - [ -n "$(set | grep '^AWS_SECRET_ACCESS_KEY=')" ] && snippet="${snippet} -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" - [ -n "$(set | grep '^AWS_SESSION_TOKEN=')" ] && snippet="${snippet} -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN" - [ -n "$(set | grep '^AWS_LAMBDA_REGION=')" ] && snippet="${snippet} -e AWS_LAMBDA_REGION=$AWS_LAMBDA_REGION" - [ -n "$(set | grep '^AWS_LAMBDA_ARN=')" ] && snippet="${snippet} -e AWS_LAMBDA_ARN=$AWS_LAMBDA_ARN" + # The Lambda integration cases assume that a Lambda function exists in $AWS_REGION with an ARN of $AWS_LAMBDA_ARN. + # The AWS credentials must have permission to invoke the Lambda function. + [ -n "$(set | grep '^AWS_ACCESS_KEY_ID=')" ] && snippet="${snippet} -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" + [ -n "$(set | grep '^AWS_SECRET_ACCESS_KEY=')" ] && snippet="${snippet} -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" + [ -n "$(set | grep '^AWS_SESSION_TOKEN=')" ] && snippet="${snippet} -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN" + [ -n "$(set | grep '^AWS_LAMBDA_REGION=')" ] && snippet="${snippet} -e AWS_LAMBDA_REGION=$AWS_LAMBDA_REGION" + [ -n "$(set | grep '^AWS_LAMBDA_ARN=')" ] && snippet="${snippet} -e AWS_LAMBDA_ARN=$AWS_LAMBDA_ARN" - echo "$snippet" + echo "$snippet" + fi } function init_workdir { @@ -175,7 +180,7 @@ function start_consul { # xDS sessions are served directly by a Consul server, and another in which it # goes through a client agent. # - # This is nessasary because servers and clients source configuration data in + # This is necessary because servers and clients source configuration data in # different ways (client agents use an RPC-backed cache and servers use their # own local data) and we want to catch regressions in both. # @@ -222,7 +227,7 @@ function start_consul { --hostname "consul-${DC}-server" \ --network-alias "consul-${DC}-server" \ -e "CONSUL_LICENSE=$license" \ - consul-dev \ + consul:local \ agent -dev -datacenter "${DC}" \ -config-dir "/workdir/${DC}/consul" \ -config-dir "/workdir/${DC}/consul-server" \ @@ -237,7 +242,7 @@ function start_consul { --network-alias "consul-${DC}-client" \ -e "CONSUL_LICENSE=$license" \ ${ports[@]} \ - consul-dev \ + consul:local \ agent -datacenter "${DC}" \ -config-dir "/workdir/${DC}/consul" \ -data-dir "/tmp/consul" \ @@ -256,7 +261,7 @@ function start_consul { --network-alias "consul-${DC}-server" \ -e "CONSUL_LICENSE=$license" \ ${ports[@]} \ - consul-dev \ + consul:local \ agent -dev -datacenter "${DC}" \ -config-dir "/workdir/${DC}/consul" \ -config-dir "/workdir/${DC}/consul-server" \ @@ -290,7 +295,7 @@ function start_partitioned_client { --hostname "consul-${PARTITION}-client" \ --network-alias "consul-${PARTITION}-client" \ -e "CONSUL_LICENSE=$license" \ - consul-dev agent \ + consul:local agent \ -datacenter "primary" \ -retry-join "consul-primary-server" \ -grpc-port 8502 \ diff --git a/testrpc/wait.go b/testrpc/wait.go index d6b72749e..39e3d6592 100644 --- a/testrpc/wait.go +++ b/testrpc/wait.go @@ -11,7 +11,9 @@ import ( type rpcFn func(string, interface{}, interface{}) error -// WaitForLeader ensures we have a leader and a node registration. +// WaitForLeader ensures we have a leader and a node registration. It +// does not wait for the Consul (node) service to be ready. Use `WaitForTestAgent` +// to make sure the Consul service is ready. // // Most uses of this would be better served in the agent/consul package by // using waitForLeaderEstablishment() instead. @@ -91,7 +93,8 @@ func flattenOptions(options []waitOption) waitOption { return flat } -// WaitForTestAgent ensures we have a node with serfHealth check registered +// WaitForTestAgent ensures we have a node with serfHealth check registered. +// You'll want to use this if you expect the Consul (node) service to be ready. func WaitForTestAgent(t *testing.T, rpc rpcFn, dc string, options ...waitOption) { t.Helper() diff --git a/tlsutil/config.go b/tlsutil/config.go index 7c9e6d2ad..2e1614165 100644 --- a/tlsutil/config.go +++ b/tlsutil/config.go @@ -102,6 +102,10 @@ type ProtocolConfig struct { // // Note: this setting only applies to the Internal RPC configuration. VerifyServerHostname bool + + // UseAutoCert is used to enable usage of auto_encrypt/auto_config generated + // certificate & key material on external gRPC listener. + UseAutoCert bool } // Config configures the Configurator. @@ -167,6 +171,10 @@ type protocolConfig struct { // combinedCAPool is a pool containing both manualCAPEMs and the certificates // received from auto-config/auto-encrypt. combinedCAPool *x509.CertPool + + // useAutoCert indicates wether we should use auto-encrypt/config data + // for TLS server/listener. NOTE: Only applies to external GRPC Server. + useAutoCert bool } // Configurator provides tls.Config and net.Dial wrappers to enable TLS for @@ -323,6 +331,7 @@ func (c *Configurator) loadProtocolConfig(base Config, pc ProtocolConfig) (*prot manualCAPEMs: pems, manualCAPool: manualPool, combinedCAPool: combinedPool, + useAutoCert: pc.UseAutoCert, }, nil } @@ -620,16 +629,15 @@ func (c *Configurator) Cert() *tls.Certificate { return cert } -// GRPCTLSConfigured returns whether there's a TLS certificate configured for -// gRPC (either manually or by auto-config/auto-encrypt). It is checked, along -// with the presence of an HTTPS port, to determine whether to enable TLS on -// incoming gRPC connections. +// GRPCServerUseTLS returns whether there's a TLS certificate configured for +// (external) gRPC (either manually or by auto-config/auto-encrypt), and use +// of TLS for gRPC has not been explicitly disabled at auto-encrypt. // // This function acquires a read lock because it reads from the config. -func (c *Configurator) GRPCTLSConfigured() bool { +func (c *Configurator) GRPCServerUseTLS() bool { c.lock.RLock() defer c.lock.RUnlock() - return c.grpc.cert != nil || c.autoTLS.cert != nil + return c.grpc.cert != nil || (c.grpc.useAutoCert && c.autoTLS.cert != nil) } // VerifyIncomingRPC returns true if we should verify incoming connnections to diff --git a/tlsutil/config_test.go b/tlsutil/config_test.go index 75fa83945..fc817aec6 100644 --- a/tlsutil/config_test.go +++ b/tlsutil/config_test.go @@ -1465,7 +1465,7 @@ func TestConfigurator_AuthorizeInternalRPCServerConn(t *testing.T) { }) } -func TestConfigurator_GRPCTLSConfigured(t *testing.T) { +func TestConfigurator_GRPCServerUseTLS(t *testing.T) { t.Run("certificate manually configured", func(t *testing.T) { c := makeConfigurator(t, Config{ GRPC: ProtocolConfig{ @@ -1473,22 +1473,47 @@ func TestConfigurator_GRPCTLSConfigured(t *testing.T) { KeyFile: "../test/hostname/Alice.key", }, }) - require.True(t, c.GRPCTLSConfigured()) + require.True(t, c.GRPCServerUseTLS()) }) - t.Run("AutoTLS", func(t *testing.T) { + t.Run("no certificate", func(t *testing.T) { + c := makeConfigurator(t, Config{}) + require.False(t, c.GRPCServerUseTLS()) + }) + + t.Run("AutoTLS (default)", func(t *testing.T) { c := makeConfigurator(t, Config{}) bobCert := loadFile(t, "../test/hostname/Bob.crt") bobKey := loadFile(t, "../test/hostname/Bob.key") require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) - - require.True(t, c.GRPCTLSConfigured()) + require.False(t, c.GRPCServerUseTLS()) }) - t.Run("no certificate", func(t *testing.T) { - c := makeConfigurator(t, Config{}) - require.False(t, c.GRPCTLSConfigured()) + t.Run("AutoTLS w/ UseAutoCert Disabled", func(t *testing.T) { + c := makeConfigurator(t, Config{ + GRPC: ProtocolConfig{ + UseAutoCert: false, + }, + }) + + bobCert := loadFile(t, "../test/hostname/Bob.crt") + bobKey := loadFile(t, "../test/hostname/Bob.key") + require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) + require.False(t, c.GRPCServerUseTLS()) + }) + + t.Run("AutoTLS w/ UseAutoCert Enabled", func(t *testing.T) { + c := makeConfigurator(t, Config{ + GRPC: ProtocolConfig{ + UseAutoCert: true, + }, + }) + + bobCert := loadFile(t, "../test/hostname/Bob.crt") + bobKey := loadFile(t, "../test/hostname/Bob.key") + require.NoError(t, c.UpdateAutoTLSCert(bobCert, bobKey)) + require.True(t, c.GRPCServerUseTLS()) }) } diff --git a/ui/.gitignore b/ui/.gitignore index 08df27ddb..6bd9a0135 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -12,6 +12,7 @@ node_modules .pnp* .sass-cache .DS_Store +.tool-versions connect.lock coverage coverage_* diff --git a/ui/packages/consul-hcp/app/components/consul/hcp/home/index.hbs b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.hbs new file mode 100644 index 000000000..1ab14457d --- /dev/null +++ b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.hbs @@ -0,0 +1,8 @@ + diff --git a/ui/packages/consul-hcp/app/components/consul/hcp/home/index.scss b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.scss new file mode 100644 index 000000000..7ae65f241 --- /dev/null +++ b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.scss @@ -0,0 +1,11 @@ +.consul-hcp-home { + position: relative; + top: -22px; +} +.consul-hcp-home a::before { + content: ''; + --icon-name: icon-arrow-left; + --icon-size: icon-300; + margin-right: 8px; +} + diff --git a/ui/packages/consul-hcp/app/components/consul/hcp/home/index.test.js b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.test.js new file mode 100644 index 000000000..84779d825 --- /dev/null +++ b/ui/packages/consul-hcp/app/components/consul/hcp/home/index.test.js @@ -0,0 +1,38 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +import ConsulHcpHome from 'consul-ui/components/consul/hcp/home'; + +module('Integration | Component | consul hcp home', function(hooks) { + setupRenderingTest(hooks); + + test('it prints the value of CONSUL_HCP_URL', async function(assert) { + // temporary registration until we are running as separate applications + this.owner.register( + 'component:consul/hcp/home', + ConsulHcpHome + ); + // + + const Helper = this.owner.resolveRegistration('helper:env'); + this.owner.register( + 'helper:env', + class extends Helper { + compute([name, def]) { + switch(name) { + case 'CONSUL_HCP_URL': + return 'http://hcp'; + } + return super.compute(...arguments); + } + } + ); + + await render(hbs``); + + assert.dom('a').hasAttribute('href', 'http://hcp'); + + }); +}); diff --git a/ui/packages/consul-hcp/vendor/consul-hcp/routes.js b/ui/packages/consul-hcp/vendor/consul-hcp/routes.js index 1b58d87ef..01fd24712 100644 --- a/ui/packages/consul-hcp/vendor/consul-hcp/routes.js +++ b/ui/packages/consul-hcp/vendor/consul-hcp/routes.js @@ -1,8 +1,6 @@ (routes => routes({ dc: { - show: { - license: null, - }, + show: null }, }))( (json, data = (typeof document !== 'undefined' ? document.currentScript.dataset : module.exports)) => { diff --git a/ui/packages/consul-hcp/vendor/consul-hcp/services.js b/ui/packages/consul-hcp/vendor/consul-hcp/services.js index 159a7a96e..27f9d4a74 100644 --- a/ui/packages/consul-hcp/vendor/consul-hcp/services.js +++ b/ui/packages/consul-hcp/vendor/consul-hcp/services.js @@ -1,5 +1,7 @@ (services => services({ - + 'component:consul/hcp/home': { + class: 'consul-ui/components/consul/hcp/home', + }, }))( (json, data = (typeof document !== 'undefined' ? document.currentScript.dataset : module.exports)) => { data[`services`] = JSON.stringify(json); diff --git a/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.hbs b/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.hbs index ec2a3d6b4..cacb0758e 100644 --- a/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.hbs +++ b/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.hbs @@ -1,169 +1,159 @@ -
- - - - +
+ + + + -{{#let - - (not (can "write nspaces")) - - @item - - (hash - help='Must be a valid DNS hostname. Must contain 1-64 characters (numbers, letters, and hyphens), and must begin with a letter. Once created, this cannot be changed.' - Name=(array - (hash - test='^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$' - error='Name must be a valid DNS hostname.' - ) - ) - ) - - (hash - Description=(array) - ) - -as |readOnly item Name Description|}} -
- - - -
-{{#if (is "new nspace" item=item)}} - -{{/if}} - -
-{{#if (can 'use acls')}} -
-

Roles

-

-{{#if (can "write nspace" item=item)}} - By adding roles to this namespaces, you will apply them to all tokens created within this namespace. -{{else}} - The following roles are applied to all tokens created within this namespace. -{{/if}} -

- -
-
-

Policies

-

-{{#if (not readOnly)}} - By adding policies to this namespace, you will apply them to all tokens created within this namespace. -{{else}} - The following policies are applied to all tokens created within this namespace. -{{/if}} -

- -
-{{/if}} -
-{{#if (and (is "new nspace" item=item) (can "create nspaces"))}} - - Save - -{{else if (can "write nspace" item=item)}} - Save -{{/if}} + - - Cancel - - -{{#if (and (not (is "new nspace" item=item)) (can "delete nspace" item=item))}} - - - - Delete - - - - - - -{{/if}} -
-
-
-{{/let}} -
-
-
+
+ {{#if (is "new nspace" item=item)}} + + {{/if}} + +
+ {{#if (can "use acls")}} +
+

Roles

+

+ {{#if (can "write nspace" item=item)}} + By adding roles to this namespaces, you will apply them to + all tokens created within this namespace. + {{else}} + The following roles are applied to all tokens created within + this namespace. + {{/if}} +

+ +
+
+

Policies

+

+ {{#if (not readOnly)}} + By adding policies to this namespace, you will apply them to + all tokens created within this namespace. + {{else}} + The following policies are applied to all tokens created + within this namespace. + {{/if}} +

+ +
+ {{/if}} +
+ {{#if (and (is "new nspace" item=item) (can "create nspaces"))}} + + Save + + {{else if (can "write nspace" item=item)}} + Save + {{/if}} + + Cancel + + + {{#if + (and + (not (is "new nspace" item=item)) + (can "delete nspace" item=item) + ) + }} + + + + Delete + + + + + + + {{/if}} + +
+ + + {{/let}} + +
+
\ No newline at end of file diff --git a/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.js b/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.js new file mode 100644 index 000000000..4c2882cde --- /dev/null +++ b/ui/packages/consul-nspaces/app/components/consul/nspace/form/index.js @@ -0,0 +1,33 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; + +export default class NspaceForm extends Component { + @action onSubmit(item) { + const onSubmit = this.args.onsubmit; + if (onSubmit) return onSubmit(item); + } + + @action onDelete(item) { + const { onsubmit, ondelete } = this.args; + + if (ondelete) { + return ondelete(item); + } else { + if (onsubmit) { + return onsubmit(item); + } + } + } + + @action onCancel(item) { + const { oncancel, onsubmit } = this.args; + + if (oncancel) { + return oncancel(item); + } else { + if (onsubmit) { + return onsubmit(item); + } + } + } +} diff --git a/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js b/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js new file mode 100644 index 000000000..531bffc7b --- /dev/null +++ b/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js @@ -0,0 +1,14 @@ +export const selectors = { + $: '.consul-peer-list', + collection: { + $: '[data-test-list-row]', + peer: { + $: 'li' + }, + } +}; +export default (collection, isPresent) => () => { + return collection(`${selectors.$} ${selectors.collection.$}`, { + peer: isPresent(selectors.collection.peer.$), + }); +}; diff --git a/ui/packages/consul-ui/.docfy-config.js b/ui/packages/consul-ui/.docfy-config.js index 0e1f6838a..bd85d31ff 100644 --- a/ui/packages/consul-ui/.docfy-config.js +++ b/ui/packages/consul-ui/.docfy-config.js @@ -13,16 +13,16 @@ const exists = fs.existsSync; const chalk = require('chalk'); // comes with ember // allow extra docfy config -let user = {sources: [], labels: {}}; +let user = { sources: [], labels: {} }; const $CONSUL_DOCFY_CONFIG = process.env.CONSUL_DOCFY_CONFIG || ''; -if($CONSUL_DOCFY_CONFIG.length > 0) { +if ($CONSUL_DOCFY_CONFIG.length > 0) { try { - if(exists($CONSUL_DOCFY_CONFIG)) { - user = JSON.parse(read($CONSUL_DOCFY_CONFIG)); - } else { - throw new Error(`Unable to locate ${$CONSUL_DOCFY_CONFIG}`); - } - } catch(e) { + if (exists($CONSUL_DOCFY_CONFIG)) { + user = JSON.parse(read($CONSUL_DOCFY_CONFIG)); + } else { + throw new Error(`Unable to locate ${$CONSUL_DOCFY_CONFIG}`); + } + } catch (e) { console.error(chalk.yellow(`Docfy: ${e.message}`)); } } @@ -33,24 +33,20 @@ refractor.register(handlebars); refractor.alias({ handlebars: ['hbs'], - shell: ['sh'] + shell: ['sh'], }); - - module.exports = { remarkHbsOptions: { - escapeCurliesCode: false + escapeCurliesCode: false, }, remarkPlugins: [ autolinkHeadings, { - behavior: 'wrap' - } - ], - rehypePlugins: [ - prism + behavior: 'wrap', + }, ], + rehypePlugins: [prism], sources: [ { root: path.resolve(__dirname, 'docs'), @@ -129,10 +125,10 @@ module.exports = { pattern: '**/README.mdx', urlSchema: 'auto', urlPrefix: 'docs/consul-nspaces', - } + }, ].concat(user.sources), labels: { - "consul": "Consul Components", - ...user.labels - } + consul: 'Consul Components', + ...user.labels, + }, }; diff --git a/ui/packages/consul-ui/.eslintignore b/ui/packages/consul-ui/.eslintignore index 349abf174..ae9599337 100644 --- a/ui/packages/consul-ui/.eslintignore +++ b/ui/packages/consul-ui/.eslintignore @@ -15,6 +15,7 @@ app/utils/dom/event-target/event-target-shim/event.js # misc /coverage/ !.* +.eslintcache # ember-try /.node_modules.ember-try/ diff --git a/ui/packages/consul-ui/.eslintrc.js b/ui/packages/consul-ui/.eslintrc.js index 994b78a24..a8103eb0d 100644 --- a/ui/packages/consul-ui/.eslintrc.js +++ b/ui/packages/consul-ui/.eslintrc.js @@ -9,24 +9,44 @@ module.exports = { }, }, plugins: ['ember'], - extends: ['eslint:recommended', 'plugin:ember/recommended'], + extends: ['eslint:recommended', 'plugin:ember/recommended', 'plugin:prettier/recommended'], env: { browser: true, }, rules: { - 'no-console': ['error', {allow: ['error', 'info']}], + 'no-console': ['error', { allow: ['error', 'info'] }], 'no-unused-vars': ['error', { args: 'none' }], 'ember/no-new-mixins': ['warn'], 'ember/no-jquery': 'warn', 'ember/no-global-jquery': 'warn', + + // for 3.24 update + 'ember/classic-decorator-no-classic-methods': ['warn'], + 'ember/classic-decorator-hooks': ['warn'], + 'ember/no-classic-classes': ['warn'], + 'ember/no-mixins': ['warn'], + 'ember/no-computed-properties-in-native-classes': ['warn'], + 'ember/no-private-routing-service': ['warn'], + 'ember/no-test-import-export': ['warn'], + 'ember/no-actions-hash': ['warn'], + 'ember/no-classic-components': ['warn'], + 'ember/no-component-lifecycle-hooks': ['warn'], + 'ember/require-tagless-components': ['warn'], + 'ember/no-legacy-test-waiters': ['warn'], + 'ember/no-empty-glimmer-component-classes': ['warn'], + 'ember/no-get': ['off'], // be careful with autofix, might change behavior + 'ember/require-computed-property-dependencies': ['off'], // be careful with autofix + 'ember/use-ember-data-rfc-395-imports': ['off'], // be carful with autofix + 'ember/require-super-in-lifecycle-hooks': ['off'], // be careful with autofix + 'ember/require-computed-macros': ['off'], // be careful with autofix }, overrides: [ // node files { files: [ '.eslintrc.js', - '.dev.eslintrc.js', '.docfy-config.js', + '.prettierrc.js', '.template-lintrc.js', 'ember-cli-build.js', 'testem.js', diff --git a/ui/packages/consul-ui/.gitignore b/ui/packages/consul-ui/.gitignore new file mode 100644 index 000000000..7e0f7ddce --- /dev/null +++ b/ui/packages/consul-ui/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/.env* +/.pnp* +/.sass-cache +/.eslintcache +/connect.lock +/coverage/ +/libpeerconnection.log +/npm-debug.log* +/testem.log +/yarn-error.log + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/ui/packages/consul-ui/.prettierignore b/ui/packages/consul-ui/.prettierignore new file mode 100644 index 000000000..922165552 --- /dev/null +++ b/ui/packages/consul-ui/.prettierignore @@ -0,0 +1,21 @@ +# unconventional js +/blueprints/*/files/ +/vendor/ + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/coverage/ +!.* +.eslintcache + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/ui/packages/consul-ui/.prettierrc.js b/ui/packages/consul-ui/.prettierrc.js new file mode 100644 index 000000000..534e6d35a --- /dev/null +++ b/ui/packages/consul-ui/.prettierrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + singleQuote: true, +}; diff --git a/ui/packages/consul-ui/app/abilities/base.js b/ui/packages/consul-ui/app/abilities/base.js index 8294273a2..323d4cf5c 100644 --- a/ui/packages/consul-ui/app/abilities/base.js +++ b/ui/packages/consul-ui/app/abilities/base.js @@ -54,7 +54,7 @@ export default class BaseAbility extends Ability { get canRead() { if (typeof this.item !== 'undefined') { - const perm = (get(this, 'item.Resources') || []).find(item => item.Access === ACCESS_READ); + const perm = (get(this, 'item.Resources') || []).find((item) => item.Access === ACCESS_READ); if (perm) { return perm.Allow; } @@ -64,7 +64,7 @@ export default class BaseAbility extends Ability { get canList() { if (typeof this.item !== 'undefined') { - const perm = (get(this, 'item.Resources') || []).find(item => item.Access === ACCESS_LIST); + const perm = (get(this, 'item.Resources') || []).find((item) => item.Access === ACCESS_LIST); if (perm) { return perm.Allow; } @@ -74,7 +74,7 @@ export default class BaseAbility extends Ability { get canWrite() { if (typeof this.item !== 'undefined') { - const perm = (get(this, 'item.Resources') || []).find(item => item.Access === ACCESS_WRITE); + const perm = (get(this, 'item.Resources') || []).find((item) => item.Access === ACCESS_WRITE); if (perm) { return perm.Allow; } diff --git a/ui/packages/consul-ui/app/abilities/intention.js b/ui/packages/consul-ui/app/abilities/intention.js index a9435f975..06f6b4004 100644 --- a/ui/packages/consul-ui/app/abilities/intention.js +++ b/ui/packages/consul-ui/app/abilities/intention.js @@ -5,13 +5,12 @@ export default class IntentionAbility extends BaseAbility { get canWrite() { // Peered intentions aren't writable - if(typeof this.item !== 'undefined' && typeof this.item.SourcePeer !== 'undefined') { + if (typeof this.item !== 'undefined' && typeof this.item.SourcePeer !== 'undefined') { return false; } - return super.canWrite && - (typeof this.item === 'undefined' || !this.canViewCRD); + return super.canWrite && (typeof this.item === 'undefined' || !this.canViewCRD); } get canViewCRD() { - return (typeof this.item !== 'undefined' && this.item.IsManagedByCRD); + return typeof this.item !== 'undefined' && this.item.IsManagedByCRD; } } diff --git a/ui/packages/consul-ui/app/abilities/overview.js b/ui/packages/consul-ui/app/abilities/overview.js index eba021772..8c4734ae7 100644 --- a/ui/packages/consul-ui/app/abilities/overview.js +++ b/ui/packages/consul-ui/app/abilities/overview.js @@ -1,9 +1,12 @@ import BaseAbility from './base'; +import { inject as service } from '@ember/service'; export default class OverviewAbility extends BaseAbility { + @service('env') env; + resource = 'operator'; segmented = false; get canAccess() { - return this.canRead; + return !this.env.var('CONSUL_HCP_ENABLED') && this.canRead; } } diff --git a/ui/packages/consul-ui/app/abilities/peer.js b/ui/packages/consul-ui/app/abilities/peer.js index 28b913a3e..440f08416 100644 --- a/ui/packages/consul-ui/app/abilities/peer.js +++ b/ui/packages/consul-ui/app/abilities/peer.js @@ -11,10 +11,7 @@ export default class PeerAbility extends BaseAbility { return this.canDelete; } get canDelete() { - return ![ - 'DELETING', - 'TERMINATED' - ].includes(this.item.State) && super.canDelete; + return !['DELETING', 'TERMINATED'].includes(this.item.State) && super.canDelete; } get canUse() { diff --git a/ui/packages/consul-ui/app/abilities/service-instance.js b/ui/packages/consul-ui/app/abilities/service-instance.js index 07a11a41a..bdce3192e 100644 --- a/ui/packages/consul-ui/app/abilities/service-instance.js +++ b/ui/packages/consul-ui/app/abilities/service-instance.js @@ -6,9 +6,11 @@ export default class ServiceInstanceAbility extends BaseAbility { // When we ask for service-instances its almost like a request for a single service // When we do that we also want to know if we can read/write intentions for services // so here we add intentions read/write for the specific segment/service prefix - return super.generateForSegment(...arguments).concat([ - this.permissions.generate('intention', ACCESS_READ, segment), - this.permissions.generate('intention', ACCESS_WRITE, segment), - ]); + return super + .generateForSegment(...arguments) + .concat([ + this.permissions.generate('intention', ACCESS_READ, segment), + this.permissions.generate('intention', ACCESS_WRITE, segment), + ]); } } diff --git a/ui/packages/consul-ui/app/abilities/zervice.js b/ui/packages/consul-ui/app/abilities/zervice.js index 60a7a16f1..c2d01b7f4 100644 --- a/ui/packages/consul-ui/app/abilities/zervice.js +++ b/ui/packages/consul-ui/app/abilities/zervice.js @@ -12,7 +12,7 @@ export default class ZerviceAbility extends BaseAbility { return false; } const found = this.item.Resources.find( - item => item.Resource === 'intention' && item.Access === 'read' && item.Allow === true + (item) => item.Resource === 'intention' && item.Access === 'read' && item.Allow === true ); return typeof found !== 'undefined'; } @@ -22,7 +22,7 @@ export default class ZerviceAbility extends BaseAbility { return false; } const found = this.item.Resources.find( - item => item.Resource === 'intention' && item.Access === 'write' && item.Allow === true + (item) => item.Resource === 'intention' && item.Access === 'write' && item.Allow === true ); return typeof found !== 'undefined'; } diff --git a/ui/packages/consul-ui/app/adapters/http.js b/ui/packages/consul-ui/app/adapters/http.js index 3794e27a5..0416f4aca 100644 --- a/ui/packages/consul-ui/app/adapters/http.js +++ b/ui/packages/consul-ui/app/adapters/http.js @@ -15,24 +15,24 @@ import AdapterError, { // `serialized, unserialized` and the other just `query` // they could actually be one function now, but would be nice to think about // the naming of things (serialized vs query etc) -const read = function(adapter, modelName, type, query = {}) { +const read = function (adapter, modelName, type, query = {}) { return adapter.rpc( - function(adapter, ...rest) { + function (adapter, ...rest) { return adapter[`requestFor${type}`](...rest); }, - function(serializer, ...rest) { + function (serializer, ...rest) { return serializer[`respondFor${type}`](...rest); }, query, modelName ); }; -const write = function(adapter, modelName, type, snapshot) { +const write = function (adapter, modelName, type, snapshot) { return adapter.rpc( - function(adapter, ...rest) { + function (adapter, ...rest) { return adapter[`requestFor${type}`](...rest); }, - function(serializer, ...rest) { + function (serializer, ...rest) { return serializer[`respondFor${type}`](...rest); }, snapshot, @@ -66,13 +66,13 @@ export default class HttpAdapter extends Adapter { } return client - .request(function(request) { + .request(function (request) { return req(adapter, request, serialized, unserialized, modelClass); }) - .catch(function(e) { + .catch(function (e) { return adapter.error(e); }) - .then(function(respond) { + .then(function (respond) { // TODO: When HTTPAdapter:responder changes, this will also need to change return resp(serializer, respond, serialized, unserialized, modelClass); }); diff --git a/ui/packages/consul-ui/app/adapters/node.js b/ui/packages/consul-ui/app/adapters/node.js index de3f3c295..6a65b6df3 100644 --- a/ui/packages/consul-ui/app/adapters/node.js +++ b/ui/packages/consul-ui/app/adapters/node.js @@ -58,10 +58,10 @@ export default class NodeAdapter extends Adapter { queryLeader(store, type, id, snapshot) { return this.rpc( - function(adapter, request, serialized, unserialized) { + function (adapter, request, serialized, unserialized) { return adapter.requestForQueryLeader(request, serialized, unserialized); }, - function(serializer, respond, serialized, unserialized) { + function (serializer, respond, serialized, unserialized) { return serializer.respondForQueryLeader(respond, serialized, unserialized); }, snapshot, diff --git a/ui/packages/consul-ui/app/adapters/nspace.js b/ui/packages/consul-ui/app/adapters/nspace.js index cb340c508..490cb54ed 100644 --- a/ui/packages/consul-ui/app/adapters/nspace.js +++ b/ui/packages/consul-ui/app/adapters/nspace.js @@ -40,8 +40,8 @@ export default class NspaceAdapter extends Adapter { Name: serialized.Name, Description: serialized.Description, ACLs: { - PolicyDefaults: serialized.ACLs.PolicyDefaults.map(item => ({ ID: item.ID })), - RoleDefaults: serialized.ACLs.RoleDefaults.map(item => ({ ID: item.ID })), + PolicyDefaults: serialized.ACLs.PolicyDefaults.map((item) => ({ ID: item.ID })), + RoleDefaults: serialized.ACLs.RoleDefaults.map((item) => ({ ID: item.ID })), }, }} `; @@ -57,8 +57,8 @@ export default class NspaceAdapter extends Adapter { ${{ Description: serialized.Description, ACLs: { - PolicyDefaults: serialized.ACLs.PolicyDefaults.map(item => ({ ID: item.ID })), - RoleDefaults: serialized.ACLs.RoleDefaults.map(item => ({ ID: item.ID })), + PolicyDefaults: serialized.ACLs.PolicyDefaults.map((item) => ({ ID: item.ID })), + RoleDefaults: serialized.ACLs.RoleDefaults.map((item) => ({ ID: item.ID })), }, }} `; diff --git a/ui/packages/consul-ui/app/adapters/oidc-provider.js b/ui/packages/consul-ui/app/adapters/oidc-provider.js index 155331f5a..0bd094cad 100644 --- a/ui/packages/consul-ui/app/adapters/oidc-provider.js +++ b/ui/packages/consul-ui/app/adapters/oidc-provider.js @@ -67,10 +67,10 @@ export default class OidcProviderAdapter extends Adapter { authorize(store, type, id, snapshot) { return this.rpc( - function(adapter, request, serialized, unserialized) { + function (adapter, request, serialized, unserialized) { return adapter.requestForAuthorize(request, serialized, unserialized); }, - function(serializer, respond, serialized, unserialized) { + function (serializer, respond, serialized, unserialized) { return serializer.respondForAuthorize(respond, serialized, unserialized); }, snapshot, @@ -80,10 +80,10 @@ export default class OidcProviderAdapter extends Adapter { logout(store, type, id, snapshot) { return this.rpc( - function(adapter, request, serialized, unserialized) { + function (adapter, request, serialized, unserialized) { return adapter.requestForLogout(request, serialized, unserialized); }, - function(serializer, respond, serialized, unserialized) { + function (serializer, respond, serialized, unserialized) { // its ok to return nothing here for the moment at least return {}; }, diff --git a/ui/packages/consul-ui/app/adapters/permission.js b/ui/packages/consul-ui/app/adapters/permission.js index 33493b375..1dd086c9a 100644 --- a/ui/packages/consul-ui/app/adapters/permission.js +++ b/ui/packages/consul-ui/app/adapters/permission.js @@ -16,10 +16,10 @@ export default class PermissionAdapter extends Adapter { // ^ same goes for Partitions if (this.env.var('CONSUL_NSPACES_ENABLED')) { - resources = resources.map(item => ({ ...item, Namespace: ns })); + resources = resources.map((item) => ({ ...item, Namespace: ns })); } if (this.env.var('CONSUL_PARTITIONS_ENABLED')) { - resources = resources.map(item => ({ ...item, Partition: partition })); + resources = resources.map((item) => ({ ...item, Partition: partition })); } return request` POST /v1/internal/acl/authorize?${{ dc }} @@ -40,24 +40,24 @@ export default class PermissionAdapter extends Adapter { // Same goes ^ for partitions const nspacesEnabled = this.env.var('CONSUL_NSPACES_ENABLED'); const partitionsEnabled = this.env.var('CONSUL_PARTITIONS_ENABLED'); - if(nspacesEnabled || partitionsEnabled) { + if (nspacesEnabled || partitionsEnabled) { const token = await this.settings.findBySlug('token'); - if(nspacesEnabled) { - if(typeof serialized.ns === 'undefined' || serialized.ns.length === 0) { + if (nspacesEnabled) { + if (typeof serialized.ns === 'undefined' || serialized.ns.length === 0) { serialized.ns = token.Namespace; } } - if(partitionsEnabled) { - if(typeof serialized.partition === 'undefined' || serialized.partition.length === 0) { + if (partitionsEnabled) { + if (typeof serialized.partition === 'undefined' || serialized.partition.length === 0) { serialized.partition = token.Partition; } } } return adapter.requestForAuthorize(request, serialized); }, - function(serializer, respond, serialized, unserialized) { + function (serializer, respond, serialized, unserialized) { // Completely skip the serializer here - return respond(function(headers, body) { + return respond(function (headers, body) { return body; }); }, diff --git a/ui/packages/consul-ui/app/adapters/token.js b/ui/packages/consul-ui/app/adapters/token.js index 286568877..5502dbd89 100644 --- a/ui/packages/consul-ui/app/adapters/token.js +++ b/ui/packages/consul-ui/app/adapters/token.js @@ -134,10 +134,10 @@ export default class TokenAdapter extends Adapter { // services/store.js self(store, type, id, unserialized) { return this.rpc( - function(adapter, request, serialized, data) { + function (adapter, request, serialized, data) { return adapter.requestForSelf(request, serialized, data); }, - function(serializer, respond, serialized, data) { + function (serializer, respond, serialized, data) { return serializer.respondForSelf(respond, serialized, data); }, unserialized, @@ -147,7 +147,7 @@ export default class TokenAdapter extends Adapter { clone(store, type, id, snapshot) { return this.rpc( - function(adapter, request, serialized, data) { + function (adapter, request, serialized, data) { return adapter.requestForCloneRecord(request, serialized, data); }, (serializer, respond, serialized, data) => { diff --git a/ui/packages/consul-ui/app/components/action/README.mdx b/ui/packages/consul-ui/app/components/action/README.mdx index 72050bd63..0cd77ac39 100644 --- a/ui/packages/consul-ui/app/components/action/README.mdx +++ b/ui/packages/consul-ui/app/components/action/README.mdx @@ -20,7 +20,7 @@ You don't need to worry/think about which native tag to use the component will use the semantically correct tag depending on whether you give it a href argument or not. -If you give the Action a `href` then that will navigate you to that hyerlink +If you give the Action a `href` then that will navigate you to that hyperlink reference (semantically this is the same as the HTML Anchor tag, and semantically renders as one). diff --git a/ui/packages/consul-ui/app/components/aria-menu/index.js b/ui/packages/consul-ui/app/components/aria-menu/index.js index 4162a1995..de1593263 100644 --- a/ui/packages/consul-ui/app/components/aria-menu/index.js +++ b/ui/packages/consul-ui/app/components/aria-menu/index.js @@ -14,20 +14,20 @@ const ARROW_DOWN = 40; const keys = { vertical: { - [ARROW_DOWN]: function($items, i = -1) { + [ARROW_DOWN]: function ($items, i = -1) { return (i + 1) % $items.length; }, - [ARROW_UP]: function($items, i = 0) { + [ARROW_UP]: function ($items, i = 0) { if (i === 0) { return $items.length - 1; } else { return i - 1; } }, - [HOME]: function($items, i) { + [HOME]: function ($items, i) { return 0; }, - [END]: function($items, i) { + [END]: function ($items, i) { return $items.length - 1; }, }, @@ -43,29 +43,29 @@ export default Component.extend({ expanded: false, orientation: 'vertical', keyboardAccess: true, - init: function() { + init: function () { this._super(...arguments); set(this, 'guid', this.dom.guid(this)); this._listeners = this.dom.listeners(); this._routelisteners = this.dom.listeners(); }, - didInsertElement: function() { + didInsertElement: function () { // TODO: How do you detect whether the children have changed? // For now we know that these elements exist and never change this.$menu = this.dom.element(`#${COMPONENT_ID}menu-${this.guid}`); const labelledBy = this.$menu.getAttribute('aria-labelledby'); this.$trigger = this.dom.element(`#${labelledBy}`); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this._listeners.remove(); this._routelisteners.remove(); }, actions: { - keypressClick: function(e) { + keypressClick: function (e) { e.target.dispatchEvent(new MouseEvent('click')); }, - keypress: function(e) { + keypress: function (e) { // If the event is from the trigger and its not an opening/closing // key then don't do anything if (![ENTER, SPACE, ARROW_UP, ARROW_DOWN].includes(e.keyCode)) { @@ -99,7 +99,7 @@ export default Component.extend({ const $focused = this.dom.element(`${MENU_ITEMS}:focus`, this.$menu); let i; if ($focused) { - i = $items.findIndex(function($item) { + i = $items.findIndex(function ($item) { return $item === $focused; }); } @@ -108,7 +108,7 @@ export default Component.extend({ }, // TODO: The argument here needs to change to an event // see toggle-button.change - change: function(e) { + change: function (e) { const open = e.target.checked; if (open) { this.actions.open.apply(this, [e]); @@ -116,7 +116,7 @@ export default Component.extend({ this.actions.close.apply(this, [e]); } }, - close: function(e) { + close: function (e) { this._listeners.remove(); set(this, 'expanded', false); // TODO: Find a better way to do this without using next @@ -127,7 +127,7 @@ export default Component.extend({ this.$trigger.removeAttribute('tabindex'); }); }, - open: function(e) { + open: function (e) { set(this, 'expanded', true); const $items = [...this.dom.elements(MENU_ITEMS, this.$menu)]; if ($items.length === 0) { @@ -138,7 +138,7 @@ export default Component.extend({ // Take the trigger out of the tabbing whilst the menu is open this.$trigger.setAttribute('tabindex', '-1'); this._listeners.add(this.dom.document(), { - keydown: e => { + keydown: (e) => { // Keep focus on the trigger when you close via ESC if (e.keyCode === ESC) { this.$trigger.focus(); diff --git a/ui/packages/consul-ui/app/components/auth-form/pageobject.js b/ui/packages/consul-ui/app/components/auth-form/pageobject.js index 040474961..b21bfb6ee 100644 --- a/ui/packages/consul-ui/app/components/auth-form/pageobject.js +++ b/ui/packages/consul-ui/app/components/auth-form/pageobject.js @@ -1,6 +1,7 @@ -export default (submitable, clickable, attribute) => (scope = '.auth-form') => { - return { - scope: scope, - ...submitable(), +export default (submitable, clickable, attribute) => + (scope = '.auth-form') => { + return { + scope: scope, + ...submitable(), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/child-selector/index.js b/ui/packages/consul-ui/app/components/child-selector/index.js index 291364fc3..7f8d895c8 100644 --- a/ui/packages/consul-ui/app/components/child-selector/index.js +++ b/ui/packages/consul-ui/app/components/child-selector/index.js @@ -8,10 +8,10 @@ import { task } from 'ember-concurrency'; import Slotted from 'block-slots'; export default Component.extend(Slotted, { - onchange: function() {}, + onchange: function () {}, tagName: '', - error: function() {}, + error: function () {}, type: '', dom: service('dom'), @@ -21,17 +21,17 @@ export default Component.extend(Slotted, { selectedOptions: alias('items'), - init: function() { + init: function () { this._super(...arguments); this._listeners = this.dom.listeners(); - this.form = this.formContainer.form(this.type); + set(this, 'form', this.formContainer.form(this.type)); this.form.clear({ Datacenter: this.dc, Namespace: this.nspace }); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this._listeners.remove(); }, - options: computed('selectedOptions.[]', 'allOptions.[]', function() { + options: computed('selectedOptions.[]', 'allOptions.[]', function () { // It's not massively important here that we are defaulting `items` and // losing reference as its just to figure out the diff let options = this.allOptions || []; @@ -40,11 +40,11 @@ export default Component.extend(Slotted, { // filter out any items from the available options that have already been // selected/added // TODO: find a proper ember-data diff - options = options.filter(item => !items.findBy('ID', get(item, 'ID'))); + options = options.filter((item) => !items.findBy('ID', get(item, 'ID'))); } return options; }), - save: task(function*(item, items, success = function() {}) { + save: task(function* (item, items, success = function () {}) { const repo = this.repo; try { item = yield repo.persist(item); @@ -64,14 +64,14 @@ export default Component.extend(Slotted, { } }), actions: { - reset: function() { + reset: function () { this.form.clear({ Datacenter: this.dc, Namespace: this.nspace, Partition: this.partition }); }, - remove: function(item, items) { + remove: function (item, items) { const prop = this.repo.getSlugKey(); const value = get(item, prop); - const pos = items.findIndex(function(item) { + const pos = items.findIndex(function (item) { return get(item, prop) === value; }); if (pos !== -1) { @@ -79,7 +79,7 @@ export default Component.extend(Slotted, { } this.onchange({ target: this }); }, - change: function(e, value, item) { + change: function (e, value, item) { const event = this.dom.normalizeEvent(...arguments); const items = value; switch (event.target.name) { diff --git a/ui/packages/consul-ui/app/components/code-editor/index.js b/ui/packages/consul-ui/app/components/code-editor/index.js index 2d54bbb61..b741f70eb 100644 --- a/ui/packages/consul-ui/app/components/code-editor/index.js +++ b/ui/packages/consul-ui/app/components/code-editor/index.js @@ -17,20 +17,20 @@ export default Component.extend({ readonly: false, syntax: '', // TODO: Change this to oninput to be consistent? We'll have to do it throughout the templates - onkeyup: function() {}, - oninput: function() {}, - init: function() { + onkeyup: function () {}, + oninput: function () {}, + init: function () { this._super(...arguments); set(this, 'modes', this.helper.modes()); }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); const editor = this.editor; if (editor) { editor.setOption('readOnly', this.readonly); } }, - setMode: function(mode) { + setMode: function (mode) { let options = { ...DEFAULTS, mode: mode.mime, @@ -48,13 +48,13 @@ export default Component.extend({ this.helper.lint(editor, mode.mode); set(this, 'mode', mode); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); if (this.observer) { this.observer.disconnect(); } }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); const $code = this.dom.element('textarea ~ pre code', this.element); if ($code.firstChild) { @@ -70,11 +70,11 @@ export default Component.extend({ set(this, 'value', $code.firstChild.wholeText); } set(this, 'editor', this.helper.getEditor(this.element)); - this.settings.findBySlug('code-editor').then(mode => { + this.settings.findBySlug('code-editor').then((mode) => { const modes = this.modes; const syntax = this.syntax; if (syntax) { - mode = modes.find(function(item) { + mode = modes.find(function (item) { return item.name.toLowerCase() == syntax.toLowerCase(); }); } @@ -82,11 +82,11 @@ export default Component.extend({ this.setMode(mode); }); }, - didAppear: function() { + didAppear: function () { this.editor.refresh(); }, actions: { - change: function(value) { + change: function (value) { this.settings.persist({ 'code-editor': value, }); diff --git a/ui/packages/consul-ui/app/components/composite-row/index.scss b/ui/packages/consul-ui/app/components/composite-row/index.scss index bd66491a7..1dce70e4b 100644 --- a/ui/packages/consul-ui/app/components/composite-row/index.scss +++ b/ui/packages/consul-ui/app/components/composite-row/index.scss @@ -95,7 +95,7 @@ } %composite-row-detail .policy::before { - @extend %with-file-fill-mask, %as-pseudo; + @extend %with-file-text-mask, %as-pseudo; margin-right: 3px; } %composite-row-detail .role::before { diff --git a/ui/packages/consul-ui/app/components/confirmation-dialog/index.js b/ui/packages/consul-ui/app/components/confirmation-dialog/index.js index d44c7471d..951e2e9f5 100644 --- a/ui/packages/consul-ui/app/components/confirmation-dialog/index.js +++ b/ui/packages/consul-ui/app/components/confirmation-dialog/index.js @@ -10,14 +10,14 @@ export default Component.extend(Slotted, { confirming: false, permanent: false, actions: { - cancel: function() { + cancel: function () { set(this, 'confirming', false); }, - execute: function() { + execute: function () { set(this, 'confirming', false); this.sendAction(...['actionName', ...this['arguments']]); }, - confirm: function() { + confirm: function () { const [action, ...args] = arguments; set(this, 'actionName', action); set(this, 'arguments', args); diff --git a/ui/packages/consul-ui/app/components/consul/datacenter/selector/index.hbs b/ui/packages/consul-ui/app/components/consul/datacenter/selector/index.hbs index be1c33da4..72b44e3a9 100644 --- a/ui/packages/consul-ui/app/components/consul/datacenter/selector/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/datacenter/selector/index.hbs @@ -58,6 +58,9 @@ {{else}}
{{@dcs.firstObject.Name}} + {{#if (env 'CONSUL_HCP_MANAGED_RUNTIME')}} + Self-managed + {{/if}}
{{/if}} diff --git a/ui/packages/consul-ui/app/components/consul/discovery-chain/index.js b/ui/packages/consul-ui/app/components/consul/discovery-chain/index.js index 8b16e5b5c..6d8ffa7f6 100644 --- a/ui/packages/consul-ui/app/components/consul/discovery-chain/index.js +++ b/ui/packages/consul-ui/app/components/consul/discovery-chain/index.js @@ -11,13 +11,13 @@ export default Component.extend({ classNames: ['discovery-chain'], classNameBindings: ['active'], selectedId: '', - init: function() { + init: function () { this._super(...arguments); this._listeners = this.dom.listeners(); }, - didInsertElement: function() { + didInsertElement: function () { this._listeners.add(this.dom.document(), { - click: e => { + click: (e) => { // all route/splitter/resolver components currently // have classes that end in '-card' if (!this.dom.closest('[class$="-card"]', e.target)) { @@ -27,21 +27,21 @@ export default Component.extend({ }, }); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this._listeners.remove(); this.ticker.destroy(this); }, - splitters: computed('chain.Nodes', function() { + splitters: computed('chain.Nodes', function () { return getSplitters(get(this, 'chain.Nodes')); }), - routes: computed('chain.Nodes', function() { + routes: computed('chain.Nodes', function () { const routes = getRoutes(get(this, 'chain.Nodes'), this.dom.guid); // if we have no routes with a PathPrefix of '/' or one with no definition at all // then add our own 'default catch all' if ( - !routes.find(item => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') && - !routes.find(item => typeof item.Definition === 'undefined') + !routes.find((item) => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') && + !routes.find((item) => typeof item.Definition === 'undefined') ) { let nextNode; const resolverID = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Partition}.${this.chain.Datacenter}`; @@ -72,7 +72,7 @@ export default Component.extend({ } return routes; }), - nodes: computed('routes', 'splitters', 'resolvers', function() { + nodes: computed('routes', 'splitters', 'resolvers', function () { let nodes = this.resolvers.reduce((prev, item) => { prev[`resolver:${item.ID}`] = item; item.Children.reduce((prev, item) => { @@ -94,7 +94,7 @@ export default Component.extend({ value.NextItem = nodes[value.NextNode]; } if (typeof value.Splits !== 'undefined') { - value.Splits.forEach(item => { + value.Splits.forEach((item) => { if (typeof item.NextNode !== 'undefined') { item.NextItem = nodes[item.NextNode]; } @@ -103,7 +103,7 @@ export default Component.extend({ }); return ''; }), - resolvers: computed('chain.{Nodes,Targets}', function() { + resolvers: computed('chain.{Nodes,Targets}', function () { return getResolvers( this.chain.Datacenter, this.chain.Partition, @@ -112,10 +112,10 @@ export default Component.extend({ get(this, 'chain.Nodes') ); }), - graph: computed('splitters', 'routes.[]', function() { + graph: computed('splitters', 'routes.[]', function () { const graph = this.dataStructs.graph(); - this.splitters.forEach(item => { - item.Splits.forEach(splitter => { + this.splitters.forEach((item) => { + item.Splits.forEach((splitter) => { graph.addLink(item.ID, splitter.NextNode); }); }); @@ -124,7 +124,7 @@ export default Component.extend({ }); return graph; }), - selected: computed('selectedId', 'graph', function() { + selected: computed('selectedId', 'graph', function () { if (this.selectedId === '' || !this.dom.element(`#${this.selectedId}`)) { return {}; } @@ -144,12 +144,12 @@ export default Component.extend({ }); }); return { - nodes: nodes.map(item => `#${CSS.escape(item)}`), - edges: edges.map(item => `#${CSS.escape(item)}`), + nodes: nodes.map((item) => `#${CSS.escape(item)}`), + edges: edges.map((item) => `#${CSS.escape(item)}`), }; }), actions: { - click: function(e) { + click: function (e) { const id = e.currentTarget.getAttribute('id'); if (id === this.selectedId) { set(this, 'active', false); diff --git a/ui/packages/consul-ui/app/components/consul/discovery-chain/route-card/index.js b/ui/packages/consul-ui/app/components/consul/discovery-chain/route-card/index.js index c9fb037d8..6d47438ef 100644 --- a/ui/packages/consul-ui/app/components/consul/discovery-chain/route-card/index.js +++ b/ui/packages/consul-ui/app/components/consul/discovery-chain/route-card/index.js @@ -4,7 +4,7 @@ import { get } from '@ember/object'; export default class RouteCard extends Component { get path() { return Object.entries(get(this.args.item, 'Definition.Match.HTTP') || {}).reduce( - function(prev, [key, value]) { + function (prev, [key, value]) { if (key.toLowerCase().startsWith('path')) { return { type: key.replace('Path', ''), diff --git a/ui/packages/consul-ui/app/components/consul/discovery-chain/utils.js b/ui/packages/consul-ui/app/components/consul/discovery-chain/utils.js index e9d8d7c4b..df20a6086 100644 --- a/ui/packages/consul-ui/app/components/consul/discovery-chain/utils.js +++ b/ui/packages/consul-ui/app/components/consul/discovery-chain/utils.js @@ -1,7 +1,7 @@ -const getNodesByType = function(nodes = {}, type) { - return Object.values(nodes).filter(item => item.Type === type); +const getNodesByType = function (nodes = {}, type) { + return Object.values(nodes).filter((item) => item.Type === type); }; -const findResolver = function(resolvers, service, nspace = 'default', partition = 'default', dc) { +const findResolver = function (resolvers, service, nspace = 'default', partition = 'default', dc) { if (typeof resolvers[service] === 'undefined') { resolvers[service] = { ID: `${service}.${nspace}.${partition}.${dc}`, @@ -11,16 +11,16 @@ const findResolver = function(resolvers, service, nspace = 'default', partition } return resolvers[service]; }; -export const getAlternateServices = function(targets, a) { +export const getAlternateServices = function (targets, a) { let type; - const Targets = targets.map(function(b) { + const Targets = targets.map(function (b) { // TODO: this isn't going to work past namespace for services // with dots in the name, but by the time that becomes an issue // we might have more data from the endpoint so we don't have to guess // right now the backend also doesn't support dots in service names - const [aRev, bRev] = [a, b].map(item => item.split('.').reverse()); + const [aRev, bRev] = [a, b].map((item) => item.split('.').reverse()); const types = ['Datacenter', 'Partition', 'Namespace', 'Service', 'Subset']; - return bRev.find(function(item, i) { + return bRev.find(function (item, i) { const res = item !== aRev[i]; if (res) { type = types[i]; @@ -34,8 +34,8 @@ export const getAlternateServices = function(targets, a) { }; }; -export const getSplitters = function(nodes) { - return getNodesByType(nodes, 'splitter').map(function(item) { +export const getSplitters = function (nodes) { + return getNodesByType(nodes, 'splitter').map(function (item) { // Splitters need IDs adding so we can find them in the DOM later // splitters have a service.nspace as a name // do the reverse dance to ensure we don't mess up any @@ -52,17 +52,17 @@ export const getSplitters = function(nodes) { }; }); }; -export const getRoutes = function(nodes, uid) { - return getNodesByType(nodes, 'router').reduce(function(prev, item) { +export const getRoutes = function (nodes, uid) { + return getNodesByType(nodes, 'router').reduce(function (prev, item) { return prev.concat( - item.Routes.map(function(route, i) { + item.Routes.map(function (route, i) { // Routes also have IDs added via createRoute return createRoute(route, item.Name, uid); }) ); }, []); }; -export const getResolvers = function( +export const getResolvers = function ( dc, partition = 'default', nspace = 'default', @@ -72,8 +72,8 @@ export const getResolvers = function( const resolvers = {}; // make all our resolver nodes Object.values(nodes) - .filter(item => item.Type === 'resolver') - .forEach(function(item) { + .filter((item) => item.Type === 'resolver') + .forEach(function (item) { const parts = item.Name.split('.'); let subset; // this will leave behind the service.name.nspace.partition.dc even if the service name contains a dot @@ -113,7 +113,7 @@ export const getResolvers = function( } } }); - Object.values(targets).forEach(target => { + Object.values(targets).forEach((target) => { // Failovers don't have a specific node if (typeof nodes[`resolver:${target.ID}`] !== 'undefined') { // We use this to figure out whether this target is a redirect target @@ -144,7 +144,7 @@ export const getResolvers = function( }); return Object.values(resolvers); }; -export const createRoute = function(route, router, uid) { +export const createRoute = function (route, router, uid) { return { ...route, Default: route.Default || typeof route.Definition.Match === 'undefined', diff --git a/ui/packages/consul-ui/app/components/consul/external-source/index.scss b/ui/packages/consul-ui/app/components/consul/external-source/index.scss index b05acb45b..0b6656ede 100644 --- a/ui/packages/consul-ui/app/components/consul/external-source/index.scss +++ b/ui/packages/consul-ui/app/components/consul/external-source/index.scss @@ -1,6 +1,11 @@ .consul-external-source { @extend %pill-200, %frame-gray-600, %p1; } + +.consul-external-source::before { + --icon-size: icon-300; +} + .consul-external-source.kubernetes::before { @extend %with-logo-kubernetes-color-icon, %as-pseudo; } @@ -15,10 +20,11 @@ @extend %with-logo-consul-color-icon, %as-pseudo; } .consul-external-source.vault::before { - @extend %with-vault-100; + @extend %with-vault-300; } +.consul-external-source.lambda::before, .consul-external-source.aws::before { - @extend %with-aws-100; + @extend %with-aws-300; } .consul-external-source.leader::before { @extend %with-star-outline-mask, %as-pseudo; diff --git a/ui/packages/consul-ui/app/components/consul/health-check/list/pageobject.js b/ui/packages/consul-ui/app/components/consul/health-check/list/pageobject.js index e57e47aea..286933d4c 100644 --- a/ui/packages/consul-ui/app/components/consul/health-check/list/pageobject.js +++ b/ui/packages/consul-ui/app/components/consul/health-check/list/pageobject.js @@ -1,10 +1,11 @@ -export default (collection, text) => (scope = '.consul-health-check-list') => { - return { - scope, - item: collection('li', { - name: text('header h3'), - type: text('[data-health-check-type]'), - exposed: text('[data-test-exposed]'), - }), +export default (collection, text) => + (scope = '.consul-health-check-list') => { + return { + scope, + item: collection('li', { + name: text('header h3'), + type: text('[data-health-check-type]'), + exposed: text('[data-test-exposed]'), + }), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.hbs b/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.hbs index e601bb557..9f9972a39 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.hbs @@ -192,7 +192,7 @@ @@ -201,7 +201,7 @@ {{else}} diff --git a/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.js b/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.js index b2d001be5..9437bb8e7 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.js +++ b/ui/packages/consul-ui/app/components/consul/intention/form/fieldsets/index.js @@ -5,20 +5,24 @@ export default Component.extend({ shouldShowPermissionForm: false, + openModal() { + this.modal?.open(); + }, + actions: { - createNewLabel: function(template, term) { + createNewLabel: function (template, term) { return template.replace(/{{term}}/g, term); }, - isUnique: function(items, term) { + isUnique: function (items, term) { return !items.findBy('Name', term); }, - add: function(name, changeset, value) { + add: function (name, changeset, value) { if (!(changeset.get(name) || []).includes(value) && value.isNew) { changeset.pushObject(name, value); changeset.validate(); } }, - delete: function(name, changeset, value) { + delete: function (name, changeset, value) { if ((changeset.get(name) || []).includes(value)) { changeset.removeObject(name, value); changeset.validate(); diff --git a/ui/packages/consul-ui/app/components/consul/intention/form/index.js b/ui/packages/consul-ui/app/components/consul/intention/form/index.js index c37ce5249..04615f7c2 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/form/index.js +++ b/ui/packages/consul-ui/app/components/consul/intention/form/index.js @@ -79,7 +79,9 @@ export default class ConsulIntentionForm extends Component { let items = e.data .uniqBy('Name') .toArray() - .filter(item => !['connect-proxy', 'mesh-gateway', 'terminating-gateway'].includes(item.Kind)) + .filter( + (item) => !['connect-proxy', 'mesh-gateway', 'terminating-gateway'].includes(item.Kind) + ) .sort((a, b) => a.Name.localeCompare(b.Name)); items = [{ Name: '*' }].concat(items); let source = items.findBy('Name', item.SourceName); diff --git a/ui/packages/consul-ui/app/components/consul/intention/list/pageobject.js b/ui/packages/consul-ui/app/components/consul/intention/list/pageobject.js index c8c60d6f4..ee53cc17f 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/list/pageobject.js +++ b/ui/packages/consul-ui/app/components/consul/intention/list/pageobject.js @@ -1,17 +1,19 @@ -export default (collection, clickable, attribute, isPresent, deletable) => ( - scope = '.consul-intention-list' -) => { - const row = { - source: attribute('data-test-intention-source', '[data-test-intention-source]'), - destination: attribute('data-test-intention-destination', '[data-test-intention-destination]'), - action: attribute('data-test-intention-action', '[data-test-intention-action]'), - intention: clickable('a'), - actions: clickable('label'), - ...deletable(), +export default (collection, clickable, attribute, isPresent, deletable) => + (scope = '.consul-intention-list') => { + const row = { + source: attribute('data-test-intention-source', '[data-test-intention-source]'), + destination: attribute( + 'data-test-intention-destination', + '[data-test-intention-destination]' + ), + action: attribute('data-test-intention-action', '[data-test-intention-action]'), + intention: clickable('a'), + actions: clickable('label'), + ...deletable(), + }; + return { + scope: scope, + customResourceNotice: isPresent('.consul-intention-notice-custom-resource'), + intentions: collection('[data-test-tabular-row]', row), + }; }; - return { - scope: scope, - customResourceNotice: isPresent('.consul-intention-notice-custom-resource'), - intentions: collection('[data-test-tabular-row]', row), - }; -}; diff --git a/ui/packages/consul-ui/app/components/consul/intention/permission/form/index.js b/ui/packages/consul-ui/app/components/consul/intention/permission/form/index.js index d52c0f885..eb6137361 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/permission/form/index.js +++ b/ui/packages/consul-ui/app/components/consul/intention/permission/form/index.js @@ -12,18 +12,18 @@ export default Component.extend({ change: service('change'), repo: service(`repository/${name}`), - onsubmit: function() {}, - onreset: function() {}, + onsubmit: function () {}, + onreset: function () {}, intents: alias(`schema.${name}.Action.allowedValues`), methods: alias(`schema.${name}-http.Methods.allowedValues`), pathProps: alias(`schema.${name}-http.PathType.allowedValues`), - pathTypes: computed('pathProps', function() { + pathTypes: computed('pathProps', function () { return ['NoPath'].concat(this.pathProps); }), - pathLabels: computed(function() { + pathLabels: computed(function () { return { NoPath: 'No Path', PathExact: 'Exact', @@ -32,7 +32,7 @@ export default Component.extend({ }; }), - pathInputLabels: computed(function() { + pathInputLabels: computed(function () { return { PathExact: 'Exact Path', PathPrefix: 'Path Prefix', @@ -40,7 +40,7 @@ export default Component.extend({ }; }), - changeset: computed('item', function() { + changeset: computed('item', function () { const changeset = this.change.changesetFor(name, this.item || this.repo.create()); if (changeset.isNew) { changeset.validate(); @@ -48,7 +48,7 @@ export default Component.extend({ return changeset; }), - pathType: computed('changeset._changes.HTTP.PathType', 'pathTypes.firstObject', function() { + pathType: computed('changeset._changes.HTTP.PathType', 'pathTypes.firstObject', function () { return this.changeset.HTTP.PathType || this.pathTypes.firstObject; }), noPathType: equal('pathType', 'NoPath'), @@ -57,14 +57,14 @@ export default Component.extend({ allMethods: false, shouldShowMethods: not('allMethods'), - didReceiveAttrs: function() { + didReceiveAttrs: function () { if (!get(this, 'item.HTTP.Methods.length')) { set(this, 'allMethods', true); } }, actions: { - change: function(name, changeset, e) { + change: function (name, changeset, e) { const value = typeof get(e, 'target.value') !== 'undefined' ? e.target.value : e; switch (name) { case 'allMethods': @@ -82,21 +82,21 @@ export default Component.extend({ } changeset.validate(); }, - add: function(prop, changeset, value) { + add: function (prop, changeset, value) { changeset.pushObject(prop, value); changeset.validate(); }, - delete: function(prop, changeset, value) { + delete: function (prop, changeset, value) { changeset.removeObject(prop, value); changeset.validate(); }, - submit: function(changeset, e) { + submit: function (changeset, e) { const pathChanged = typeof changeset.changes.find( ({ key, value }) => key === 'HTTP.PathType' || key === 'HTTP.Path' ) !== 'undefined'; if (pathChanged) { - this.pathProps.forEach(prop => { + this.pathProps.forEach((prop) => { changeset.set(`HTTP.${prop}`, undefined); }); if (changeset.HTTP.PathType !== 'NoPath') { @@ -115,7 +115,7 @@ export default Component.extend({ this.repo.persist(changeset); this.onsubmit(changeset.data); }, - reset: function(changeset, e) { + reset: function (changeset, e) { changeset.rollback(); this.onreset(changeset.data); }, diff --git a/ui/packages/consul-ui/app/components/consul/intention/permission/header/form/index.js b/ui/packages/consul-ui/app/components/consul/intention/permission/header/form/index.js index 80c79a0f4..ce63d2ad3 100644 --- a/ui/packages/consul-ui/app/components/consul/intention/permission/header/form/index.js +++ b/ui/packages/consul-ui/app/components/consul/intention/permission/header/form/index.js @@ -12,10 +12,10 @@ export default Component.extend({ change: service('change'), repo: service(`repository/${name}`), - onsubmit: function() {}, - onreset: function() {}, + onsubmit: function () {}, + onreset: function () {}, - changeset: computed('item', function() { + changeset: computed('item', function () { return this.change.changesetFor( name, this.item || @@ -27,7 +27,7 @@ export default Component.extend({ headerTypes: alias(`schema.${name}.HeaderType.allowedValues`), - headerLabels: computed(function() { + headerLabels: computed(function () { return { Exact: 'Exactly Matching', Prefix: 'Prefixed by', @@ -37,7 +37,7 @@ export default Component.extend({ }; }), - headerType: computed('changeset.HeaderType', 'headerTypes.firstObject', function() { + headerType: computed('changeset.HeaderType', 'headerTypes.firstObject', function () { return this.changeset.HeaderType || this.headerTypes.firstObject; }), @@ -45,7 +45,7 @@ export default Component.extend({ shouldShowValueField: not('headerTypeEqualsPresent'), actions: { - change: function(name, changeset, e) { + change: function (name, changeset, e) { const value = typeof get(e, 'target.value') !== 'undefined' ? e.target.value : e; switch (name) { default: @@ -53,8 +53,8 @@ export default Component.extend({ } changeset.validate(); }, - submit: function(changeset) { - this.headerTypes.forEach(prop => { + submit: function (changeset) { + this.headerTypes.forEach((prop) => { changeset.set(prop, undefined); }); // Present is a boolean, whereas all other header types have a value @@ -78,7 +78,7 @@ export default Component.extend({ }) ); }, - reset: function(changeset, e) { + reset: function (changeset, e) { changeset.rollback(); }, }, diff --git a/ui/packages/consul-ui/app/components/consul/kind/index.scss b/ui/packages/consul-ui/app/components/consul/kind/index.scss index 7467195f2..0431ac306 100644 --- a/ui/packages/consul-ui/app/components/consul/kind/index.scss +++ b/ui/packages/consul-ui/app/components/consul/kind/index.scss @@ -3,4 +3,5 @@ } .consul-kind::before { @extend %with-gateway-mask, %as-pseudo; + --icon-size: icon-300; } diff --git a/ui/packages/consul-ui/app/components/consul/kv/form/index.js b/ui/packages/consul-ui/app/components/consul/kv/form/index.js index 5ab27d694..baf628a6f 100644 --- a/ui/packages/consul-ui/app/components/consul/kv/form/index.js +++ b/ui/packages/consul-ui/app/components/consul/kv/form/index.js @@ -6,15 +6,15 @@ export default Component.extend({ tagName: '', encoder: service('btoa'), json: true, - ondelete: function() { + ondelete: function () { this.onsubmit(...arguments); }, - oncancel: function() { + oncancel: function () { this.onsubmit(...arguments); }, - onsubmit: function() {}, + onsubmit: function () {}, actions: { - change: function(e, form) { + change: function (e, form) { const item = form.getData(); try { form.handleEvent(e); diff --git a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js index 01a7249f3..852a42ecc 100644 --- a/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js +++ b/ui/packages/consul-ui/app/components/consul/tomography/graph/index.js @@ -3,10 +3,10 @@ import { tracked } from '@glimmer/tracking'; const size = 336; const insetSize = size / 2 - 8; -const inset = function(num) { +const inset = function (num) { return insetSize * num; }; -const milliseconds = function(num, max) { +const milliseconds = function (num, max) { return max > 0 ? parseInt(max * num) / 100 : 0; }; export default class TomographyGraph extends Component { @@ -19,7 +19,7 @@ export default class TomographyGraph extends Component { get milliseconds() { const distances = this.args.distances || []; const max = distances.reduce((prev, d) => Math.max(prev, d.distance), this.max); - return [25, 50, 75, 100].map(item => milliseconds(item, max)); + return [25, 50, 75, 100].map((item) => milliseconds(item, max)); } get distances() { @@ -30,7 +30,7 @@ export default class TomographyGraph extends Component { // We have more nodes than we want to show, take a random sampling to keep // the number around 360. const sampling = 360 / len; - distances = distances.filter(function(_, i) { + distances = distances.filter(function (_, i) { return i == 0 || i == len - 1 || Math.random() < sampling; }); } diff --git a/ui/packages/consul-ui/app/components/consul/upstream-instance/list/pageobject.js b/ui/packages/consul-ui/app/components/consul/upstream-instance/list/pageobject.js index 552200a39..b233a60c2 100644 --- a/ui/packages/consul-ui/app/components/consul/upstream-instance/list/pageobject.js +++ b/ui/packages/consul-ui/app/components/consul/upstream-instance/list/pageobject.js @@ -1,11 +1,12 @@ -export default (collection, text) => (scope = '.consul-upstream-instance-list') => { - return { - scope, - item: collection('li', { - name: text('.header p'), - nspace: text('.nspace dd'), - datacenter: text('.datacenter dd'), - localAddress: text('.local-address dd'), - }), +export default (collection, text) => + (scope = '.consul-upstream-instance-list') => { + return { + scope, + item: collection('li', { + name: text('.header p'), + nspace: text('.nspace dd'), + datacenter: text('.datacenter dd'), + localAddress: text('.local-address dd'), + }), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/custom-element/index.js b/ui/packages/consul-ui/app/components/custom-element/index.js index 6d52fdfcc..7e1605a68 100644 --- a/ui/packages/consul-ui/app/components/custom-element/index.js +++ b/ui/packages/consul-ui/app/components/custom-element/index.js @@ -11,17 +11,23 @@ const typeCast = (attributeInfo, value) => { let type = attributeInfo.type; const d = attributeInfo.default; value = value == null ? attributeInfo.default : value; - if(type.indexOf('|') !== -1) { - assert(`"${value} is not of type '${type}'"`, type.split('|').map(item => item.replaceAll('"', '').trim()).includes(value)); + if (type.indexOf('|') !== -1) { + assert( + `"${value} is not of type '${type}'"`, + type + .split('|') + .map((item) => item.replaceAll('"', '').trim()) + .includes(value) + ); type = 'string'; } - switch(type) { + switch (type) { case '': case '': case '': case 'number': { const num = parseFloat(value); - if(isNaN(num)) { + if (isNaN(num)) { return typeof d === 'undefined' ? 0 : d; } else { return num; @@ -33,7 +39,7 @@ const typeCast = (attributeInfo, value) => { case 'string': return (value || '').toString(); } -} +}; const attributeChangingElement = (name, Cls = HTMLElement, attributes = {}, cssprops = {}) => { const attrs = Object.keys(attributes); @@ -48,65 +54,58 @@ const attributeChangingElement = (name, Cls = HTMLElement, attributes = {}, cssp const value = typeCast(attributes[name], newValue); const cssProp = cssprops[`--${name}`]; - if(typeof cssProp !== 'undefined' && cssProp.track === `[${name}]`) { - this.style.setProperty( - `--${name}`, - value - ); + if (typeof cssProp !== 'undefined' && cssProp.track === `[${name}]`) { + this.style.setProperty(`--${name}`, value); } - if(typeof super.attributeChangedCallback === 'function') { + if (typeof super.attributeChangedCallback === 'function') { super.attributeChangedCallback(name, prev, value); } this.dispatchEvent( - new CustomEvent( - ATTRIBUTE_CHANGE, - { - detail: { - name: name, - previousValue: prev, - value: value - } - } - ) + new CustomEvent(ATTRIBUTE_CHANGE, { + detail: { + name: name, + previousValue: prev, + value: value, + }, + }) ); - } - } + }; customElements.define(name, customClass); return () => {}; -} +}; const infoFromArray = (arr, keys) => { return (arr || []).reduce((prev, info) => { let key; const obj = {}; keys.forEach((item, i) => { - if(item === '_') { + if (item === '_') { key = i; return; } - obj[item] = info[i] + obj[item] = info[i]; }); prev[info[key]] = obj; return prev; }, {}); -} +}; const debounceRAF = (cb, prev) => { - if(typeof prev !== 'undefined') { + if (typeof prev !== 'undefined') { cancelAnimationFrame(prev); } return requestAnimationFrame(cb); -} +}; const createElementProxy = ($element, component) => { return new Proxy($element, { get: (target, prop, receiver) => { - switch(prop) { + switch (prop) { case 'attrs': return component.attributes; default: - if(typeof target[prop] === 'function') { + if (typeof target[prop] === 'function') { // need to ensure we use a MultiWeakMap here // if(this.methods.has(prop)) { // return this.methods.get(prop); @@ -115,30 +114,27 @@ const createElementProxy = ($element, component) => { // this.methods.set(prop, method); return method; } - } - } + }, }); -} +}; export default class CustomElementComponent extends Component { - @tracked $element; @tracked _attributes = {}; __attributes; _attchange; - constructor(owner, args) { super(...arguments); - if(!elements.has(args.element)) { + if (!elements.has(args.element)) { const cb = attributeChangingElement( args.element, args.class, infoFromArray(args.attrs, ['_', 'type', 'default', 'description']), infoFromArray(args.cssprops, ['_', 'type', 'track', 'description']) - ) + ); elements.set(args.element, cb); } } @@ -148,8 +144,8 @@ export default class CustomElementComponent extends Component { } get element() { - if(this.$element) { - if(proxies.has(this.$element)) { + if (this.$element) { + if (proxies.has(this.$element)) { return proxies.get(this.$element); } const proxy = createElementProxy(this.$element, this); @@ -165,9 +161,9 @@ export default class CustomElementComponent extends Component { this.$element = $element; this.$element.addEventListener(ATTRIBUTE_CHANGE, this.attributeChange); - (this.args.attrs || []).forEach(entry => { + (this.args.attrs || []).forEach((entry) => { const value = $element.getAttribute(entry[0]); - $element.attributeChangedCallback(entry[0], value, value) + $element.attributeChangedCallback(entry[0], value, value); }); } @@ -183,7 +179,7 @@ export default class CustomElementComponent extends Component { // they all change this.__attributes = { ...this.__attributes, - [e.detail.name]: e.detail.value + [e.detail.name]: e.detail.value, }; this._attchange = debounceRAF(() => { // tell glimmer we changed the attrs diff --git a/ui/packages/consul-ui/app/components/data-form/index.js b/ui/packages/consul-ui/app/components/data-form/index.js index b7db7af66..1f6dae1fd 100644 --- a/ui/packages/consul-ui/app/components/data-form/index.js +++ b/ui/packages/consul-ui/app/components/data-form/index.js @@ -9,17 +9,17 @@ export default Component.extend(Slotted, { dom: service('dom'), builder: service('form'), create: false, - ondelete: function() { + ondelete: function () { return this.onsubmit(...arguments); }, - oncancel: function() { + oncancel: function () { return this.onsubmit(...arguments); }, - onsubmit: function() {}, - onchange: function(e, form) { + onsubmit: function () {}, + onchange: function (e, form) { return form.handleEvent(e); }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); try { this.form = this.builder.form(this.type); @@ -28,18 +28,18 @@ export default Component.extend(Slotted, { // this lets us load view only data that doesn't have a form } }, - willRender: function() { + willRender: function () { this._super(...arguments); set(this, 'hasError', this._isRegistered('error')); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); if (get(this, 'data.isNew')) { this.data.rollbackAttributes(); } }, actions: { - setData: function(data) { + setData: function (data) { let changeset = data; // convert to a real changeset if (!isChangeset(data) && typeof this.form !== 'undefined') { @@ -49,7 +49,7 @@ export default Component.extend(Slotted, { // and autofill the new record if required if (get(data, 'isNew')) { set(this, 'create', true); - changeset = Object.entries(this.autofill || {}).reduce(function(prev, [key, value]) { + changeset = Object.entries(this.autofill || {}).reduce(function (prev, [key, value]) { set(prev, key, value); return prev; }, changeset); @@ -57,7 +57,7 @@ export default Component.extend(Slotted, { set(this, 'data', changeset); return this.data; }, - change: function(e, value, item) { + change: function (e, value, item) { this.onchange(this.dom.normalizeEvent(e, value), this.form, this.form.getData()); }, }, diff --git a/ui/packages/consul-ui/app/components/data-loader/index.js b/ui/packages/consul-ui/app/components/data-loader/index.js index 19967a12a..16fc091db 100644 --- a/ui/packages/consul-ui/app/components/data-loader/index.js +++ b/ui/packages/consul-ui/app/components/data-loader/index.js @@ -5,26 +5,26 @@ import Slotted from 'block-slots'; import chart from './chart.xstate'; export default Component.extend(Slotted, { tagName: '', - onchange: data => data, - init: function() { + onchange: (data) => data, + init: function () { this._super(...arguments); this.chart = chart; }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); if (typeof this.items !== 'undefined') { this.actions.change.apply(this, [this.items]); } }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); this.dispatch('LOAD'); }, actions: { - isLoaded: function() { + isLoaded: function () { return typeof this.items !== 'undefined' || typeof this.src === 'undefined'; }, - change: function(data) { + change: function (data) { set(this, 'data', this.onchange(data)); }, }, diff --git a/ui/packages/consul-ui/app/components/data-sink/index.js b/ui/packages/consul-ui/app/components/data-sink/index.js index c3e463d69..4472cf26d 100644 --- a/ui/packages/consul-ui/app/components/data-sink/index.js +++ b/ui/packages/consul-ui/app/components/data-sink/index.js @@ -11,10 +11,10 @@ export default Component.extend({ dom: service('dom'), logger: service('logger'), - onchange: function(e) {}, - onerror: function(e) {}, + onchange: function (e) {}, + onerror: function (e) {}, - state: computed('instance', 'instance.{dirtyType,isSaving}', function() { + state: computed('instance', 'instance.{dirtyType,isSaving}', function () { let id; const isSaving = get(this, 'instance.isSaving'); const dirtyType = get(this, 'instance.dirtyType'); @@ -36,21 +36,21 @@ export default Component.extend({ id = `active.${id}`; } return { - matches: name => id.indexOf(name) !== -1, + matches: (name) => id.indexOf(name) !== -1, }; }), - init: function() { + init: function () { this._super(...arguments); this._listeners = this.dom.listeners(); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this._listeners.remove(); }, - source: function(cb) { + source: function (cb) { const source = once(cb); - const error = err => { + const error = (err) => { set(this, 'instance', undefined); try { this.onerror(err); @@ -60,7 +60,7 @@ export default Component.extend({ } }; this._listeners.add(source, { - message: e => { + message: (e) => { try { set(this, 'instance', undefined); this.onchange(e); @@ -68,17 +68,17 @@ export default Component.extend({ error(err); } }, - error: e => error(e), + error: (e) => error(e), }); return source; }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); if (typeof this.data !== 'undefined' || typeof this.item !== 'undefined') { this.actions.open.apply(this, [this.data, this.item]); } }, - persist: function(data, instance) { + persist: function (data, instance) { if (typeof data !== 'undefined') { set(this, 'instance', this.service.prepare(this.sink, data, instance)); } else { @@ -86,12 +86,12 @@ export default Component.extend({ } this.source(() => this.service.persist(this.sink, this.instance)); }, - remove: function(instance) { + remove: function (instance) { set(this, 'instance', instance); this.source(() => this.service.remove(this.sink, instance)); }, actions: { - open: function(data, item) { + open: function (data, item) { if (item instanceof Event) { item = undefined; } diff --git a/ui/packages/consul-ui/app/components/data-source/index.js b/ui/packages/consul-ui/app/components/data-source/index.js index 3d5d66ebd..ac0b56506 100644 --- a/ui/packages/consul-ui/app/components/data-source/index.js +++ b/ui/packages/consul-ui/app/components/data-source/index.js @@ -15,7 +15,7 @@ import { runInDebug } from '@ember/debug'; * @param value - value to use for replacement * @param destroy {(prev: any, value: any) => any} - teardown function */ -const replace = function( +const replace = function ( obj, prop, value, @@ -29,7 +29,7 @@ const replace = function( }; const noop = () => {}; -const optional = op => (typeof op === 'function' ? op : noop); +const optional = (op) => (typeof op === 'function' ? op : noop); // possible values for @loading="" const LOADING = ['eager', 'lazy']; @@ -74,7 +74,7 @@ export default class DataSource extends Component { // otherwise its an array from the did-insert-helper if (!Array.isArray($el)) { this._lazyListeners.add( - this.dom.isInViewport($el, inViewport => { + this.dom.isInViewport($el, (inViewport) => { this.isIntersecting = inViewport; if (!this.isIntersecting) { this.close(); @@ -130,7 +130,7 @@ export default class DataSource extends Component { this.dataSource.close(prev, this); } ); - const error = err => { + const error = (err) => { try { const error = get(err, 'error.errors.firstObject') || {}; if (get(error, 'status') !== '429') { @@ -143,14 +143,14 @@ export default class DataSource extends Component { }; // set up the listeners (which auto cleanup on component destruction) const remove = this._listeners.add(this.source, { - message: e => { + message: (e) => { try { this.onchange(e); } catch (err) { error(err); } }, - error: e => { + error: (e) => { error(e); }, }); @@ -187,7 +187,7 @@ export default class DataSource extends Component { this.disconnect(); schedule('afterRender', () => { // TODO: Support lazy data-sources by keeping a reference to $el - runInDebug(_ => + runInDebug((_) => console.debug( `Invalidation is only supported for non-lazy data sources. If you want to use this you should fixup support for lazy data sources` ) diff --git a/ui/packages/consul-ui/app/components/data-writer/index.hbs b/ui/packages/consul-ui/app/components/data-writer/index.hbs index 5f0ecdfcd..36f418459 100644 --- a/ui/packages/consul-ui/app/components/data-writer/index.hbs +++ b/ui/packages/consul-ui/app/components/data-writer/index.hbs @@ -108,6 +108,8 @@ as |after|}} There was an error saving your {{or label type}}. {{#if (and api.error.status api.error.detail)}}
{{api.error.status}}: {{api.error.detail}} + {{else if api.error.message}} +
{{api.error.message}} {{/if}}

diff --git a/ui/packages/consul-ui/app/components/data-writer/index.js b/ui/packages/consul-ui/app/components/data-writer/index.js index 3dd1f8ab9..af6e7207f 100644 --- a/ui/packages/consul-ui/app/components/data-writer/index.js +++ b/ui/packages/consul-ui/app/components/data-writer/index.js @@ -5,31 +5,30 @@ import chart from './chart.xstate'; export default Component.extend(Slotted, { tagName: '', - ondelete: function() { + ondelete: function () { return this.onchange(...arguments); }, - onchange: function() {}, - init: function() { + onchange: function () {}, + init: function () { this._super(...arguments); this.chart = chart; }, actions: { - persist: function(data, e) { + persist: function (data, e) { if (e && typeof e.preventDefault === 'function') { e.preventDefault(); } set(this, 'data', data); this.dispatch('PERSIST'); }, - error: function(data, e) { + error: function (data, e) { if (e && typeof e.preventDefault === 'function') { e.preventDefault(); } set( this, 'error', - typeof data.error.errors !== 'undefined' ? - data.error.errors.firstObject : data.error + typeof data.error.errors !== 'undefined' ? data.error.errors.firstObject : data.error ); this.dispatch('ERROR'); }, diff --git a/ui/packages/consul-ui/app/components/delete-confirmation/index.js b/ui/packages/consul-ui/app/components/delete-confirmation/index.js index fe5ae1b25..6d5168aa7 100644 --- a/ui/packages/consul-ui/app/components/delete-confirmation/index.js +++ b/ui/packages/consul-ui/app/components/delete-confirmation/index.js @@ -2,6 +2,6 @@ import Component from '@ember/component'; export default Component.extend({ tagName: '', - execute: function() {}, - cancel: function() {}, + execute: function () {}, + cancel: function () {}, }); diff --git a/ui/packages/consul-ui/app/components/disclosure/index.js b/ui/packages/consul-ui/app/components/disclosure/index.js index 0774ae310..575082739 100644 --- a/ui/packages/consul-ui/app/components/disclosure/index.js +++ b/ui/packages/consul-ui/app/components/disclosure/index.js @@ -17,7 +17,7 @@ export default class DisclosureComponent extends Component { remove(id) { this.ids = this.ids .split(' ') - .filter(item => item !== id) + .filter((item) => item !== id) .join(' '); } } diff --git a/ui/packages/consul-ui/app/components/distribution-meter/index.css.js b/ui/packages/consul-ui/app/components/distribution-meter/index.css.js index eeda65a0b..336511f68 100644 --- a/ui/packages/consul-ui/app/components/distribution-meter/index.css.js +++ b/ui/packages/consul-ui/app/components/distribution-meter/index.css.js @@ -22,11 +22,11 @@ export default (css) => { border-radius: var(--decor-radius-999); transition-property: transform; transition-timing-function: ease-out; - transition-duration: .1s; + transition-duration: 0.1s; } :host([type='linear']) dl:hover { transform: scaleY(3); box-shadow: var(--decor-elevation-200); } `; -} +}; diff --git a/ui/packages/consul-ui/app/components/distribution-meter/meter/element.js b/ui/packages/consul-ui/app/components/distribution-meter/meter/element.js index 69e9ace4b..5c058fdf0 100644 --- a/ui/packages/consul-ui/app/components/distribution-meter/meter/element.js +++ b/ui/packages/consul-ui/app/components/distribution-meter/meter/element.js @@ -1,19 +1,22 @@ const parseFloatWithDefault = (val, d = 0) => { const num = parseFloat(val); return isNaN(num) ? d : num; -} +}; export default (Component) => { return class extends Component { attributeChangedCallback(name, prev, value) { const target = this; - switch(name) { + switch (name) { case 'percentage': { let prevSibling = target; - while(prevSibling) { + while (prevSibling) { const nextSibling = prevSibling.nextElementSibling; - const aggregatedPercentage = nextSibling ? parseFloatWithDefault(nextSibling.style.getPropertyValue('--aggregated-percentage')) : 0; - const perc = parseFloatWithDefault(prevSibling.getAttribute('percentage')) + aggregatedPercentage; + const aggregatedPercentage = nextSibling + ? parseFloatWithDefault(nextSibling.style.getPropertyValue('--aggregated-percentage')) + : 0; + const perc = + parseFloatWithDefault(prevSibling.getAttribute('percentage')) + aggregatedPercentage; prevSibling.style.setProperty('--aggregated-percentage', perc); prevSibling.setAttribute('aggregated-percentage', perc); prevSibling = prevSibling.previousElementSibling; @@ -22,5 +25,5 @@ export default (Component) => { } } } - } -} + }; +}; diff --git a/ui/packages/consul-ui/app/components/distribution-meter/meter/index.css.js b/ui/packages/consul-ui/app/components/distribution-meter/meter/index.css.js index f2d75c4b4..7f105a8d9 100644 --- a/ui/packages/consul-ui/app/components/distribution-meter/meter/index.css.js +++ b/ui/packages/consul-ui/app/components/distribution-meter/meter/index.css.js @@ -18,9 +18,10 @@ export default (css) => { height: 100%; transition-timing-function: ease-out; - transition-duration: .5s; + transition-duration: 0.5s; } - dt, dd meter { + dt, + dd meter { animation-name: visually-hidden; animation-fill-mode: forwards; animation-play-state: paused; @@ -49,7 +50,7 @@ export default (css) => { :host(.type-radial) circle, :host(.type-circular) circle { transition-timing-function: ease-out; - transition-duration: .5s; + transition-duration: 0.5s; pointer-events: stroke; transition-property: stroke-dashoffset, stroke-width; transform: rotate(-90deg); @@ -76,4 +77,4 @@ export default (css) => { stroke-width: 14; } `; -} +}; diff --git a/ui/packages/consul-ui/app/components/empty-state/index.js b/ui/packages/consul-ui/app/components/empty-state/index.js index 2a0182c36..c4682abad 100644 --- a/ui/packages/consul-ui/app/components/empty-state/index.js +++ b/ui/packages/consul-ui/app/components/empty-state/index.js @@ -4,7 +4,7 @@ import Slotted from 'block-slots'; export default Component.extend(Slotted, { tagName: '', - willRender: function() { + willRender: function () { this._super(...arguments); set(this, 'hasHeader', this._isRegistered('header') || this._isRegistered('subheader')); }, diff --git a/ui/packages/consul-ui/app/components/empty-state/pageobject.js b/ui/packages/consul-ui/app/components/empty-state/pageobject.js index b6dbb951f..e995697f9 100644 --- a/ui/packages/consul-ui/app/components/empty-state/pageobject.js +++ b/ui/packages/consul-ui/app/components/empty-state/pageobject.js @@ -1,6 +1,7 @@ -export default present => (scope = '.empty-state') => { - return { - scope: scope, - login: present('[data-test-empty-state-login]'), +export default (present) => + (scope = '.empty-state') => { + return { + scope: scope, + login: present('[data-test-empty-state-login]'), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/event-source/index.js b/ui/packages/consul-ui/app/components/event-source/index.js index 833829a11..f8eb439fb 100644 --- a/ui/packages/consul-ui/app/components/event-source/index.js +++ b/ui/packages/consul-ui/app/components/event-source/index.js @@ -2,7 +2,7 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; import { get, set } from '@ember/object'; -const replace = function( +const replace = function ( obj, prop, value, @@ -20,21 +20,21 @@ export default Component.extend({ logger: service('logger'), data: service('data-source/service'), closeOnDestroy: true, - onerror: function(e) { + onerror: function (e) { this.logger.execute(e.error); }, - init: function() { + init: function () { this._super(...arguments); this._listeners = this.dom.listeners(); }, - willDestroyElement: function() { + willDestroyElement: function () { if (this.closeOnDestroy) { this.actions.close.apply(this, []); } this._listeners.remove(); this._super(...arguments); }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); // only close and reopen if the uri changes // otherwise this will fire whenever the proxies data changes @@ -43,7 +43,7 @@ export default Component.extend({ } }, actions: { - open: function() { + open: function () { replace(this, 'source', this.data.open(this.src, this), (prev, source) => { // Makes sure any previous source (if different) is ALWAYS closed if (typeof prev !== 'undefined') { @@ -56,7 +56,7 @@ export default Component.extend({ prev.destroy(); } }); - const error = err => { + const error = (err) => { try { const error = get(err, 'error.errors.firstObject'); if (get(error || {}, 'status') !== '429') { @@ -71,13 +71,13 @@ export default Component.extend({ // we only need errors here as this only uses proxies which // automatically update their data const remove = this._listeners.add(this.source, { - error: e => { + error: (e) => { error(e); }, }); replace(this, '_remove', remove); }, - close: function() { + close: function () { if (typeof this.source !== 'undefined') { this.data.close(this.source, this); replace(this, '_remove', undefined); diff --git a/ui/packages/consul-ui/app/components/form-component/index.js b/ui/packages/consul-ui/app/components/form-component/index.js index d1b23fee3..3976d603a 100644 --- a/ui/packages/consul-ui/app/components/form-component/index.js +++ b/ui/packages/consul-ui/app/components/form-component/index.js @@ -6,10 +6,10 @@ import { alias } from '@ember/object/computed'; const propRe = /([^[\]])+/g; export default Component.extend(Slotted, { tagName: '', - onreset: function() {}, - onchange: function() {}, - onerror: function() {}, - onsuccess: function() {}, + onreset: function () {}, + onchange: function () {}, + onerror: function () {}, + onsuccess: function () {}, data: alias('form.data'), item: alias('form.data'), @@ -20,7 +20,7 @@ export default Component.extend(Slotted, { container: service('form'), actions: { - change: function(e, value, item) { + change: function (e, value, item) { let event = this.dom.normalizeEvent(e, value); // currently form-components don't deal with deeply nested forms, only top level // we therefore grab the end of the nest off here, diff --git a/ui/packages/consul-ui/app/components/form-group/element/index.js b/ui/packages/consul-ui/app/components/form-group/element/index.js index c76c2d7ad..41306ff73 100644 --- a/ui/packages/consul-ui/app/components/form-group/element/index.js +++ b/ui/packages/consul-ui/app/components/form-group/element/index.js @@ -21,15 +21,12 @@ export default class Element extends Component { } } get prop() { - return `${this.args.name - .toLowerCase() - .split('.') - .join('-')}`; + return `${this.args.name.toLowerCase().split('.').join('-')}`; } get state() { const error = this.touched && this.args.error; return { - matches: name => name === 'error' && error, + matches: (name) => name === 'error' && error, }; } diff --git a/ui/packages/consul-ui/app/components/freetext-filter/pageobject.js b/ui/packages/consul-ui/app/components/freetext-filter/pageobject.js index 2dae76669..ea1afa92a 100644 --- a/ui/packages/consul-ui/app/components/freetext-filter/pageobject.js +++ b/ui/packages/consul-ui/app/components/freetext-filter/pageobject.js @@ -1,4 +1,4 @@ -export default triggerable => () => { +export default (triggerable) => () => { return { ...{ search: triggerable('keypress', '[name="s"]'), diff --git a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs index 4d7a040ff..672985310 100644 --- a/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs +++ b/ui/packages/consul-ui/app/components/hashicorp-consul/index.hbs @@ -86,13 +86,14 @@ <:main-nav> +
    - + scope => { +export default (collection, clickable, attribute, is, authForm, emptyState) => (scope) => { const page = { navigation: [ 'services', @@ -12,7 +12,7 @@ export default (collection, clickable, attribute, is, authForm, emptyState) => s 'settings', 'auth', ].reduce( - function(prev, item, i, arr) { + function (prev, item, i, arr) { const key = item; return Object.assign({}, prev, { [key]: clickable(`[data-test-main-nav-${item}] > *`), @@ -23,7 +23,7 @@ export default (collection, clickable, attribute, is, authForm, emptyState) => s } ), footer: ['copyright', 'docs'].reduce( - function(prev, item, i, arr) { + function (prev, item, i, arr) { const key = item; return Object.assign({}, prev, { [key]: clickable(`[data-test-main-nav-${item}`), diff --git a/ui/packages/consul-ui/app/components/jwt-source/index.js b/ui/packages/consul-ui/app/components/jwt-source/index.js index 664e5fcf7..346db0299 100644 --- a/ui/packages/consul-ui/app/components/jwt-source/index.js +++ b/ui/packages/consul-ui/app/components/jwt-source/index.js @@ -18,19 +18,19 @@ export default class JWTSource extends Component { // TODO: Could this use once? Double check but I don't think it can this.source = fromPromise(this.repo.findCodeByURL(this.args.src)); this._listeners.add(this.source, { - message: e => this.onchange(e), - error: e => this.onerror(e), + message: (e) => this.onchange(e), + error: (e) => this.onerror(e), }); } onchange(e) { - if(typeof this.args.onchange === 'function') { + if (typeof this.args.onchange === 'function') { this.args.onchange(...arguments); } } onerror(e) { - if(typeof this.args.onerror === 'function') { + if (typeof this.args.onerror === 'function') { this.args.onerror(...arguments); } } diff --git a/ui/packages/consul-ui/app/components/list-collection/index.js b/ui/packages/consul-ui/app/components/list-collection/index.js index 9724e9a73..bfafb198d 100644 --- a/ui/packages/consul-ui/app/components/list-collection/index.js +++ b/ui/packages/consul-ui/app/components/list-collection/index.js @@ -13,19 +13,19 @@ export default Component.extend(Slotted, { cellHeight: 70, checked: null, scroll: 'virtual', - init: function() { + init: function () { this._super(...arguments); this.columns = [100]; this.guid = this.dom.guid(this); }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); this.$element = this.dom.element(`#${this.guid}`); if (this.scroll === 'virtual') { this.actions.resize.apply(this, [{ target: this.dom.viewport() }]); } }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); this._cellLayout = this['cell-layout'] = new PercentageColumns( get(this, 'items.length'), @@ -33,7 +33,7 @@ export default Component.extend(Slotted, { get(this, 'cellHeight') ); const o = this; - this['cell-layout'].formatItemStyle = function(itemIndex) { + this['cell-layout'].formatItemStyle = function (itemIndex) { let style = formatItemStyle.apply(this, arguments); if (o.checked === itemIndex) { style = `${style};z-index: 1`; @@ -41,7 +41,7 @@ export default Component.extend(Slotted, { return style; }; }, - style: computed('height', function() { + style: computed('height', function () { if (this.scroll !== 'virtual') { return {}; } @@ -50,7 +50,7 @@ export default Component.extend(Slotted, { }; }), actions: { - resize: function(e) { + resize: function (e) { // TODO: This top part is very similar to resize in tabular-collection // see if it make sense to DRY out const dom = get(this, 'dom'); @@ -65,10 +65,10 @@ export default Component.extend(Slotted, { this.updateScrollPosition(); } }, - click: function(e) { + click: function (e) { return this.dom.clickFirstAnchor(e, '.list-collection > ul > li'); }, - change: function(index, e = {}) { + change: function (index, e = {}) { if (e.target.checked && index !== get(this, 'checked')) { set(this, 'checked', parseInt(index)); this.$row = this.dom.closest('li', e.target); diff --git a/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss b/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss index a37b1d83e..ab9130b93 100644 --- a/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss +++ b/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss @@ -8,9 +8,8 @@ @extend %main-nav-vertical-item; } /**/ - /* actual clickable button-y things plus states */ -%main-nav-vertical > ul > li > a { +%main-nav-vertical a { @extend %main-nav-vertical-action; } %main-nav-vertical > ul > li.is-active > a { diff --git a/ui/packages/consul-ui/app/components/menu-panel/index.js b/ui/packages/consul-ui/app/components/menu-panel/index.js index 1e645ea5b..fca3fcb99 100644 --- a/ui/packages/consul-ui/app/components/menu-panel/index.js +++ b/ui/packages/consul-ui/app/components/menu-panel/index.js @@ -10,9 +10,9 @@ export default Component.extend(Slotted, { dom: service('dom'), isConfirmation: false, actions: { - connect: function($el) { + connect: function ($el) { next(() => { - if(!this.isDestroyed) { + if (!this.isDestroyed) { // if theres only a single choice in the menu and it doesn't have an // immediate button/link/label to click then it will be a // confirmation/informed action @@ -24,7 +24,7 @@ export default Component.extend(Slotted, { } }); }, - change: function(e) { + change: function (e) { const id = e.target.getAttribute('id'); const $trigger = this.dom.element(`[for='${id}']`); const $panel = this.dom.element('[role=menu]', $trigger.parentElement); diff --git a/ui/packages/consul-ui/app/components/modal-dialog/index.js b/ui/packages/consul-ui/app/components/modal-dialog/index.js index c9025fc7d..79b68b667 100644 --- a/ui/packages/consul-ui/app/components/modal-dialog/index.js +++ b/ui/packages/consul-ui/app/components/modal-dialog/index.js @@ -6,31 +6,31 @@ import { schedule } from '@ember/runloop'; export default Component.extend(Slotted, { tagName: '', - onclose: function() {}, - onopen: function() {}, + onclose: function () {}, + onopen: function () {}, isOpen: false, actions: { - connect: function($el) { + connect: function ($el) { this.dialog = new A11yDialog($el); this.dialog.on('hide', () => { - schedule('afterRender', _ => set(this, 'isOpen', false)); - this.onclose({ target: $el }) + schedule('afterRender', (_) => set(this, 'isOpen', false)); + this.onclose({ target: $el }); }); this.dialog.on('show', () => { - set(this, 'isOpen', true) - this.onopen({ target: $el }) + set(this, 'isOpen', true); + this.onopen({ target: $el }); }); if (this.open) { this.actions.open.apply(this, []); } }, - disconnect: function($el) { + disconnect: function ($el) { this.dialog.destroy(); }, - open: function() { + open: function () { this.dialog.show(); }, - close: function() { + close: function () { this.dialog.hide(); }, }, diff --git a/ui/packages/consul-ui/app/components/outlet/index.js b/ui/packages/consul-ui/app/components/outlet/index.js index 4910f1d1b..e63ec68e2 100644 --- a/ui/packages/consul-ui/app/components/outlet/index.js +++ b/ui/packages/consul-ui/app/components/outlet/index.js @@ -59,7 +59,7 @@ export default class Outlet extends Component { } break; case 'model': - if(typeof this.route !== 'undefined') { + if (typeof this.route !== 'undefined') { this.route._model = value; } break; diff --git a/ui/packages/consul-ui/app/components/paged-collection/index.js b/ui/packages/consul-ui/app/components/paged-collection/index.js index 840ee17f7..8013aaf0b 100644 --- a/ui/packages/consul-ui/app/components/paged-collection/index.js +++ b/ui/packages/consul-ui/app/components/paged-collection/index.js @@ -1,9 +1,9 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; +import { scheduleOnce } from '@ember/runloop'; export default class PagedCollectionComponent extends Component { - @tracked $pane; @tracked $viewport; @@ -25,7 +25,7 @@ export default class PagedCollectionComponent extends Component { get perPage() { switch (this.type) { case 'virtual-scroll': - return this.visibleItems + (this.overflow * 2); + return this.visibleItems + this.overflow * 2; case 'index': return parseInt(this.args.perPage); } @@ -45,7 +45,7 @@ export default class PagedCollectionComponent extends Component { } get itemsBefore() { - if(typeof this.$viewport === 'undefined') { + if (typeof this.$viewport === 'undefined') { return 0; } return Math.max(0, Math.round(this.top / this.rowHeight) - this.overflow); @@ -84,7 +84,7 @@ export default class PagedCollectionComponent extends Component { @action resize() { - if(this.$viewport.clientHeight > 0 && this.rowHeight > 0) { + if (this.$viewport.clientHeight > 0 && this.rowHeight > 0) { this.visibleItems = Math.ceil(this.$viewport.clientHeight / this.rowHeight); } else { this.visibleItems = 0; @@ -93,9 +93,10 @@ export default class PagedCollectionComponent extends Component { @action setViewport($viewport) { - this.$viewport = $viewport === 'html' ? [...document.getElementsByTagName('html')][0] : $viewport; + this.$viewport = + $viewport === 'html' ? [...document.getElementsByTagName('html')][0] : $viewport; this.$viewport.addEventListener('scroll', this.scroll); - if($viewport === 'html') { + if ($viewport === 'html') { this.$viewport.addEventListener('resize', this.resize); } this.scroll(); @@ -111,8 +112,13 @@ export default class PagedCollectionComponent extends Component { } @action setMaxHeight(str) { + scheduleOnce('actions', this, '_setMaxHeight'); + } + + @action _setMaxHeight(str) { const maxHeight = parseFloat(str); - if(!isNaN(maxHeight)) { + + if (!isNaN(maxHeight)) { this._type = 'virtual-scroll'; } } diff --git a/ui/packages/consul-ui/app/components/peerings/badge/index.hbs b/ui/packages/consul-ui/app/components/peerings/badge/index.hbs index d90d1cbf0..f8d398dfd 100644 --- a/ui/packages/consul-ui/app/components/peerings/badge/index.hbs +++ b/ui/packages/consul-ui/app/components/peerings/badge/index.hbs @@ -1,5 +1,5 @@ {{#if @peering.State}} -
    +
    {{capitalize (lowercase @peering.State)}}
    diff --git a/ui/packages/consul-ui/app/components/peerings/badge/index.js b/ui/packages/consul-ui/app/components/peerings/badge/index.js index 7fd211d4d..c9aa9468d 100644 --- a/ui/packages/consul-ui/app/components/peerings/badge/index.js +++ b/ui/packages/consul-ui/app/components/peerings/badge/index.js @@ -20,9 +20,11 @@ const BADGE_LOOKUP = { TERMINATED: { tooltip: 'Someone in the other peer may have deleted this peering connection.', }, - UNDEFINED: {}, + UNDEFINED: { + tooltip: '', + }, }; -export default class PeeingsBadge extends Component { +export default class PeeringsBadge extends Component { get styles() { const { peering: { State }, diff --git a/ui/packages/consul-ui/app/components/pill/index.scss b/ui/packages/consul-ui/app/components/pill/index.scss index d08626db8..c528bd9ff 100644 --- a/ui/packages/consul-ui/app/components/pill/index.scss +++ b/ui/packages/consul-ui/app/components/pill/index.scss @@ -18,6 +18,9 @@ span.policy-node-identity::before { span.policy-service-identity::before { content: 'Service Identity: '; } +%pill::before { + --icon-size: icon-300; +} %pill.leader::before { @extend %with-star-outline-mask, %as-pseudo; } diff --git a/ui/packages/consul-ui/app/components/policy-form/index.js b/ui/packages/consul-ui/app/components/policy-form/index.js index 561918b79..7843184f8 100644 --- a/ui/packages/consul-ui/app/components/policy-form/index.js +++ b/ui/packages/consul-ui/app/components/policy-form/index.js @@ -8,7 +8,7 @@ export default FormComponent.extend({ classNames: ['policy-form'], isScoped: false, - init: function() { + init: function () { this._super(...arguments); set(this, 'isScoped', get(this, 'item.Datacenters.length') > 0); this.templates = [ @@ -27,7 +27,7 @@ export default FormComponent.extend({ ]; }, actions: { - change: function(e) { + change: function (e) { try { this._super(...arguments); } catch (err) { diff --git a/ui/packages/consul-ui/app/components/policy-form/pageobject.js b/ui/packages/consul-ui/app/components/policy-form/pageobject.js index 7937dc1a7..12f7977e4 100644 --- a/ui/packages/consul-ui/app/components/policy-form/pageobject.js +++ b/ui/packages/consul-ui/app/components/policy-form/pageobject.js @@ -1,16 +1,15 @@ -export default (submitable, cancelable, radiogroup, text) => ( - scope = '[data-test-policy-form]' -) => { - return { - // this should probably be settable - resetScope: true, - scope: scope, - prefix: 'policy', - ...submitable(), - ...cancelable(), - ...radiogroup('template', ['', 'service-identity', 'node-identity'], 'policy'), - rules: { - error: text('[data-test-rules] strong'), - }, +export default (submitable, cancelable, radiogroup, text) => + (scope = '[data-test-policy-form]') => { + return { + // this should probably be settable + resetScope: true, + scope: scope, + prefix: 'policy', + ...submitable(), + ...cancelable(), + ...radiogroup('template', ['', 'service-identity', 'node-identity'], 'policy'), + rules: { + error: text('[data-test-rules] strong'), + }, + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/policy-selector/index.hbs b/ui/packages/consul-ui/app/components/policy-selector/index.hbs index 0a1bb618b..5ca0d27a6 100644 --- a/ui/packages/consul-ui/app/components/policy-selector/index.hbs +++ b/ui/packages/consul-ui/app/components/policy-selector/index.hbs @@ -20,7 +20,7 @@ diff --git a/ui/packages/consul-ui/app/components/policy-selector/index.js b/ui/packages/consul-ui/app/components/policy-selector/index.js index 2c20eb1d9..e7aaad491 100644 --- a/ui/packages/consul-ui/app/components/policy-selector/index.js +++ b/ui/packages/consul-ui/app/components/policy-selector/index.js @@ -12,26 +12,26 @@ export default ChildSelectorComponent.extend({ type: 'policy', allowIdentity: true, classNames: ['policy-selector'], - init: function() { + init: function () { this._super(...arguments); const source = this.source; if (source) { this._listeners.add(source, { - save: e => { + save: (e) => { this.save.perform(...e.data); }, }); } }, - reset: function(e) { + reset: function (e) { this._super(...arguments); set(this, 'isScoped', false); }, - refreshCodeEditor: function(e, target) { + refreshCodeEditor: function (e, target) { const selector = '.code-editor'; this.dom.component(selector, target).didAppear(); }, - error: function(e) { + error: function (e) { const item = this.item; const err = e.error; if (typeof err.errors !== 'undefined') { @@ -57,8 +57,15 @@ export default ChildSelectorComponent.extend({ throw err; } }, + openModal: function () { + const { modal } = this; + + if (modal) { + modal.open(); + } + }, actions: { - open: function(e) { + open: function (e) { this.refreshCodeEditor(e, e.target.parentElement); }, }, diff --git a/ui/packages/consul-ui/app/components/policy-selector/pageobject.js b/ui/packages/consul-ui/app/components/policy-selector/pageobject.js index 17c34fccf..55f4a19d8 100644 --- a/ui/packages/consul-ui/app/components/policy-selector/pageobject.js +++ b/ui/packages/consul-ui/app/components/policy-selector/pageobject.js @@ -1,20 +1,18 @@ -export default (clickable, deletable, collection, alias, policyForm) => ( - scope = '#policies', - createSelector = '[data-test-policy-create]' -) => { - return { - scope: scope, - create: clickable(createSelector), - form: policyForm('#new-policy'), - policies: alias('selectedOptions'), - selectedOptions: collection( - '[data-test-policies] [data-test-tabular-row]', - deletable( - { - expand: clickable('label'), - }, - '+ tr' - ) - ), +export default (clickable, deletable, collection, alias, policyForm) => + (scope = '#policies', createSelector = '[data-test-policy-create]') => { + return { + scope: scope, + create: clickable(createSelector), + form: policyForm('#new-policy'), + policies: alias('selectedOptions'), + selectedOptions: collection( + '[data-test-policies] [data-test-tabular-row]', + deletable( + { + expand: clickable('label'), + }, + '+ tr' + ) + ), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/popover-menu/index.js b/ui/packages/consul-ui/app/components/popover-menu/index.js index 2229e9f4e..f38eac4f1 100644 --- a/ui/packages/consul-ui/app/components/popover-menu/index.js +++ b/ui/packages/consul-ui/app/components/popover-menu/index.js @@ -9,32 +9,32 @@ export default Component.extend(Slotted, { dom: service('dom'), expanded: false, keyboardAccess: true, - onchange: function() {}, + onchange: function () {}, // TODO: this needs to be made dynamic/auto detect // for now use this to set left/right explicitly position: '', - init: function() { + init: function () { this._super(...arguments); this.guid = this.dom.guid(this); this.submenus = []; }, - willRender: function() { + willRender: function () { set(this, 'hasHeader', this._isRegistered('header')); }, actions: { - addSubmenu: function(name) { + addSubmenu: function (name) { set(this, 'submenus', this.submenus.concat(name)); }, - removeSubmenu: function(name) { + removeSubmenu: function (name) { const pos = this.submenus.indexOf(name); if (pos !== -1) { this.submenus.splice(pos, 1); set(this, 'submenus', this.submenus); } }, - change: function(e) { + change: function (e) { if (!e.target.checked) { - [...this.dom.elements(`[id^=popover-menu-${this.guid}]`)].forEach(function($item) { + [...this.dom.elements(`[id^=popover-menu-${this.guid}]`)].forEach(function ($item) { $item.checked = false; }); } @@ -43,7 +43,7 @@ export default Component.extend(Slotted, { // Temporary send here so we can send route actions // easily. It kind of makes sense that you'll want to perform // route actions from a popup menu for the moment - send: function() { + send: function () { this.sendAction(...arguments); }, }, diff --git a/ui/packages/consul-ui/app/components/popover-menu/menu-item/index.js b/ui/packages/consul-ui/app/components/popover-menu/menu-item/index.js index e98665c1a..69f46191a 100644 --- a/ui/packages/consul-ui/app/components/popover-menu/menu-item/index.js +++ b/ui/packages/consul-ui/app/components/popover-menu/menu-item/index.js @@ -7,19 +7,19 @@ import Slotted from 'block-slots'; export default Component.extend(Slotted, { tagName: '', dom: service('dom'), - init: function() { + init: function () { this._super(...arguments); this.guid = this.dom.guid(this); }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); this.menu.addSubmenu(this.guid); }, - didDestroyElement: function() { + didDestroyElement: function () { this._super(...arguments); this.menu.removeSubmenu(this.guid); }, - willRender: function() { + willRender: function () { this._super(...arguments); set(this, 'hasConfirmation', this._isRegistered('confirmation')); }, diff --git a/ui/packages/consul-ui/app/components/popover-select/index.js b/ui/packages/consul-ui/app/components/popover-select/index.js index cd8bc330e..4a18ea824 100644 --- a/ui/packages/consul-ui/app/components/popover-select/index.js +++ b/ui/packages/consul-ui/app/components/popover-select/index.js @@ -7,31 +7,31 @@ export default Component.extend(Slotted, { dom: service('dom'), multiple: false, required: false, - onchange: function() {}, - addOption: function(option) { + onchange: function () {}, + addOption: function (option) { if (typeof this._options === 'undefined') { this._options = new Set(); } this._options.add(option); }, - removeOption: function(option) { + removeOption: function (option) { this._options.delete(option); }, actions: { - click: function(option, e) { + click: function (option, e) { // required={{true}} ? if (!this.multiple) { if (option.selected && this.required) { return e; } [...this._options] - .filter(item => item !== option) - .forEach(item => { + .filter((item) => item !== option) + .forEach((item) => { item.selected = false; }); } else { if (option.selected && this.required) { - const other = [...this._options].find(item => item !== option && item.selected); + const other = [...this._options].find((item) => item !== option && item.selected); if (!other) { return e; } @@ -40,11 +40,11 @@ export default Component.extend(Slotted, { option.selected = !option.selected; this.onchange( this.dom.setEventTargetProperties(e, { - selected: target => option.args.value, - selectedItems: target => { + selected: (target) => option.args.value, + selectedItems: (target) => { return [...this._options] - .filter(item => item.selected) - .map(item => item.args.value) + .filter((item) => item.selected) + .map((item) => item.args.value) .join(','); }, }) diff --git a/ui/packages/consul-ui/app/components/popover-select/index.scss b/ui/packages/consul-ui/app/components/popover-select/index.scss index 0f9a1b950..90dd0fd27 100644 --- a/ui/packages/consul-ui/app/components/popover-select/index.scss +++ b/ui/packages/consul-ui/app/components/popover-select/index.scss @@ -52,6 +52,7 @@ @extend %with-user-team-mask, %as-pseudo; color: rgb(var(--tone-gray-500)); } +%popover-select .lambda button::before, %popover-select .aws button::before { @extend %with-aws-300; } diff --git a/ui/packages/consul-ui/app/components/popover-select/pageobject.js b/ui/packages/consul-ui/app/components/popover-select/pageobject.js index 8a2161c18..8caced158 100644 --- a/ui/packages/consul-ui/app/components/popover-select/pageobject.js +++ b/ui/packages/consul-ui/app/components/popover-select/pageobject.js @@ -1,9 +1,10 @@ -export default (clickable, collection) => (scope = '.popover-select') => { - return { - scope: scope, - selected: clickable('button'), - options: collection('li[role="none"]', { - button: clickable('button'), - }), +export default (clickable, collection) => + (scope = '.popover-select') => { + return { + scope: scope, + selected: clickable('button'), + options: collection('li[role="none"]', { + button: clickable('button'), + }), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/power-select/pageobject.js b/ui/packages/consul-ui/app/components/power-select/pageobject.js index 5ca14cb5b..5af0fecb4 100644 --- a/ui/packages/consul-ui/app/components/power-select/pageobject.js +++ b/ui/packages/consul-ui/app/components/power-select/pageobject.js @@ -1,6 +1,6 @@ import { clickable, isPresent } from 'ember-cli-page-object'; -export default options => { +export default (options) => { return { present: isPresent('.ember-power-select-trigger'), click: clickable('.ember-power-select-trigger'), diff --git a/ui/packages/consul-ui/app/components/radio-group/index.js b/ui/packages/consul-ui/app/components/radio-group/index.js index 805bc683c..cb246eed0 100644 --- a/ui/packages/consul-ui/app/components/radio-group/index.js +++ b/ui/packages/consul-ui/app/components/radio-group/index.js @@ -6,19 +6,19 @@ export default Component.extend({ tagName: '', keyboardAccess: false, dom: service('dom'), - init: function() { + init: function () { this._super(...arguments); this.name = this.dom.guid(this); }, actions: { - keydown: function(e) { + keydown: function (e) { if (e.keyCode === ENTER) { e.target.dispatchEvent(new MouseEvent('click')); } }, - change: function(e) { + change: function (e) { this.onchange( - this.dom.setEventTargetProperty(e, 'value', value => (value === '' ? undefined : value)) + this.dom.setEventTargetProperty(e, 'value', (value) => (value === '' ? undefined : value)) ); }, }, diff --git a/ui/packages/consul-ui/app/components/radio-group/pageobject.js b/ui/packages/consul-ui/app/components/radio-group/pageobject.js index 016c22e8c..34ac48bfb 100644 --- a/ui/packages/consul-ui/app/components/radio-group/pageobject.js +++ b/ui/packages/consul-ui/app/components/radio-group/pageobject.js @@ -2,14 +2,14 @@ import { is, clickable } from 'ember-cli-page-object'; import ucfirst from 'consul-ui/utils/ucfirst'; // TODO: We no longer need to use name here // remove the arg in all objects -export default function(name, items, blankKey = 'all') { - return items.reduce(function(prev, item, i, arr) { +export default function (name, items, blankKey = 'all') { + return items.reduce(function (prev, item, i, arr) { // if item is empty then it means 'all' // otherwise camelCase based on something-here = somethingHere for the key const key = item === '' ? blankKey - : item.split('-').reduce(function(prev, item, i, arr) { + : item.split('-').reduce(function (prev, item, i, arr) { if (i === 0) { return item; } diff --git a/ui/packages/consul-ui/app/components/ref/index.js b/ui/packages/consul-ui/app/components/ref/index.js index e96f699b6..b68a024f6 100644 --- a/ui/packages/consul-ui/app/components/ref/index.js +++ b/ui/packages/consul-ui/app/components/ref/index.js @@ -3,7 +3,7 @@ import { set } from '@ember/object'; export default Component.extend({ tagName: '', - didReceiveAttrs: function() { + didReceiveAttrs: function () { set(this.target, this.name, this.value); }, }); diff --git a/ui/packages/consul-ui/app/components/role-selector/index.js b/ui/packages/consul-ui/app/components/role-selector/index.js index 07cc6d7b7..2034acf5f 100644 --- a/ui/packages/consul-ui/app/components/role-selector/index.js +++ b/ui/packages/consul-ui/app/components/role-selector/index.js @@ -15,20 +15,20 @@ export default ChildSelectorComponent.extend({ // You have to alias data. // If you just set it, it loses its reference? policy: alias('policyForm.data'), - init: function() { + init: function () { this._super(...arguments); - this.policyForm = this.formContainer.form('policy'); + set(this, 'policyForm', this.formContainer.form('policy')); this.source = new EventSource(); }, actions: { - reset: function(e) { + reset: function (e) { this._super(...arguments); this.policyForm.clear({ Datacenter: this.dc }); }, - dispatch: function(type, data) { + dispatch: function (type, data) { this.source.dispatchEvent({ type: type, data: data }); }, - change: function() { + change: function () { const event = this.dom.normalizeEvent(...arguments); const target = event.target; switch (target.name) { diff --git a/ui/packages/consul-ui/app/components/role-selector/pageobject.js b/ui/packages/consul-ui/app/components/role-selector/pageobject.js index c8d79bf42..9fa3cd3ef 100644 --- a/ui/packages/consul-ui/app/components/role-selector/pageobject.js +++ b/ui/packages/consul-ui/app/components/role-selector/pageobject.js @@ -1,16 +1,14 @@ -export default (clickable, deletable, collection, alias, roleForm) => (scope = '#roles') => { - return { - scope: scope, - create: clickable('[data-test-role-create]'), - form: roleForm(), - roles: alias('selectedOptions'), - selectedOptions: collection( - '[data-test-roles] [data-test-tabular-row]', - { +export default (clickable, deletable, collection, alias, roleForm) => + (scope = '#roles') => { + return { + scope: scope, + create: clickable('[data-test-role-create]'), + form: roleForm(), + roles: alias('selectedOptions'), + selectedOptions: collection('[data-test-roles] [data-test-tabular-row]', { actions: clickable('label > button'), delete: clickable('[data-test-delete]'), confirmDelete: clickable('.informed-action button'), - } - ), + }), + }; }; -}; diff --git a/ui/packages/consul-ui/app/components/route/index.js b/ui/packages/consul-ui/app/components/route/index.js index 32065ca47..8951764f8 100644 --- a/ui/packages/consul-ui/app/components/route/index.js +++ b/ui/packages/consul-ui/app/components/route/index.js @@ -14,7 +14,7 @@ export default class RouteComponent extends Component { constructor() { super(...arguments); - this.intlKey = this.encoder.createRegExpEncoder(templateRe, _ => _); + this.intlKey = this.encoder.createRegExpEncoder(templateRe, (_) => _); } get params() { @@ -27,7 +27,10 @@ export default class RouteComponent extends Component { } if (this.args.name) { const outlet = this.routlet.outletFor(this.args.name); - return this.routlet.modelFor(outlet.name); + + if (outlet) { + return this.routlet.modelFor(outlet.name); + } } return undefined; } diff --git a/ui/packages/consul-ui/app/components/search-bar/utils.js b/ui/packages/consul-ui/app/components/search-bar/utils.js index d56a6580c..75a14a318 100644 --- a/ui/packages/consul-ui/app/components/search-bar/utils.js +++ b/ui/packages/consul-ui/app/components/search-bar/utils.js @@ -1,5 +1,5 @@ export const diff = (a, b) => { - return a.filter(item => !b.includes(item)); + return a.filter((item) => !b.includes(item)); }; /** * filters accepts the args.filter @attribute which is shaped like @@ -11,7 +11,7 @@ export const diff = (a, b) => { * There is more explanation in the unit tests for this function so thats worthwhile * checking if you are in amongst this */ -export const filters = filters => { +export const filters = (filters) => { return Object.entries(filters) .filter(([key, value]) => { if (key === 'searchproperty') { @@ -21,7 +21,7 @@ export const filters = filters => { }) .reduce((prev, [key, value]) => { return prev.concat( - value.value.map(item => { + value.value.map((item) => { const obj = { key: key, value: item, diff --git a/ui/packages/consul-ui/app/components/shadow-host/index.js b/ui/packages/consul-ui/app/components/shadow-host/index.js index c5eb1046e..d13e34c19 100644 --- a/ui/packages/consul-ui/app/components/shadow-host/index.js +++ b/ui/packages/consul-ui/app/components/shadow-host/index.js @@ -3,12 +3,10 @@ import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class ShadowHostComponent extends Component { - @tracked shadowRoot; @action attachShadow($element) { this.shadowRoot = $element.attachShadow({ mode: 'open' }); } - } diff --git a/ui/packages/consul-ui/app/components/state-chart/action/index.js b/ui/packages/consul-ui/app/components/state-chart/action/index.js index 2e22f6f04..c416586bc 100644 --- a/ui/packages/consul-ui/app/components/state-chart/action/index.js +++ b/ui/packages/consul-ui/app/components/state-chart/action/index.js @@ -2,11 +2,11 @@ import Component from '@ember/component'; export default Component.extend({ tagName: '', - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); this.chart.addAction(this.name, (context, event) => this.exec(context, event)); }, - willDestroy: function() { + willDestroy: function () { this._super(...arguments); this.chart.removeAction(this.type); }, diff --git a/ui/packages/consul-ui/app/components/state-chart/guard/index.js b/ui/packages/consul-ui/app/components/state-chart/guard/index.js index e5f650029..b4c27b845 100644 --- a/ui/packages/consul-ui/app/components/state-chart/guard/index.js +++ b/ui/packages/consul-ui/app/components/state-chart/guard/index.js @@ -2,10 +2,10 @@ import Component from '@ember/component'; export default Component.extend({ tagName: '', - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); const component = this; - this.chart.addGuard(this.name, function() { + this.chart.addGuard(this.name, function () { if (typeof component.cond === 'function') { return component.cond(...arguments); } else { @@ -13,7 +13,7 @@ export default Component.extend({ } }); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this.chart.removeGuard(this.name); }, diff --git a/ui/packages/consul-ui/app/components/state-chart/index.js b/ui/packages/consul-ui/app/components/state-chart/index.js index ea6cf8a7b..f14cdb67b 100644 --- a/ui/packages/consul-ui/app/components/state-chart/index.js +++ b/ui/packages/consul-ui/app/components/state-chart/index.js @@ -5,13 +5,13 @@ import { set } from '@ember/object'; export default Component.extend({ chart: service('state'), tagName: '', - ontransition: function(e) {}, - init: function() { + ontransition: function (e) {}, + init: function () { this._super(...arguments); this._actions = {}; this._guards = {}; }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { if (typeof this.machine !== 'undefined') { this.machine.stop(); } @@ -19,11 +19,11 @@ export default Component.extend({ this.src.initial = this.initial; } this.machine = this.chart.interpret(this.src, { - onTransition: state => { + onTransition: (state) => { const e = new CustomEvent('transition', { detail: state }); this.ontransition(e); if (!e.defaultPrevented) { - state.actions.forEach(item => { + state.actions.forEach((item) => { const action = this._actions[item.type]; if (typeof action === 'function') { this._actions[item.type](item.type, state.context, state.event); @@ -37,35 +37,35 @@ export default Component.extend({ }, }); }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); // xstate has initialState xstate/fsm has state set(this, 'state', this.machine.initialState || this.machine.state); // set(this, 'state', this.machine.initialState); this.machine.start(); }, - willDestroy: function() { + willDestroy: function () { this._super(...arguments); this.machine.stop(); }, - addAction: function(name, value) { + addAction: function (name, value) { this._actions[name] = value; }, - removeAction: function(name) { + removeAction: function (name) { delete this._actions[name]; }, - addGuard: function(name, value) { + addGuard: function (name, value) { this._guards[name] = value; }, - removeGuard: function(name) { + removeGuard: function (name) { delete this._guards[name]; }, - dispatch: function(eventName, payload) { + dispatch: function (eventName, payload) { this.machine.state.context = payload; this.machine.send({ type: eventName }); }, actions: { - dispatch: function(eventName, e) { + dispatch: function (eventName, e) { if (e && e.preventDefault) { if (typeof e.target.nodeName === 'undefined' || e.target.nodeName.toLowerCase() !== 'a') { e.preventDefault(); diff --git a/ui/packages/consul-ui/app/components/state-machine/index.js b/ui/packages/consul-ui/app/components/state-machine/index.js index 97507fb0e..721b54286 100644 --- a/ui/packages/consul-ui/app/components/state-machine/index.js +++ b/ui/packages/consul-ui/app/components/state-machine/index.js @@ -1,4 +1,3 @@ import Component from '../state-chart/index'; export default Component.extend({}); - diff --git a/ui/packages/consul-ui/app/components/state/index.js b/ui/packages/consul-ui/app/components/state/index.js index 3742bbe74..c21d3eec6 100644 --- a/ui/packages/consul-ui/app/components/state/index.js +++ b/ui/packages/consul-ui/app/components/state/index.js @@ -13,8 +13,9 @@ export default class State extends Component { if (typeof state === 'undefined') { return; } - this.render = typeof matches !== 'undefined' ? - this.state.matches(state, matches) : - !this.state.matches(state, notMatches); + this.render = + typeof matches !== 'undefined' + ? this.state.matches(state, matches) + : !this.state.matches(state, notMatches); } } diff --git a/ui/packages/consul-ui/app/components/tab-nav/index.hbs b/ui/packages/consul-ui/app/components/tab-nav/index.hbs index fc6da077b..6b8acd765 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/index.hbs +++ b/ui/packages/consul-ui/app/components/tab-nav/index.hbs @@ -31,16 +31,10 @@ as |select name|}} > diff --git a/ui/packages/consul-ui/app/components/tab-nav/index.js b/ui/packages/consul-ui/app/components/tab-nav/index.js index 5a1379423..c2afccbdf 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/index.js +++ b/ui/packages/consul-ui/app/components/tab-nav/index.js @@ -1,3 +1,12 @@ import Component from '@glimmer/component'; -export default class TabNav extends Component {} +function noop() {} +export default class TabNav extends Component { + get onClick() { + return this.args.onclick || noop; + } + + get onTabClicked() { + return this.args.onTabClicked || noop; + } +} diff --git a/ui/packages/consul-ui/app/components/tab-nav/pageobject.js b/ui/packages/consul-ui/app/components/tab-nav/pageobject.js index 19d371f3b..03b40c882 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/pageobject.js +++ b/ui/packages/consul-ui/app/components/tab-nav/pageobject.js @@ -1,13 +1,13 @@ -import { is, clickable, attribute } from 'ember-cli-page-object'; +import { is, clickable, attribute, isVisible } from 'ember-cli-page-object'; import ucfirst from 'consul-ui/utils/ucfirst'; -export default function(name, items, blankKey = 'all') { - return items.reduce(function(prev, item, i, arr) { +export default function (name, items, blankKey = 'all') { + return items.reduce(function (prev, item, i, arr) { // if item is empty then it means 'all' // otherwise camelCase based on something-here = somethingHere for the key const key = item === '' ? blankKey - : item.split('-').reduce(function(prev, item, i, arr) { + : item.split('-').reduce(function (prev, item, i, arr) { if (i === 0) { return item; } @@ -19,6 +19,7 @@ export default function(name, items, blankKey = 'all') { [`${key}IsSelected`]: is('.selected', `[data-test-tab="${name}_${item}"]`), [`${key}Url`]: attribute('href', `[data-test-tab="${name}_${item}"] a`), [key]: clickable(`[data-test-tab="${name}_${item}"] a`), + [`${key}IsVisible`]: isVisible(`[data-test-tab="${name}_${item}"] a`), }, }; }, {}); diff --git a/ui/packages/consul-ui/app/components/tabular-collection/index.js b/ui/packages/consul-ui/app/components/tabular-collection/index.js index a9d71e030..5bb3ff7f2 100644 --- a/ui/packages/consul-ui/app/components/tabular-collection/index.js +++ b/ui/packages/consul-ui/app/components/tabular-collection/index.js @@ -15,13 +15,13 @@ export default CollectionComponent.extend(Slotted, { maxHeight: 500, checked: null, hasCaption: false, - init: function() { + init: function () { this._super(...arguments); this.guid = this.dom.guid(this); // TODO: The row height should auto calculate properly from the CSS const o = this; this['cell-layout'] = new Grid(get(this, 'width'), get(this, 'rowHeight')); - this['cell-layout'].formatItemStyle = function(itemIndex) { + this['cell-layout'].formatItemStyle = function (itemIndex) { let style = formatItemStyle.apply(this, arguments); if (o.checked === itemIndex) { style = `${style};z-index: 1`; @@ -29,12 +29,12 @@ export default CollectionComponent.extend(Slotted, { return style; }; }, - didInsertElement: function() { + didInsertElement: function () { this._super(...arguments); this.$element = this.dom.element(`#${this.guid}`); this.actions.resize.apply(this, [{ target: this.dom.viewport() }]); }, - style: computed('rowHeight', '_items', 'maxRows', 'maxHeight', function() { + style: computed('rowHeight', '_items', 'maxRows', 'maxHeight', function () { const maxRows = get(this, 'rows'); let height = get(this, 'maxHeight'); if (maxRows) { @@ -46,14 +46,14 @@ export default CollectionComponent.extend(Slotted, { height: height, }; }), - willRender: function() { + willRender: function () { this._super(...arguments); set(this, 'hasCaption', this._isRegistered('caption')); set(this, 'hasActions', this._isRegistered('actions')); }, // `ember-collection` bug workaround // https://github.com/emberjs/ember-collection/issues/138 - _needsRevalidate: function() { + _needsRevalidate: function () { if (this.isDestroyed || this.isDestroying) { return; } @@ -64,7 +64,7 @@ export default CollectionComponent.extend(Slotted, { } }, actions: { - resize: function(e) { + resize: function (e) { const $tbody = this.$element; const $appContent = this.dom.element('.app-view'); if ($appContent) { @@ -77,7 +77,7 @@ export default CollectionComponent.extend(Slotted, { // TODO: The row height should auto calculate properly from the CSS this['cell-layout'] = new Grid($appContent.clientWidth, get(this, 'rowHeight')); const o = this; - this['cell-layout'].formatItemStyle = function(itemIndex) { + this['cell-layout'].formatItemStyle = function (itemIndex) { let style = formatItemStyle.apply(this, arguments); if (o.checked === itemIndex) { style = `${style};z-index: 1`; @@ -88,10 +88,10 @@ export default CollectionComponent.extend(Slotted, { this.updateScrollPosition(); } }, - click: function(e) { + click: function (e) { return this.dom.clickFirstAnchor(e); }, - change: function(index, e = {}) { + change: function (index, e = {}) { if (this.$tr) { this.$tr.style.zIndex = null; } diff --git a/ui/packages/consul-ui/app/components/tabular-details/index.js b/ui/packages/consul-ui/app/components/tabular-details/index.js index 91f758d0d..1b2a5aeb9 100644 --- a/ui/packages/consul-ui/app/components/tabular-details/index.js +++ b/ui/packages/consul-ui/app/components/tabular-details/index.js @@ -4,16 +4,16 @@ import Slotted from 'block-slots'; export default Component.extend(Slotted, { dom: service('dom'), - onchange: function() {}, - init: function() { + onchange: function () {}, + init: function () { this._super(...arguments); this.guid = this.dom.guid(this); }, actions: { - click: function(e) { + click: function (e) { this.dom.clickFirstAnchor(e); }, - change: function(item, items, e) { + change: function (item, items, e) { this.onchange(e, item, items); }, }, diff --git a/ui/packages/consul-ui/app/components/toggle-button/index.js b/ui/packages/consul-ui/app/components/toggle-button/index.js index 11f0a7efc..73de491a0 100644 --- a/ui/packages/consul-ui/app/components/toggle-button/index.js +++ b/ui/packages/consul-ui/app/components/toggle-button/index.js @@ -5,19 +5,19 @@ export default Component.extend({ dom: service('dom'), tagName: '', checked: false, - onchange: function() {}, + onchange: function () {}, // TODO: reserved for the moment but we don't need it yet - onblur: function() {}, - init: function() { + onblur: function () {}, + init: function () { this._super(...arguments); this.guid = this.dom.guid(this); this._listeners = this.dom.listeners(); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); this._listeners.remove(); }, - didReceiveAttrs: function() { + didReceiveAttrs: function () { this._super(...arguments); if (this.checked) { this.addClickOutsideListener(); @@ -25,10 +25,10 @@ export default Component.extend({ this._listeners.remove(); } }, - addClickOutsideListener: function() { + addClickOutsideListener: function () { // default onblur event this._listeners.remove(); - this._listeners.add(this.dom.document(), 'click', e => { + this._listeners.add(this.dom.document(), 'click', (e) => { if (this.dom.isOutside(this.label, e.target)) { if (this.dom.isOutside(this.label.nextElementSibling, e.target)) { if (this.input.checked) { @@ -42,7 +42,7 @@ export default Component.extend({ }); }, actions: { - click: function(e) { + click: function (e) { // only preventDefault if the target isn't an external link // TODO: this should be changed for an explicit close if ((e.target.rel || '').indexOf('noopener') === -1) { @@ -56,7 +56,7 @@ export default Component.extend({ } this.actions.change.apply(this, [e]); }, - change: function(e) { + change: function (e) { if (this.input.checked) { this.addClickOutsideListener(); } diff --git a/ui/packages/consul-ui/app/components/token-source/index.js b/ui/packages/consul-ui/app/components/token-source/index.js index 1eb317369..6b88952b0 100644 --- a/ui/packages/consul-ui/app/components/token-source/index.js +++ b/ui/packages/consul-ui/app/components/token-source/index.js @@ -20,7 +20,7 @@ export default class TokenSource extends Component { @action change(e) { - e.data.toJSON = function() { + e.data.toJSON = function () { return { AccessorID: this.AccessorID, // TODO: In the past we've always ignored the SecretID returned @@ -39,9 +39,8 @@ export default class TokenSource extends Component { }; }; // TODO: We should probably put the component into idle state - if(typeof this.args.onchange === 'function') { + if (typeof this.args.onchange === 'function') { this.args.onchange(e); } } - } diff --git a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.js b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.js index 4c7741b72..d0fe1088d 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.js @@ -16,7 +16,7 @@ export default class TopoloyMetricsDownLines extends Component { const view = this.args.view; const lines = [...document.querySelectorAll('#downstream-lines path')]; - this.iconPositions = lines.map(item => { + this.iconPositions = lines.map((item) => { const pathLen = parseFloat(item.getTotalLength()); const thirdLen = item.getPointAtLength(Math.ceil(pathLen / 3)); diff --git a/ui/packages/consul-ui/app/components/topology-metrics/index.js b/ui/packages/consul-ui/app/components/topology-metrics/index.js index 627c2394b..a5696c1ce 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/index.js @@ -24,7 +24,7 @@ export default class TopologyMetrics extends Component { }; return items - .map(item => { + .map((item) => { const dimensions = item.getBoundingClientRect(); const src = { x: dimensions.x + dimensions.width, @@ -51,7 +51,7 @@ export default class TopologyMetrics extends Component { }; return items - .map(item => { + .map((item) => { const dimensions = item.getBoundingClientRect(); const dest = { x: dimensions.x - dimensions.width - 25, @@ -104,7 +104,7 @@ export default class TopologyMetrics extends Component { get upstreams() { const upstreams = get(this.args.topology, 'Upstreams') || []; - upstreams.forEach(u => { + upstreams.forEach((u) => { u.PeerOrDatacenter = u.PeerName || u.Datacenter; }); const items = [...upstreams]; diff --git a/ui/packages/consul-ui/app/components/topology-metrics/series/index.js b/ui/packages/consul-ui/app/components/topology-metrics/series/index.js index 16be89a69..18141adde 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/series/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/series/index.js @@ -24,17 +24,17 @@ export default Component.extend({ data: null, empty: false, actions: { - redraw: function(evt) { + redraw: function (evt) { this.drawGraphs(); }, - change: function(evt) { + change: function (evt) { this.set('data', evt.data.series); this.drawGraphs(); this.rerender(); }, }, - drawGraphs: function() { + drawGraphs: function () { if (!this.data) { set(this, 'empty', true); return; @@ -56,7 +56,7 @@ export default Component.extend({ let series = maybeData.data || []; let labels = maybeData.labels || {}; let unitSuffix = maybeData.unitSuffix || ''; - let keys = Object.keys(labels).filter(l => l != 'Total'); + let keys = Object.keys(labels).filter((l) => l != 'Total'); if (series.length == 0 || keys.length == 0) { // Put the graph in an error state that might get fixed if metrics show up @@ -67,9 +67,7 @@ export default Component.extend({ set(this, 'empty', false); } - let st = stack() - .keys(keys) - .order(stackOrderReverse); + let st = stack().keys(keys).order(stackOrderReverse); let stackData = st(series); @@ -77,16 +75,16 @@ export default Component.extend({ // stackData contains this but I didn't find reliable documentation on // whether we can rely on the highest stacked area to always be first/last // in array etc. so this is simpler. - let summed = series.map(d => { + let summed = series.map((d) => { let sum = 0; - keys.forEach(l => { + keys.forEach((l) => { sum = sum + d[l]; }); return sum; }); let x = scaleTime() - .domain(extent(series, d => d.time)) + .domain(extent(series, (d) => d.time)) .range([0, w]); let y = scaleLinear() @@ -94,9 +92,9 @@ export default Component.extend({ .range([h, 0]); let a = area() - .x(d => x(d.data.time)) - .y1(d => y(d[0])) - .y0(d => y(d[1])); + .x((d) => x(d.data.time)) + .y1((d) => y(d[0])) + .y0((d) => y(d[1])); // Use the grey/red we prefer by default but have more colors available in // case user adds extra series with a custom provider. @@ -136,11 +134,7 @@ export default Component.extend({ .attr('class', 'sparkline-tt-legend-color') .style('background-color', color(k)); - legend - .append('span') - .text(k) - .append('span') - .attr('class', 'sparkline-tt-legend-value'); + legend.append('span').text(k).append('span').attr('class', 'sparkline-tt-legend-value'); } let tipVals = tooltip.selectAll('.sparkline-tt-legend-value'); @@ -158,7 +152,7 @@ export default Component.extend({ let self = this; svg - .on('mouseover', function(e) { + .on('mouseover', function (e) { tooltip.style('visibility', 'visible'); cursor.style('visibility', 'visible'); // We update here since we might redraw the graph with user's cursor @@ -166,26 +160,26 @@ export default Component.extend({ // mousemove but the tooltip and cursor are wrong (based on old data). self.updateTooltip(e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor); }) - .on('mousemove', function(e) { + .on('mousemove', function (e) { self.updateTooltip(e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor); }) - .on('mouseout', function(e) { + .on('mouseout', function (e) { tooltip.style('visibility', 'hidden'); cursor.style('visibility', 'hidden'); }); }, - willDestroyElement: function() { + willDestroyElement: function () { this._super(...arguments); if (typeof this.svg !== 'undefined') { this.svg.on('mouseover mousemove mouseout', null); } }, - updateTooltip: function(e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor) { + updateTooltip: function (e, series, stackData, summed, unitSuffix, x, tooltip, tipVals, cursor) { let [mouseX] = pointer(e); cursor.attr('x', mouseX); let mouseTime = x.invert(mouseX); - var bisectTime = bisector(function(d) { + var bisectTime = bisector(function (d) { return d.time; }).left; let tipIdx = bisectTime(series, mouseTime); diff --git a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js index 51bd4aab0..62073c3fb 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js @@ -17,7 +17,7 @@ export default class TopologyMetricsUpLines extends Component { const view = this.args.view; const lines = [...document.querySelectorAll('#upstream-lines path')]; - this.iconPositions = lines.map(item => { + this.iconPositions = lines.map((item) => { const pathLen = parseFloat(item.getTotalLength()); const partLen = item.getPointAtLength(Math.ceil(pathLen * 0.666)); return { diff --git a/ui/packages/consul-ui/app/controllers/_peered-resource.js b/ui/packages/consul-ui/app/controllers/_peered-resource.js index 1490727b6..edcf50605 100644 --- a/ui/packages/consul-ui/app/controllers/_peered-resource.js +++ b/ui/packages/consul-ui/app/controllers/_peered-resource.js @@ -8,7 +8,7 @@ export default class PeeredResourceController extends Controller { const { searchProperties } = this; if (!this.abilities.can('use peers')) { - return searchProperties.filter(propertyName => propertyName !== 'PeerName'); + return searchProperties.filter((propertyName) => propertyName !== 'PeerName'); } else { return searchProperties; } diff --git a/ui/packages/consul-ui/app/controllers/application.js b/ui/packages/consul-ui/app/controllers/application.js index 14bfc4018..4247d6cc8 100644 --- a/ui/packages/consul-ui/app/controllers/application.js +++ b/ui/packages/consul-ui/app/controllers/application.js @@ -46,13 +46,13 @@ export default class ApplicationController extends Controller { return container .lookup('route:application') .refresh() - .promise.catch(function(e) { + .promise.catch(function (e) { // passthrough // if you are on an error page a refresh of the application route will reject // thats ok as we then transition to the actual route you were trying // to get to originally anyway }) - .then(res => { + .then((res) => { // Use transitionable if we need to change a section of the URL // or routeName and currentRouteName aren't equal (i.e. error page) if ( @@ -68,7 +68,7 @@ export default class ApplicationController extends Controller { }); }, e.type, - function(type, e) { + function (type, e) { return type; }, {} diff --git a/ui/packages/consul-ui/app/controllers/dc/acls/tokens/edit.js b/ui/packages/consul-ui/app/controllers/dc/acls/tokens/edit.js index 4355a2790..32f929cb9 100644 --- a/ui/packages/consul-ui/app/controllers/dc/acls/tokens/edit.js +++ b/ui/packages/consul-ui/app/controllers/dc/acls/tokens/edit.js @@ -4,11 +4,11 @@ export default Controller.extend({ dom: service('dom'), builder: service('form'), isScoped: false, - init: function() { + init: function () { this._super(...arguments); this.form = this.builder.form('token'); }, - setProperties: function(model) { + setProperties: function (model) { // essentially this replaces the data with changesets this._super( Object.keys(model).reduce((prev, key, i) => { @@ -22,7 +22,7 @@ export default Controller.extend({ ); }, actions: { - change: function(e, value, item) { + change: function (e, value, item) { const event = this.dom.normalizeEvent(e, value); const form = this.form; try { diff --git a/ui/packages/consul-ui/app/decorators/data-source.js b/ui/packages/consul-ui/app/decorators/data-source.js index a0b3bf632..628937310 100644 --- a/ui/packages/consul-ui/app/decorators/data-source.js +++ b/ui/packages/consul-ui/app/decorators/data-source.js @@ -3,18 +3,18 @@ import wayfarer from 'wayfarer'; const router = wayfarer(); const routes = {}; -export default path => (target, propertyKey, desc) => { +export default (path) => (target, propertyKey, desc) => { runInDebug(() => { routes[path] = { cls: target, method: propertyKey }; }); - router.on(path, function(params, owner, request) { + router.on(path, function (params, owner, request) { const container = owner.lookup('service:container'); const instance = container.get(target); - return configuration => desc.value.apply(instance, [params, configuration, request]); + return (configuration) => desc.value.apply(instance, [params, configuration, request]); }); return desc; }; -export const match = path => { +export const match = (path) => { return router.match(path); }; @@ -29,13 +29,10 @@ runInDebug(() => {
     ${Object.entries(routes)
       .map(([key, value]) => {
    -    let cls = container
    -      .keyForClass(value.cls)
    -      .split('/')
    -      .pop();
    +    let cls = container.keyForClass(value.cls).split('/').pop();
         cls = cls
           .split('-')
    -      .map(item => `${item[0].toUpperCase()}${item.substr(1)}`)
    +      .map((item) => `${item[0].toUpperCase()}${item.substr(1)}`)
           .join('');
         return `${key}
           ${cls}Repository.${value.method}(params)
    diff --git a/ui/packages/consul-ui/app/decorators/replace.js b/ui/packages/consul-ui/app/decorators/replace.js
    index 671d1dffe..72703b18c 100644
    --- a/ui/packages/consul-ui/app/decorators/replace.js
    +++ b/ui/packages/consul-ui/app/decorators/replace.js
    @@ -4,19 +4,19 @@
      */
     export const replace = (find, replace) => (target, propertyKey, desc) => {
       return {
    -    get: function() {
    +    get: function () {
           const value = desc.get.apply(this, arguments);
           if (value === find) {
             return replace;
           }
           return value;
         },
    -    set: function() {
    +    set: function () {
           return desc.set.apply(this, arguments);
         },
       };
     };
    -export const nullValue = function(val) {
    +export const nullValue = function (val) {
       return replace(null, val);
     };
     export default replace;
    diff --git a/ui/packages/consul-ui/app/forms/intention.js b/ui/packages/consul-ui/app/forms/intention.js
    index 3d4139fd0..dbcba3474 100644
    --- a/ui/packages/consul-ui/app/forms/intention.js
    +++ b/ui/packages/consul-ui/app/forms/intention.js
    @@ -1,6 +1,6 @@
     import validations from 'consul-ui/validations/intention';
     import builderFactory from 'consul-ui/utils/form/builder';
     const builder = builderFactory();
    -export default function(container, name = '', v = validations, form = builder) {
    +export default function (container, name = '', v = validations, form = builder) {
       return form(name, {}).setValidators(v);
     }
    diff --git a/ui/packages/consul-ui/app/forms/kv.js b/ui/packages/consul-ui/app/forms/kv.js
    index a68d5f1b8..729dfd4b7 100644
    --- a/ui/packages/consul-ui/app/forms/kv.js
    +++ b/ui/packages/consul-ui/app/forms/kv.js
    @@ -1,6 +1,6 @@
     import validations from 'consul-ui/validations/kv';
     import builderFactory from 'consul-ui/utils/form/builder';
     const builder = builderFactory();
    -export default function(container, name = '', v = validations, form = builder) {
    +export default function (container, name = '', v = validations, form = builder) {
       return form(name, {}).setValidators(v);
     }
    diff --git a/ui/packages/consul-ui/app/forms/policy.js b/ui/packages/consul-ui/app/forms/policy.js
    index f920c5b56..5023b7d59 100644
    --- a/ui/packages/consul-ui/app/forms/policy.js
    +++ b/ui/packages/consul-ui/app/forms/policy.js
    @@ -1,7 +1,7 @@
     import validations from 'consul-ui/validations/policy';
     import builderFactory from 'consul-ui/utils/form/builder';
     const builder = builderFactory();
    -export default function(container, name = 'policy', v = validations, form = builder) {
    +export default function (container, name = 'policy', v = validations, form = builder) {
       return form(name, {
         Datacenters: {
           type: 'array',
    diff --git a/ui/packages/consul-ui/app/forms/role.js b/ui/packages/consul-ui/app/forms/role.js
    index 54f6f80ef..0550c04c7 100644
    --- a/ui/packages/consul-ui/app/forms/role.js
    +++ b/ui/packages/consul-ui/app/forms/role.js
    @@ -1,8 +1,6 @@
     import validations from 'consul-ui/validations/role';
     import builderFactory from 'consul-ui/utils/form/builder';
     const builder = builderFactory();
    -export default function(container, name = 'role', v = validations, form = builder) {
    -  return form(name, {})
    -    .setValidators(v)
    -    .add(container.form('policy'));
    +export default function (container, name = 'role', v = validations, form = builder) {
    +  return form(name, {}).setValidators(v).add(container.form('policy'));
     }
    diff --git a/ui/packages/consul-ui/app/forms/token.js b/ui/packages/consul-ui/app/forms/token.js
    index 03f3f1d43..2994a397c 100644
    --- a/ui/packages/consul-ui/app/forms/token.js
    +++ b/ui/packages/consul-ui/app/forms/token.js
    @@ -1,9 +1,6 @@
     import validations from 'consul-ui/validations/token';
     import builderFactory from 'consul-ui/utils/form/builder';
     const builder = builderFactory();
    -export default function(container, name = '', v = validations, form = builder) {
    -  return form(name, {})
    -    .setValidators(v)
    -    .add(container.form('policy'))
    -    .add(container.form('role'));
    +export default function (container, name = '', v = validations, form = builder) {
    +  return form(name, {}).setValidators(v).add(container.form('policy')).add(container.form('role'));
     }
    diff --git a/ui/packages/consul-ui/app/helpers/adopt-styles.js b/ui/packages/consul-ui/app/helpers/adopt-styles.js
    index b683633a3..959558dca 100644
    --- a/ui/packages/consul-ui/app/helpers/adopt-styles.js
    +++ b/ui/packages/consul-ui/app/helpers/adopt-styles.js
    @@ -13,7 +13,7 @@ export default class AdoptStylesHelper extends Helper {
           'adopt-styles can only be used to apply styles to ShadowDOM elements',
           $shadow instanceof ShadowRoot
         );
    -    if(!Array.isArray(styles)) {
    +    if (!Array.isArray(styles)) {
           styles = [styles];
         }
         adoptStyles($shadow, styles);
    diff --git a/ui/packages/consul-ui/app/helpers/atob.js b/ui/packages/consul-ui/app/helpers/atob.js
    index 3ca50f2dd..76b49122f 100644
    --- a/ui/packages/consul-ui/app/helpers/atob.js
    +++ b/ui/packages/consul-ui/app/helpers/atob.js
    @@ -1,5 +1,5 @@
     import { helper } from '@ember/component/helper';
     import atob from 'consul-ui/utils/atob';
    -export default helper(function([str = '']) {
    +export default helper(function ([str = '']) {
       return atob(str);
     });
    diff --git a/ui/packages/consul-ui/app/helpers/can.js b/ui/packages/consul-ui/app/helpers/can.js
    deleted file mode 100644
    index 97dc63880..000000000
    --- a/ui/packages/consul-ui/app/helpers/can.js
    +++ /dev/null
    @@ -1,9 +0,0 @@
    -import Helper from 'ember-can/helpers/can';
    -
    -export default class extends Helper {
    -  _addAbilityObserver(ability, propertyName) {
    -    if(!this.isDestroyed && !this.isDestroying) {
    -      super._addAbilityObserver(...arguments);
    -    }
    -  }
    -}
    diff --git a/ui/packages/consul-ui/app/helpers/class-map.js b/ui/packages/consul-ui/app/helpers/class-map.js
    index 41f2664fe..a3034a06e 100644
    --- a/ui/packages/consul-ui/app/helpers/class-map.js
    +++ b/ui/packages/consul-ui/app/helpers/class-map.js
    @@ -7,11 +7,11 @@ import { helper } from '@ember/component/helper';
      * @typedef {([string, boolean] | [string])} classInfo
      * @param {(classInfo | string)[]} entries - An array of 'entry-like' arrays of `classInfo`s to map
      */
    -const classMap = entries => {
    +const classMap = (entries) => {
       const str = entries
         .filter(Boolean)
    -    .filter(entry => (typeof entry === 'string' ? true : entry[entry.length - 1]))
    -    .map(entry => (typeof entry === 'string' ? entry : entry[0]))
    +    .filter((entry) => (typeof entry === 'string' ? true : entry[entry.length - 1]))
    +    .map((entry) => (typeof entry === 'string' ? entry : entry[0]))
         .join(' ');
       return str.length > 0 ? str : undefined;
     };
    diff --git a/ui/packages/consul-ui/app/helpers/css-map.js b/ui/packages/consul-ui/app/helpers/css-map.js
    index 9f5bb0409..da4f89ef7 100644
    --- a/ui/packages/consul-ui/app/helpers/css-map.js
    +++ b/ui/packages/consul-ui/app/helpers/css-map.js
    @@ -8,9 +8,9 @@ import { CSSResult } from '@lit/reactive-element';
      * @typedef {([CSSResult, boolean] | [CSSResult])} cssInfo
      * @param {(cssInfo | string)[]} entries - An array of 'entry-like' arrays of `cssInfo`s to map
      */
    -const cssMap = entries => {
    +const cssMap = (entries) => {
       return entries
    -    .filter(entry => (entry instanceof CSSResult ? true : entry[entry.length - 1]))
    -    .map(entry => (entry instanceof CSSResult ? entry : entry[0]))
    +    .filter((entry) => (entry instanceof CSSResult ? true : entry[entry.length - 1]))
    +    .map((entry) => (entry instanceof CSSResult ? entry : entry[0]));
     };
     export default helper(cssMap);
    diff --git a/ui/packages/consul-ui/app/helpers/document-attrs.js b/ui/packages/consul-ui/app/helpers/document-attrs.js
    index 4f91fe14d..a1ccfc1ac 100644
    --- a/ui/packages/consul-ui/app/helpers/document-attrs.js
    +++ b/ui/packages/consul-ui/app/helpers/document-attrs.js
    @@ -30,7 +30,7 @@ export default class DocumentAttrsHelper extends Helper {
             let map = attrs.get(key);
     
             if (typeof map !== 'undefined') {
    -          [...new Set(value.split(' '))].map(val => map.remove(val, this));
    +          [...new Set(value.split(' '))].map((val) => map.remove(val, this));
             }
           });
         }
    @@ -43,7 +43,7 @@ export default class DocumentAttrsHelper extends Helper {
               values = new MultiMap(Set);
               attrs.set(key, values);
             }
    -        [...new Set(value.split(' '))].map(val => {
    +        [...new Set(value.split(' '))].map((val) => {
               if (values.count(val) === 0) {
                 values.set(val, null);
               }
    @@ -60,7 +60,7 @@ export default class DocumentAttrsHelper extends Helper {
           }
           // go through our list of properties and synchronize the DOM
           // properties with our properties
    -      [...values.keys()].forEach(value => {
    +      [...values.keys()].forEach((value) => {
             if (values.count(value) === 1) {
               switch (type) {
                 case 'class':
    diff --git a/ui/packages/consul-ui/app/helpers/dom-position.js b/ui/packages/consul-ui/app/helpers/dom-position.js
    index d28c95700..39caafc3e 100644
    --- a/ui/packages/consul-ui/app/helpers/dom-position.js
    +++ b/ui/packages/consul-ui/app/helpers/dom-position.js
    @@ -2,7 +2,7 @@ import Helper from '@ember/component/helper';
     
     export default class DomPosition extends Helper {
       compute([target], { from, offset = false }) {
    -    return e => {
    +    return (e) => {
           if (typeof target === 'function') {
             let rect;
             let $el;
    @@ -29,11 +29,9 @@ export default class DomPosition extends Helper {
             // provide and easy way to map coords to CSS props
             const $el = e.target;
             const rect = $el.getBoundingClientRect();
    -        target.forEach(
    -          ([prop, value]) => {
    -            $el.style[value] = `${rect[prop]}px`;
    -          }
    -        );
    +        target.forEach(([prop, value]) => {
    +          $el.style[value] = `${rect[prop]}px`;
    +        });
           }
         };
       }
    diff --git a/ui/packages/consul-ui/app/helpers/flatten-property.js b/ui/packages/consul-ui/app/helpers/flatten-property.js
    index 761785b55..9790dc912 100644
    --- a/ui/packages/consul-ui/app/helpers/flatten-property.js
    +++ b/ui/packages/consul-ui/app/helpers/flatten-property.js
    @@ -3,6 +3,6 @@ import { helper } from '@ember/component/helper';
     export default helper(function flattenProperty([obj, prop], hash) {
       const pages = hash.pages || [];
       pages.push(...obj.pages);
    -  obj.children.forEach(child => flattenProperty([child], { pages: pages }));
    +  obj.children.forEach((child) => flattenProperty([child], { pages: pages }));
       return pages;
     });
    diff --git a/ui/packages/consul-ui/app/helpers/href-to.js b/ui/packages/consul-ui/app/helpers/href-to.js
    index 16ca41601..9c428b827 100644
    --- a/ui/packages/consul-ui/app/helpers/href-to.js
    +++ b/ui/packages/consul-ui/app/helpers/href-to.js
    @@ -12,7 +12,7 @@ import { routes } from 'consul-ui/router';
     
     const isWildcard = wildcard(routes);
     
    -export const hrefTo = function(container, params, hash = {}) {
    +export const hrefTo = function (container, params, hash = {}) {
       // TODO: consider getting this from @service('router')._router which is
       // private but we don't need getOwner, and it ensures setupRouter is called
       // How private is 'router:main'? If its less private maybe stick with it?
    @@ -34,10 +34,7 @@ export const hrefTo = function(container, params, hash = {}) {
         // if the routeName is a wildcard (*) route url encode all of the params
         if (isWildcard(routeName)) {
           _params = _params.map((item, i) => {
    -        return item
    -          .split('/')
    -          .map(encodeURIComponent)
    -          .join('/');
    +        return item.split('/').map(encodeURIComponent).join('/');
           });
         }
         return location.hrefTo(routeName, _params, _hash);
    diff --git a/ui/packages/consul-ui/app/helpers/icons-debug.js b/ui/packages/consul-ui/app/helpers/icons-debug.js
    index a64039cab..30b12c9e2 100644
    --- a/ui/packages/consul-ui/app/helpers/icons-debug.js
    +++ b/ui/packages/consul-ui/app/helpers/icons-debug.js
    @@ -1,1057 +1,1058 @@
     import { helper } from '@ember/component/helper';
     
    -export default helper(function([lib], hash) {
    +export default helper(function ([lib], hash) {
       return [
         'activity-16.svg',
    -'activity-24.svg',
    -'alert-circle-16.svg',
    -'alert-circle-24.svg',
    -'alert-circle-fill-16.svg',
    -'alert-circle-fill-24.svg',
    -'alert-circle-fill.svg',
    -'alert-circle-outline.svg',
    -'alert-octagon-16.svg',
    -'alert-octagon-24.svg',
    -'alert-octagon-fill-16.svg',
    -'alert-octagon-fill-24.svg',
    -'alert-triangle-16.svg',
    -'alert-triangle-24.svg',
    -'alert-triangle-fill-16.svg',
    -'alert-triangle-fill-24.svg',
    -'alert-triangle.svg',
    -'alibaba-16.svg',
    -'alibaba-24.svg',
    -'alibaba-color-16.svg',
    -'alibaba-color-24.svg',
    -'align-center-16.svg',
    -'align-center-24.svg',
    -'align-justify-16.svg',
    -'align-justify-24.svg',
    -'align-left-16.svg',
    -'align-left-24.svg',
    -'align-right-16.svg',
    -'align-right-24.svg',
    -'apple-16.svg',
    -'apple-24.svg',
    -'apple-color-16.svg',
    -'apple-color-24.svg',
    -'archive-16.svg',
    -'archive-24.svg',
    -'arrow-down-16.svg',
    -'arrow-down-24.svg',
    -'arrow-down-circle-16.svg',
    -'arrow-down-circle-24.svg',
    -'arrow-down-left-16.svg',
    -'arrow-down-left-24.svg',
    -'arrow-down-right-16.svg',
    -'arrow-down-right-24.svg',
    -'arrow-down.svg',
    -'arrow-left-16.svg',
    -'arrow-left-24.svg',
    -'arrow-left-circle-16.svg',
    -'arrow-left-circle-24.svg',
    -'arrow-left.svg',
    -'arrow-right-16.svg',
    -'arrow-right-24.svg',
    -'arrow-right-circle-16.svg',
    -'arrow-right-circle-24.svg',
    -'arrow-right.svg',
    -'arrow-up-16.svg',
    -'arrow-up-24.svg',
    -'arrow-up-circle-16.svg',
    -'arrow-up-circle-24.svg',
    -'arrow-up-left-16.svg',
    -'arrow-up-left-24.svg',
    -'arrow-up-right-16.svg',
    -'arrow-up-right-24.svg',
    -'arrow-up.svg',
    -'at-sign-16.svg',
    -'at-sign-24.svg',
    -'auth0-16.svg',
    -'auth0-24.svg',
    -'auth0-color-16.svg',
    -'auth0-color-24.svg',
    -'auto-apply-16.svg',
    -'auto-apply-24.svg',
    -'award-16.svg',
    -'award-24.svg',
    -'aws-16.svg',
    -'aws-24.svg',
    -'aws-color-16.svg',
    -'aws-color-24.svg',
    -'azure-16.svg',
    -'azure-24.svg',
    -'azure-color-16.svg',
    -'azure-color-24.svg',
    -'azure-devops-16.svg',
    -'azure-devops-24.svg',
    -'azure-devops-color-16.svg',
    -'azure-devops-color-24.svg',
    -'bar-chart-16.svg',
    -'bar-chart-24.svg',
    -'bar-chart-alt-16.svg',
    -'bar-chart-alt-24.svg',
    -'battery-16.svg',
    -'battery-24.svg',
    -'battery-charging-16.svg',
    -'battery-charging-24.svg',
    -'beaker-16.svg',
    -'beaker-24.svg',
    -'bell-16.svg',
    -'bell-24.svg',
    -'bell-active-16.svg',
    -'bell-active-24.svg',
    -'bell-active-fill-16.svg',
    -'bell-active-fill-24.svg',
    -'bell-off-16.svg',
    -'bell-off-24.svg',
    -'bitbucket-16.svg',
    -'bitbucket-24.svg',
    -'bitbucket-color-16.svg',
    -'bitbucket-color-24.svg',
    -'bolt.svg',
    -'bookmark-16.svg',
    -'bookmark-24.svg',
    -'bookmark-add-16.svg',
    -'bookmark-add-24.svg',
    -'bookmark-add-fill-16.svg',
    -'bookmark-add-fill-24.svg',
    -'bookmark-fill-16.svg',
    -'bookmark-fill-24.svg',
    -'bookmark-remove-16.svg',
    -'bookmark-remove-24.svg',
    -'bookmark-remove-fill-16.svg',
    -'bookmark-remove-fill-24.svg',
    -'bottom-16.svg',
    -'bottom-24.svg',
    -'box-16.svg',
    -'box-24.svg',
    -'box-check-fill.svg',
    -'box-outline.svg',
    -'briefcase-16.svg',
    -'briefcase-24.svg',
    -'broadcast.svg',
    -'bug-16.svg',
    -'bug-24.svg',
    -'bug.svg',
    -'build-16.svg',
    -'build-24.svg',
    -'calendar-16.svg',
    -'calendar-24.svg',
    -'calendar.svg',
    -'camera-16.svg',
    -'camera-24.svg',
    -'camera-off-16.svg',
    -'camera-off-24.svg',
    -'cancel-circle-fill.svg',
    -'cancel-circle-outline.svg',
    -'cancel-plain.svg',
    -'cancel-square-fill.svg',
    -'cancel-square-outline.svg',
    -'caret-16.svg',
    -'caret-24.svg',
    -'caret-down.svg',
    -'caret-up.svg',
    -'cast-16.svg',
    -'cast-24.svg',
    -'certificate-16.svg',
    -'certificate-24.svg',
    -'change-16.svg',
    -'change-24.svg',
    -'change-circle-16.svg',
    -'change-circle-24.svg',
    -'change-square-16.svg',
    -'change-square-24.svg',
    -'check-16.svg',
    -'check-24.svg',
    -'check-circle-16.svg',
    -'check-circle-24.svg',
    -'check-circle-fill-16.svg',
    -'check-circle-fill-24.svg',
    -'check-circle-fill.svg',
    -'check-circle-outline.svg',
    -'check-diamond-16.svg',
    -'check-diamond-24.svg',
    -'check-diamond-fill-16.svg',
    -'check-diamond-fill-24.svg',
    -'check-hexagon-16.svg',
    -'check-hexagon-24.svg',
    -'check-hexagon-fill-16.svg',
    -'check-hexagon-fill-24.svg',
    -'check-plain.svg',
    -'check-square-16.svg',
    -'check-square-24.svg',
    -'check-square-fill-16.svg',
    -'check-square-fill-24.svg',
    -'chevron-down-16.svg',
    -'chevron-down-24.svg',
    -'chevron-down.svg',
    -'chevron-left-16.svg',
    -'chevron-left-24.svg',
    -'chevron-left.svg',
    -'chevron-right-16.svg',
    -'chevron-right-24.svg',
    -'chevron-right.svg',
    -'chevron-up-16.svg',
    -'chevron-up-24.svg',
    -'chevron-up.svg',
    -'chevrons-down-16.svg',
    -'chevrons-down-24.svg',
    -'chevrons-left-16.svg',
    -'chevrons-left-24.svg',
    -'chevrons-right-16.svg',
    -'chevrons-right-24.svg',
    -'chevrons-up-16.svg',
    -'chevrons-up-24.svg',
    -'circle-16.svg',
    -'circle-24.svg',
    -'circle-dot-16.svg',
    -'circle-dot-24.svg',
    -'circle-fill-16.svg',
    -'circle-fill-24.svg',
    -'circle-half-16.svg',
    -'circle-half-24.svg',
    -'clipboard-16.svg',
    -'clipboard-24.svg',
    -'clipboard-checked-16.svg',
    -'clipboard-checked-24.svg',
    -'clipboard-copy-16.svg',
    -'clipboard-copy-24.svg',
    -'clock-16.svg',
    -'clock-24.svg',
    -'clock-fill.svg',
    -'clock-outline.svg',
    -'cloud-16.svg',
    -'cloud-24.svg',
    -'cloud-check-16.svg',
    -'cloud-check-24.svg',
    -'cloud-cross.svg',
    -'cloud-download-16.svg',
    -'cloud-download-24.svg',
    -'cloud-lightning-16.svg',
    -'cloud-lightning-24.svg',
    -'cloud-lock-16.svg',
    -'cloud-lock-24.svg',
    -'cloud-off-16.svg',
    -'cloud-off-24.svg',
    -'cloud-upload-16.svg',
    -'cloud-upload-24.svg',
    -'cloud-x-16.svg',
    -'cloud-x-24.svg',
    -'code-16.svg',
    -'code-24.svg',
    -'code.svg',
    -'collections-16.svg',
    -'collections-24.svg',
    -'command-16.svg',
    -'command-24.svg',
    -'compass-16.svg',
    -'compass-24.svg',
    -'connection-16.svg',
    -'connection-24.svg',
    -'connection-gateway-16.svg',
    -'connection-gateway-24.svg',
    -'console.svg',
    -'copy-action.svg',
    -'copy-success.svg',
    -'corner-down-left-16.svg',
    -'corner-down-left-24.svg',
    -'corner-down-right-16.svg',
    -'corner-down-right-24.svg',
    -'corner-left-down-16.svg',
    -'corner-left-down-24.svg',
    -'corner-left-up-16.svg',
    -'corner-left-up-24.svg',
    -'corner-right-down-16.svg',
    -'corner-right-down-24.svg',
    -'corner-right-up-16.svg',
    -'corner-right-up-24.svg',
    -'corner-up-left-16.svg',
    -'corner-up-left-24.svg',
    -'corner-up-right-16.svg',
    -'corner-up-right-24.svg',
    -'cpu-16.svg',
    -'cpu-24.svg',
    -'credit-card-16.svg',
    -'credit-card-24.svg',
    -'crop-16.svg',
    -'crop-24.svg',
    -'crosshair-16.svg',
    -'crosshair-24.svg',
    -'dashboard-16.svg',
    -'dashboard-24.svg',
    -'database-16.svg',
    -'database-24.svg',
    -'database.svg',
    -'delay-16.svg',
    -'delay-24.svg',
    -'delay.svg',
    -'delete-16.svg',
    -'delete-24.svg',
    -'deny-alt.svg',
    -'deny-color.svg',
    -'deny-default.svg',
    -'diamond-16.svg',
    -'diamond-24.svg',
    -'diamond-fill-16.svg',
    -'diamond-fill-24.svg',
    -'disabled.svg',
    -'disc-16.svg',
    -'disc-24.svg',
    -'discussion-circle-16.svg',
    -'discussion-circle-24.svg',
    -'discussion-square-16.svg',
    -'discussion-square-24.svg',
    -'docker-16.svg',
    -'docker-24.svg',
    -'docker-color-16.svg',
    -'docker-color-24.svg',
    -'docs-16.svg',
    -'docs-24.svg',
    -'docs-download-16.svg',
    -'docs-download-24.svg',
    -'docs-link-16.svg',
    -'docs-link-24.svg',
    -'docs.svg',
    -'dollar-sign-16.svg',
    -'dollar-sign-24.svg',
    -'dot-16.svg',
    -'dot-24.svg',
    -'dot-half-16.svg',
    -'dot-half-24.svg',
    -'download-16.svg',
    -'download-24.svg',
    -'download.svg',
    -'droplet-16.svg',
    -'droplet-24.svg',
    -'duplicate-16.svg',
    -'duplicate-24.svg',
    -'edit-16.svg',
    -'edit-24.svg',
    -'edit.svg',
    -'entry-point-16.svg',
    -'entry-point-24.svg',
    -'envelope-sealed-fill.svg',
    -'envelope-sealed-outline.svg',
    -'envelope-unsealed--outline.svg',
    -'envelope-unsealed-fill.svg',
    -'event-16.svg',
    -'event-24.svg',
    -'exit-point-16.svg',
    -'exit-point-24.svg',
    -'exit.svg',
    -'expand-less.svg',
    -'expand-more.svg',
    -'external-link-16.svg',
    -'external-link-24.svg',
    -'eye-16.svg',
    -'eye-24.svg',
    -'eye-off-16.svg',
    -'eye-off-24.svg',
    -'f5-16.svg',
    -'f5-24.svg',
    -'f5-color-16.svg',
    -'f5-color-24.svg',
    -'fast-forward-16.svg',
    -'fast-forward-24.svg',
    -'file-16.svg',
    -'file-24.svg',
    -'file-change-16.svg',
    -'file-change-24.svg',
    -'file-check-16.svg',
    -'file-check-24.svg',
    -'file-diff-16.svg',
    -'file-diff-24.svg',
    -'file-fill.svg',
    -'file-minus-16.svg',
    -'file-minus-24.svg',
    -'file-outline.svg',
    -'file-plus-16.svg',
    -'file-plus-24.svg',
    -'file-source-16.svg',
    -'file-source-24.svg',
    -'file-text-16.svg',
    -'file-text-24.svg',
    -'file-x-16.svg',
    -'file-x-24.svg',
    -'files-16.svg',
    -'files-24.svg',
    -'film-16.svg',
    -'film-24.svg',
    -'filter-16.svg',
    -'filter-24.svg',
    -'filter-circle-16.svg',
    -'filter-circle-24.svg',
    -'filter-fill-16.svg',
    -'filter-fill-24.svg',
    -'filter.svg',
    -'fingerprint-16.svg',
    -'fingerprint-24.svg',
    -'flag-16.svg',
    -'flag-24.svg',
    -'flag.svg',
    -'folder-16.svg',
    -'folder-24.svg',
    -'folder-fill-16.svg',
    -'folder-fill-24.svg',
    -'folder-fill.svg',
    -'folder-minus-16.svg',
    -'folder-minus-24.svg',
    -'folder-minus-fill-16.svg',
    -'folder-minus-fill-24.svg',
    -'folder-outline.svg',
    -'folder-plus-16.svg',
    -'folder-plus-24.svg',
    -'folder-plus-fill-16.svg',
    -'folder-plus-fill-24.svg',
    -'folder-star-16.svg',
    -'folder-star-24.svg',
    -'folder-users-16.svg',
    -'folder-users-24.svg',
    -'frown-16.svg',
    -'frown-24.svg',
    -'gateway-16.svg',
    -'gateway-24.svg',
    -'gateway.svg',
    -'gcp-16.svg',
    -'gcp-24.svg',
    -'gcp-color-16.svg',
    -'gcp-color-24.svg',
    -'gift-16.svg',
    -'gift-24.svg',
    -'gift-fill.svg',
    -'gift-outline.svg',
    -'git-branch-16.svg',
    -'git-branch-24.svg',
    -'git-branch.svg',
    -'git-commit-16.svg',
    -'git-commit-24.svg',
    -'git-commit.svg',
    -'git-merge-16.svg',
    -'git-merge-24.svg',
    -'git-pull-request-16.svg',
    -'git-pull-request-24.svg',
    -'git-pull-request.svg',
    -'git-repo-16.svg',
    -'git-repo-24.svg',
    -'git-repository.svg',
    -'github-16.svg',
    -'github-24.svg',
    -'github-color-16.svg',
    -'github-color-24.svg',
    -'gitlab-16.svg',
    -'gitlab-24.svg',
    -'gitlab-color-16.svg',
    -'gitlab-color-24.svg',
    -'globe-16.svg',
    -'globe-24.svg',
    -'globe-private-16.svg',
    -'globe-private-24.svg',
    -'google-16.svg',
    -'google-24.svg',
    -'google-color-16.svg',
    -'google-color-24.svg',
    -'grid-16.svg',
    -'grid-24.svg',
    -'grid-alt-16.svg',
    -'grid-alt-24.svg',
    -'guide-16.svg',
    -'guide-24.svg',
    -'guide-link-16.svg',
    -'guide-link-24.svg',
    -'guide.svg',
    -'hammer-16.svg',
    -'hammer-24.svg',
    -'hard-drive-16.svg',
    -'hard-drive-24.svg',
    -'hash-16.svg',
    -'hash-24.svg',
    -'headphones-16.svg',
    -'headphones-24.svg',
    -'health.svg',
    -'heart-16.svg',
    -'heart-24.svg',
    -'heart-fill-16.svg',
    -'heart-fill-24.svg',
    -'heart-off-16.svg',
    -'heart-off-24.svg',
    -'help-16.svg',
    -'help-24.svg',
    -'help-circle-fill.svg',
    -'help-circle-outline.svg',
    -'hexagon-16.svg',
    -'hexagon-24.svg',
    -'hexagon-fill-16.svg',
    -'hexagon-fill-24.svg',
    -'history-16.svg',
    -'history-24.svg',
    -'history.svg',
    -'home-16.svg',
    -'home-24.svg',
    -'hourglass-16.svg',
    -'hourglass-24.svg',
    -'identity-service-16.svg',
    -'identity-service-24.svg',
    -'identity-user-16.svg',
    -'identity-user-24.svg',
    -'image-16.svg',
    -'image-24.svg',
    -'inbox-16.svg',
    -'inbox-24.svg',
    -'info-16.svg',
    -'info-24.svg',
    -'info-circle-fill.svg',
    -'info-circle-outline.svg',
    -'jump-link-16.svg',
    -'jump-link-24.svg',
    -'key-16.svg',
    -'key-24.svg',
    -'key-values-16.svg',
    -'key-values-24.svg',
    -'key.svg',
    -'kubernetes-16.svg',
    -'kubernetes-24.svg',
    -'kubernetes-color-16.svg',
    -'kubernetes-color-24.svg',
    -'labyrinth-16.svg',
    -'labyrinth-24.svg',
    -'layers-16.svg',
    -'layers-24.svg',
    -'layers.svg',
    -'layout-16.svg',
    -'layout-24.svg',
    -'learn-16.svg',
    -'learn-24.svg',
    -'learn-link-16.svg',
    -'learn-link-24.svg',
    -'learn.svg',
    -'line-chart-16.svg',
    -'line-chart-24.svg',
    -'line-chart-up-16.svg',
    -'line-chart-up-24.svg',
    -'link-16.svg',
    -'link-24.svg',
    -'link.svg',
    -'list-16.svg',
    -'list-24.svg',
    -'loading.svg',
    -'lock-16.svg',
    -'lock-24.svg',
    -'lock-closed-fill.svg',
    -'lock-closed-outline.svg',
    -'lock-closed.svg',
    -'lock-disabled.svg',
    -'lock-fill-16.svg',
    -'lock-fill-24.svg',
    -'lock-off-16.svg',
    -'lock-off-24.svg',
    -'lock-open.svg',
    -'logo-alicloud-color.svg',
    -'logo-alicloud-monochrome.svg',
    -'logo-auth0-color.svg',
    -'logo-aws-color.svg',
    -'logo-aws-monochrome.svg',
    -'logo-azure-color.svg',
    -'logo-azure-dev-ops-color.svg',
    -'logo-azure-dev-ops-monochrome.svg',
    -'logo-azure-monochrome.svg',
    -'logo-bitbucket-color.svg',
    -'logo-bitbucket-monochrome.svg',
    -'logo-consul-color.svg',
    -'logo-ember-circle-color.svg',
    -'logo-gcp-color.svg',
    -'logo-gcp-monochrome.svg',
    -'logo-github-color.svg',
    -'logo-github-monochrome.svg',
    -'logo-gitlab-color.svg',
    -'logo-gitlab-monochrome.svg',
    -'logo-glimmer-color.svg',
    -'logo-google-color.svg',
    -'logo-google-monochrome.svg',
    -'logo-hashicorp-color.svg',
    -'logo-jwt-color.svg',
    -'logo-kubernetes-color.svg',
    -'logo-kubernetes-monochrome.svg',
    -'logo-microsoft-color.svg',
    -'logo-nomad-color.svg',
    -'logo-oidc-color.svg',
    -'logo-okta-color.svg',
    -'logo-oracle-color.svg',
    -'logo-oracle-monochrome.svg',
    -'logo-slack-color.svg',
    -'logo-slack-monochrome.svg',
    -'logo-terraform-color.svg',
    -'logo-vault-color.svg',
    -'logo-vmware-color.svg',
    -'logo-vmware-monochrome.svg',
    -'mail-16.svg',
    -'mail-24.svg',
    -'mail-open-16.svg',
    -'mail-open-24.svg',
    -'map-16.svg',
    -'map-24.svg',
    -'map-pin-16.svg',
    -'map-pin-24.svg',
    -'maximize-16.svg',
    -'maximize-24.svg',
    -'maximize-alt-16.svg',
    -'maximize-alt-24.svg',
    -'meh-16.svg',
    -'meh-24.svg',
    -'menu-16.svg',
    -'menu-24.svg',
    -'menu.svg',
    -'mesh-16.svg',
    -'mesh-24.svg',
    -'mesh.svg',
    -'message-circle-16.svg',
    -'message-circle-24.svg',
    -'message-circle-fill-16.svg',
    -'message-circle-fill-24.svg',
    -'message-square-16.svg',
    -'message-square-24.svg',
    -'message-square-fill-16.svg',
    -'message-square-fill-24.svg',
    -'message.svg',
    -'mic-16.svg',
    -'mic-24.svg',
    -'mic-off-16.svg',
    -'mic-off-24.svg',
    -'microsoft-16.svg',
    -'microsoft-24.svg',
    -'microsoft-color-16.svg',
    -'microsoft-color-24.svg',
    -'migrate-16.svg',
    -'migrate-24.svg',
    -'minimize-16.svg',
    -'minimize-24.svg',
    -'minimize-alt-16.svg',
    -'minimize-alt-24.svg',
    -'minus-16.svg',
    -'minus-24.svg',
    -'minus-circle-16.svg',
    -'minus-circle-24.svg',
    -'minus-circle-fill.svg',
    -'minus-circle-outline.svg',
    -'minus-plain.svg',
    -'minus-plus-16.svg',
    -'minus-plus-24.svg',
    -'minus-plus-circle-16.svg',
    -'minus-plus-circle-24.svg',
    -'minus-plus-square-16.svg',
    -'minus-plus-square-24.svg',
    -'minus-square-16.svg',
    -'minus-square-24.svg',
    -'minus-square-fill.svg',
    -'module-16.svg',
    -'module-24.svg',
    -'module.svg',
    -'monitor-16.svg',
    -'monitor-24.svg',
    -'moon-16.svg',
    -'moon-24.svg',
    -'more-horizontal-16.svg',
    -'more-horizontal-24.svg',
    -'more-horizontal.svg',
    -'more-vertical-16.svg',
    -'more-vertical-24.svg',
    -'more-vertical.svg',
    -'mouse-pointer-16.svg',
    -'mouse-pointer-24.svg',
    -'move-16.svg',
    -'move-24.svg',
    -'music-16.svg',
    -'music-24.svg',
    -'navigation-16.svg',
    -'navigation-24.svg',
    -'navigation-alt-16.svg',
    -'navigation-alt-24.svg',
    -'network-16.svg',
    -'network-24.svg',
    -'network-alt-16.svg',
    -'network-alt-24.svg',
    -'newspaper-16.svg',
    -'newspaper-24.svg',
    -'node-16.svg',
    -'node-24.svg',
    -'notification-disabled.svg',
    -'notification-fill.svg',
    -'notification-outline.svg',
    -'octagon-16.svg',
    -'octagon-24.svg',
    -'okta-16.svg',
    -'okta-24.svg',
    -'okta-color-16.svg',
    -'okta-color-24.svg',
    -'oracle-16.svg',
    -'oracle-24.svg',
    -'oracle-color-16.svg',
    -'oracle-color-24.svg',
    -'org-16.svg',
    -'org-24.svg',
    -'outline-16.svg',
    -'outline-24.svg',
    -'outline.svg',
    -'package-16.svg',
    -'package-24.svg',
    -'page-outline.svg',
    -'paperclip-16.svg',
    -'paperclip-24.svg',
    -'partner.svg',
    -'path-16.svg',
    -'path-24.svg',
    -'path.svg',
    -'pause-16.svg',
    -'pause-24.svg',
    -'pause-circle-16.svg',
    -'pause-circle-24.svg',
    -'pen-tool-16.svg',
    -'pen-tool-24.svg',
    -'pencil-tool-16.svg',
    -'pencil-tool-24.svg',
    -'phone-16.svg',
    -'phone-24.svg',
    -'phone-call-16.svg',
    -'phone-call-24.svg',
    -'phone-off-16.svg',
    -'phone-off-24.svg',
    -'pie-chart-16.svg',
    -'pie-chart-24.svg',
    -'pin-16.svg',
    -'pin-24.svg',
    -'play-16.svg',
    -'play-24.svg',
    -'play-circle-16.svg',
    -'play-circle-24.svg',
    -'play-fill.svg',
    -'play-outline.svg',
    -'play-plain.svg',
    -'plus-16.svg',
    -'plus-24.svg',
    -'plus-circle-16.svg',
    -'plus-circle-24.svg',
    -'plus-circle-fill.svg',
    -'plus-circle-outline.svg',
    -'plus-plain.svg',
    -'plus-square-16.svg',
    -'plus-square-24.svg',
    -'plus-square-fill.svg',
    -'port.svg',
    -'power-16.svg',
    -'power-24.svg',
    -'printer-16.svg',
    -'printer-24.svg',
    -'protocol.svg',
    -'provider-16.svg',
    -'provider-24.svg',
    -'provider.svg',
    -'public-default.svg',
    -'public-locked.svg',
    -'queue-16.svg',
    -'queue-24.svg',
    -'queue.svg',
    -'radio-16.svg',
    -'radio-24.svg',
    -'radio-button-checked.svg',
    -'radio-button-unchecked.svg',
    -'random-16.svg',
    -'random-24.svg',
    -'random.svg',
    -'redirect-16.svg',
    -'redirect-24.svg',
    -'redirect.svg',
    -'refresh-alert.svg',
    -'refresh-default.svg',
    -'reload-16.svg',
    -'reload-24.svg',
    -'remix.svg',
    -'repeat-16.svg',
    -'repeat-24.svg',
    -'replication-direct-16.svg',
    -'replication-direct-24.svg',
    -'replication-perf-16.svg',
    -'replication-perf-24.svg',
    -'rewind-16.svg',
    -'rewind-24.svg',
    -'ribbon.svg',
    -'rocket-16.svg',
    -'rocket-24.svg',
    -'rotate-ccw-16.svg',
    -'rotate-ccw-24.svg',
    -'rotate-cw-16.svg',
    -'rotate-cw-24.svg',
    -'rss-16.svg',
    -'rss-24.svg',
    -'run.svg',
    -'save-16.svg',
    -'save-24.svg',
    -'scissors-16.svg',
    -'scissors-24.svg',
    -'search-16.svg',
    -'search-24.svg',
    -'search-color.svg',
    -'search.svg',
    -'send-16.svg',
    -'send-24.svg',
    -'server-16.svg',
    -'server-24.svg',
    -'server-cluster-16.svg',
    -'server-cluster-24.svg',
    -'serverless-16.svg',
    -'serverless-24.svg',
    -'settings-16.svg',
    -'settings-24.svg',
    -'settings.svg',
    -'share-16.svg',
    -'share-24.svg',
    -'shield-16.svg',
    -'shield-24.svg',
    -'shield-alert-16.svg',
    -'shield-alert-24.svg',
    -'shield-check-16.svg',
    -'shield-check-24.svg',
    -'shield-off-16.svg',
    -'shield-off-24.svg',
    -'shield-x-16.svg',
    -'shield-x-24.svg',
    -'shopping-bag-16.svg',
    -'shopping-bag-24.svg',
    -'shopping-cart-16.svg',
    -'shopping-cart-24.svg',
    -'shuffle-16.svg',
    -'shuffle-24.svg',
    -'sidebar-16.svg',
    -'sidebar-24.svg',
    -'sidebar-hide-16.svg',
    -'sidebar-hide-24.svg',
    -'sidebar-show-16.svg',
    -'sidebar-show-24.svg',
    -'sign-in-16.svg',
    -'sign-in-24.svg',
    -'sign-out-16.svg',
    -'sign-out-24.svg',
    -'skip-16.svg',
    -'skip-24.svg',
    -'skip-back-16.svg',
    -'skip-back-24.svg',
    -'skip-forward-16.svg',
    -'skip-forward-24.svg',
    -'slack-16.svg',
    -'slack-24.svg',
    -'slack-color-16.svg',
    -'slack-color-24.svg',
    -'slash-16.svg',
    -'slash-24.svg',
    -'slash-square-16.svg',
    -'slash-square-24.svg',
    -'sliders-16.svg',
    -'sliders-24.svg',
    -'smartphone-16.svg',
    -'smartphone-24.svg',
    -'smile-16.svg',
    -'smile-24.svg',
    -'socket-16.svg',
    -'socket-24.svg',
    -'socket.svg',
    -'sort-asc-16.svg',
    -'sort-asc-24.svg',
    -'sort-desc-16.svg',
    -'sort-desc-24.svg',
    -'sort.svg',
    -'source-file.svg',
    -'speaker-16.svg',
    -'speaker-24.svg',
    -'square-16.svg',
    -'square-24.svg',
    -'square-fill-16.svg',
    -'square-fill-24.svg',
    -'star-16.svg',
    -'star-24.svg',
    -'star-circle-16.svg',
    -'star-circle-24.svg',
    -'star-fill-16.svg',
    -'star-fill-24.svg',
    -'star-fill.svg',
    -'star-off-16.svg',
    -'star-off-24.svg',
    -'star-outline.svg',
    -'stop-circle-16.svg',
    -'stop-circle-24.svg',
    -'sub-left.svg',
    -'sub-right.svg',
    -'sun-16.svg',
    -'sun-24.svg',
    -'support-16.svg',
    -'support-24.svg',
    -'support.svg',
    -'swap-horizontal-16.svg',
    -'swap-horizontal-24.svg',
    -'swap-horizontal.svg',
    -'swap-vertical-16.svg',
    -'swap-vertical-24.svg',
    -'swap-vertical.svg',
    -'switcher-16.svg',
    -'switcher-24.svg',
    -'sync-16.svg',
    -'sync-24.svg',
    -'sync-alert-16.svg',
    -'sync-alert-24.svg',
    -'sync-reverse-16.svg',
    -'sync-reverse-24.svg',
    -'tablet-16.svg',
    -'tablet-24.svg',
    -'tag-16.svg',
    -'tag-24.svg',
    -'tag.svg',
    -'target-16.svg',
    -'target-24.svg',
    -'terminal-16.svg',
    -'terminal-24.svg',
    -'terminal-screen-16.svg',
    -'terminal-screen-24.svg',
    -'thumbs-down-16.svg',
    -'thumbs-down-24.svg',
    -'thumbs-up-16.svg',
    -'thumbs-up-24.svg',
    -'toggle-left-16.svg',
    -'toggle-left-24.svg',
    -'toggle-right-16.svg',
    -'toggle-right-24.svg',
    -'token-16.svg',
    -'token-24.svg',
    -'tools-16.svg',
    -'tools-24.svg',
    -'top-16.svg',
    -'top-24.svg',
    -'trash-16.svg',
    -'trash-24.svg',
    -'trash.svg',
    -'trend-down-16.svg',
    -'trend-down-24.svg',
    -'trend-up-16.svg',
    -'trend-up-24.svg',
    -'triangle-16.svg',
    -'triangle-24.svg',
    -'triangle-fill-16.svg',
    -'triangle-fill-24.svg',
    -'truck-16.svg',
    -'truck-24.svg',
    -'tune.svg',
    -'tv-16.svg',
    -'tv-24.svg',
    -'type-16.svg',
    -'type-24.svg',
    -'unfold-close-16.svg',
    -'unfold-close-24.svg',
    -'unfold-less.svg',
    -'unfold-more.svg',
    -'unfold-open-16.svg',
    -'unfold-open-24.svg',
    -'unlock-16.svg',
    -'unlock-24.svg',
    -'upload-16.svg',
    -'upload-24.svg',
    -'upload.svg',
    -'user-16.svg',
    -'user-24.svg',
    -'user-add.svg',
    -'user-check-16.svg',
    -'user-check-24.svg',
    -'user-circle-16.svg',
    -'user-circle-24.svg',
    -'user-circle-fill-16.svg',
    -'user-circle-fill-24.svg',
    -'user-minus-16.svg',
    -'user-minus-24.svg',
    -'user-organization.svg',
    -'user-plain.svg',
    -'user-plus-16.svg',
    -'user-plus-24.svg',
    -'user-square-fill.svg',
    -'user-square-outline.svg',
    -'user-team.svg',
    -'user-x-16.svg',
    -'user-x-24.svg',
    -'users-16.svg',
    -'users-24.svg',
    -'vault-16.svg',
    -'vault-24.svg',
    -'verified-16.svg',
    -'verified-24.svg',
    -'video-16.svg',
    -'video-24.svg',
    -'video-off-16.svg',
    -'video-off-24.svg',
    -'visibility-hide.svg',
    -'visibility-show.svg',
    -'vmware-16.svg',
    -'vmware-24.svg',
    -'vmware-color-16.svg',
    -'vmware-color-24.svg',
    -'volume-16.svg',
    -'volume-2-16.svg',
    -'volume-2-24.svg',
    -'volume-24.svg',
    -'volume-down-16.svg',
    -'volume-down-24.svg',
    -'volume-x-16.svg',
    -'volume-x-24.svg',
    -'wall-16.svg',
    -'wall-24.svg',
    -'watch-16.svg',
    -'watch-24.svg',
    -'webhook-16.svg',
    -'webhook-24.svg',
    -'webhook.svg',
    -'wifi-16.svg',
    -'wifi-24.svg',
    -'wifi-off-16.svg',
    -'wifi-off-24.svg',
    -'wrench-16.svg',
    -'wrench-24.svg',
    -'x-16.svg',
    -'x-24.svg',
    -'x-circle-16.svg',
    -'x-circle-24.svg',
    -'x-circle-fill-16.svg',
    -'x-circle-fill-24.svg',
    -'x-diamond-16.svg',
    -'x-diamond-24.svg',
    -'x-diamond-fill-16.svg',
    -'x-diamond-fill-24.svg',
    -'x-hexagon-16.svg',
    -'x-hexagon-24.svg',
    -'x-hexagon-fill-16.svg',
    -'x-hexagon-fill-24.svg',
    -'x-square-16.svg',
    -'x-square-24.svg',
    -'x-square-fill-16.svg',
    -'x-square-fill-24.svg',
    -'zap-16.svg',
    -'zap-24.svg',
    -'zap-off-16.svg',
    -'zap-off-24.svg',
    -'zoom-in-16.svg',
    -'zoom-in-24.svg',
    -'zoom-out-16.svg',
    -'zoom-out-24.svg'
    -
    -  ].filter(item => {
    -    const sized = item.endsWith('-16.svg') || item.endsWith('-24.svg');
    -    return lib === 'structure' ? !sized : sized;
    -  }).map(item => {
    -    const temp = item.split('.');
    -    temp.pop();
    -    const name = temp.join('.');
    -    let placeholder = `with-${name}`;
    -      if(name.indexOf('-color') !== -1) {
    +    'activity-24.svg',
    +    'alert-circle-16.svg',
    +    'alert-circle-24.svg',
    +    'alert-circle-fill-16.svg',
    +    'alert-circle-fill-24.svg',
    +    'alert-circle-fill.svg',
    +    'alert-circle-outline.svg',
    +    'alert-octagon-16.svg',
    +    'alert-octagon-24.svg',
    +    'alert-octagon-fill-16.svg',
    +    'alert-octagon-fill-24.svg',
    +    'alert-triangle-16.svg',
    +    'alert-triangle-24.svg',
    +    'alert-triangle-fill-16.svg',
    +    'alert-triangle-fill-24.svg',
    +    'alert-triangle.svg',
    +    'alibaba-16.svg',
    +    'alibaba-24.svg',
    +    'alibaba-color-16.svg',
    +    'alibaba-color-24.svg',
    +    'align-center-16.svg',
    +    'align-center-24.svg',
    +    'align-justify-16.svg',
    +    'align-justify-24.svg',
    +    'align-left-16.svg',
    +    'align-left-24.svg',
    +    'align-right-16.svg',
    +    'align-right-24.svg',
    +    'apple-16.svg',
    +    'apple-24.svg',
    +    'apple-color-16.svg',
    +    'apple-color-24.svg',
    +    'archive-16.svg',
    +    'archive-24.svg',
    +    'arrow-down-16.svg',
    +    'arrow-down-24.svg',
    +    'arrow-down-circle-16.svg',
    +    'arrow-down-circle-24.svg',
    +    'arrow-down-left-16.svg',
    +    'arrow-down-left-24.svg',
    +    'arrow-down-right-16.svg',
    +    'arrow-down-right-24.svg',
    +    'arrow-down.svg',
    +    'arrow-left-16.svg',
    +    'arrow-left-24.svg',
    +    'arrow-left-circle-16.svg',
    +    'arrow-left-circle-24.svg',
    +    'arrow-left.svg',
    +    'arrow-right-16.svg',
    +    'arrow-right-24.svg',
    +    'arrow-right-circle-16.svg',
    +    'arrow-right-circle-24.svg',
    +    'arrow-right.svg',
    +    'arrow-up-16.svg',
    +    'arrow-up-24.svg',
    +    'arrow-up-circle-16.svg',
    +    'arrow-up-circle-24.svg',
    +    'arrow-up-left-16.svg',
    +    'arrow-up-left-24.svg',
    +    'arrow-up-right-16.svg',
    +    'arrow-up-right-24.svg',
    +    'arrow-up.svg',
    +    'at-sign-16.svg',
    +    'at-sign-24.svg',
    +    'auth0-16.svg',
    +    'auth0-24.svg',
    +    'auth0-color-16.svg',
    +    'auth0-color-24.svg',
    +    'auto-apply-16.svg',
    +    'auto-apply-24.svg',
    +    'award-16.svg',
    +    'award-24.svg',
    +    'aws-16.svg',
    +    'aws-24.svg',
    +    'aws-color-16.svg',
    +    'aws-color-24.svg',
    +    'azure-16.svg',
    +    'azure-24.svg',
    +    'azure-color-16.svg',
    +    'azure-color-24.svg',
    +    'azure-devops-16.svg',
    +    'azure-devops-24.svg',
    +    'azure-devops-color-16.svg',
    +    'azure-devops-color-24.svg',
    +    'bar-chart-16.svg',
    +    'bar-chart-24.svg',
    +    'bar-chart-alt-16.svg',
    +    'bar-chart-alt-24.svg',
    +    'battery-16.svg',
    +    'battery-24.svg',
    +    'battery-charging-16.svg',
    +    'battery-charging-24.svg',
    +    'beaker-16.svg',
    +    'beaker-24.svg',
    +    'bell-16.svg',
    +    'bell-24.svg',
    +    'bell-active-16.svg',
    +    'bell-active-24.svg',
    +    'bell-active-fill-16.svg',
    +    'bell-active-fill-24.svg',
    +    'bell-off-16.svg',
    +    'bell-off-24.svg',
    +    'bitbucket-16.svg',
    +    'bitbucket-24.svg',
    +    'bitbucket-color-16.svg',
    +    'bitbucket-color-24.svg',
    +    'bolt.svg',
    +    'bookmark-16.svg',
    +    'bookmark-24.svg',
    +    'bookmark-add-16.svg',
    +    'bookmark-add-24.svg',
    +    'bookmark-add-fill-16.svg',
    +    'bookmark-add-fill-24.svg',
    +    'bookmark-fill-16.svg',
    +    'bookmark-fill-24.svg',
    +    'bookmark-remove-16.svg',
    +    'bookmark-remove-24.svg',
    +    'bookmark-remove-fill-16.svg',
    +    'bookmark-remove-fill-24.svg',
    +    'bottom-16.svg',
    +    'bottom-24.svg',
    +    'box-16.svg',
    +    'box-24.svg',
    +    'box-check-fill.svg',
    +    'box-outline.svg',
    +    'briefcase-16.svg',
    +    'briefcase-24.svg',
    +    'broadcast.svg',
    +    'bug-16.svg',
    +    'bug-24.svg',
    +    'bug.svg',
    +    'build-16.svg',
    +    'build-24.svg',
    +    'calendar-16.svg',
    +    'calendar-24.svg',
    +    'calendar.svg',
    +    'camera-16.svg',
    +    'camera-24.svg',
    +    'camera-off-16.svg',
    +    'camera-off-24.svg',
    +    'cancel-circle-fill.svg',
    +    'cancel-circle-outline.svg',
    +    'cancel-plain.svg',
    +    'cancel-square-fill.svg',
    +    'cancel-square-outline.svg',
    +    'caret-16.svg',
    +    'caret-24.svg',
    +    'caret-down.svg',
    +    'caret-up.svg',
    +    'cast-16.svg',
    +    'cast-24.svg',
    +    'certificate-16.svg',
    +    'certificate-24.svg',
    +    'change-16.svg',
    +    'change-24.svg',
    +    'change-circle-16.svg',
    +    'change-circle-24.svg',
    +    'change-square-16.svg',
    +    'change-square-24.svg',
    +    'check-16.svg',
    +    'check-24.svg',
    +    'check-circle-16.svg',
    +    'check-circle-24.svg',
    +    'check-circle-fill-16.svg',
    +    'check-circle-fill-24.svg',
    +    'check-circle-fill.svg',
    +    'check-circle-outline.svg',
    +    'check-diamond-16.svg',
    +    'check-diamond-24.svg',
    +    'check-diamond-fill-16.svg',
    +    'check-diamond-fill-24.svg',
    +    'check-hexagon-16.svg',
    +    'check-hexagon-24.svg',
    +    'check-hexagon-fill-16.svg',
    +    'check-hexagon-fill-24.svg',
    +    'check-plain.svg',
    +    'check-square-16.svg',
    +    'check-square-24.svg',
    +    'check-square-fill-16.svg',
    +    'check-square-fill-24.svg',
    +    'chevron-down-16.svg',
    +    'chevron-down-24.svg',
    +    'chevron-down.svg',
    +    'chevron-left-16.svg',
    +    'chevron-left-24.svg',
    +    'chevron-left.svg',
    +    'chevron-right-16.svg',
    +    'chevron-right-24.svg',
    +    'chevron-right.svg',
    +    'chevron-up-16.svg',
    +    'chevron-up-24.svg',
    +    'chevron-up.svg',
    +    'chevrons-down-16.svg',
    +    'chevrons-down-24.svg',
    +    'chevrons-left-16.svg',
    +    'chevrons-left-24.svg',
    +    'chevrons-right-16.svg',
    +    'chevrons-right-24.svg',
    +    'chevrons-up-16.svg',
    +    'chevrons-up-24.svg',
    +    'circle-16.svg',
    +    'circle-24.svg',
    +    'circle-dot-16.svg',
    +    'circle-dot-24.svg',
    +    'circle-fill-16.svg',
    +    'circle-fill-24.svg',
    +    'circle-half-16.svg',
    +    'circle-half-24.svg',
    +    'clipboard-16.svg',
    +    'clipboard-24.svg',
    +    'clipboard-checked-16.svg',
    +    'clipboard-checked-24.svg',
    +    'clipboard-copy-16.svg',
    +    'clipboard-copy-24.svg',
    +    'clock-16.svg',
    +    'clock-24.svg',
    +    'clock-fill.svg',
    +    'clock-outline.svg',
    +    'cloud-16.svg',
    +    'cloud-24.svg',
    +    'cloud-check-16.svg',
    +    'cloud-check-24.svg',
    +    'cloud-cross.svg',
    +    'cloud-download-16.svg',
    +    'cloud-download-24.svg',
    +    'cloud-lightning-16.svg',
    +    'cloud-lightning-24.svg',
    +    'cloud-lock-16.svg',
    +    'cloud-lock-24.svg',
    +    'cloud-off-16.svg',
    +    'cloud-off-24.svg',
    +    'cloud-upload-16.svg',
    +    'cloud-upload-24.svg',
    +    'cloud-x-16.svg',
    +    'cloud-x-24.svg',
    +    'code-16.svg',
    +    'code-24.svg',
    +    'code.svg',
    +    'collections-16.svg',
    +    'collections-24.svg',
    +    'command-16.svg',
    +    'command-24.svg',
    +    'compass-16.svg',
    +    'compass-24.svg',
    +    'connection-16.svg',
    +    'connection-24.svg',
    +    'connection-gateway-16.svg',
    +    'connection-gateway-24.svg',
    +    'console.svg',
    +    'copy-action.svg',
    +    'copy-success.svg',
    +    'corner-down-left-16.svg',
    +    'corner-down-left-24.svg',
    +    'corner-down-right-16.svg',
    +    'corner-down-right-24.svg',
    +    'corner-left-down-16.svg',
    +    'corner-left-down-24.svg',
    +    'corner-left-up-16.svg',
    +    'corner-left-up-24.svg',
    +    'corner-right-down-16.svg',
    +    'corner-right-down-24.svg',
    +    'corner-right-up-16.svg',
    +    'corner-right-up-24.svg',
    +    'corner-up-left-16.svg',
    +    'corner-up-left-24.svg',
    +    'corner-up-right-16.svg',
    +    'corner-up-right-24.svg',
    +    'cpu-16.svg',
    +    'cpu-24.svg',
    +    'credit-card-16.svg',
    +    'credit-card-24.svg',
    +    'crop-16.svg',
    +    'crop-24.svg',
    +    'crosshair-16.svg',
    +    'crosshair-24.svg',
    +    'dashboard-16.svg',
    +    'dashboard-24.svg',
    +    'database-16.svg',
    +    'database-24.svg',
    +    'database.svg',
    +    'delay-16.svg',
    +    'delay-24.svg',
    +    'delay.svg',
    +    'delete-16.svg',
    +    'delete-24.svg',
    +    'deny-alt.svg',
    +    'deny-color.svg',
    +    'deny-default.svg',
    +    'diamond-16.svg',
    +    'diamond-24.svg',
    +    'diamond-fill-16.svg',
    +    'diamond-fill-24.svg',
    +    'disabled.svg',
    +    'disc-16.svg',
    +    'disc-24.svg',
    +    'discussion-circle-16.svg',
    +    'discussion-circle-24.svg',
    +    'discussion-square-16.svg',
    +    'discussion-square-24.svg',
    +    'docker-16.svg',
    +    'docker-24.svg',
    +    'docker-color-16.svg',
    +    'docker-color-24.svg',
    +    'docs-16.svg',
    +    'docs-24.svg',
    +    'docs-download-16.svg',
    +    'docs-download-24.svg',
    +    'docs-link-16.svg',
    +    'docs-link-24.svg',
    +    'docs.svg',
    +    'dollar-sign-16.svg',
    +    'dollar-sign-24.svg',
    +    'dot-16.svg',
    +    'dot-24.svg',
    +    'dot-half-16.svg',
    +    'dot-half-24.svg',
    +    'download-16.svg',
    +    'download-24.svg',
    +    'download.svg',
    +    'droplet-16.svg',
    +    'droplet-24.svg',
    +    'duplicate-16.svg',
    +    'duplicate-24.svg',
    +    'edit-16.svg',
    +    'edit-24.svg',
    +    'edit.svg',
    +    'entry-point-16.svg',
    +    'entry-point-24.svg',
    +    'envelope-sealed-fill.svg',
    +    'envelope-sealed-outline.svg',
    +    'envelope-unsealed--outline.svg',
    +    'envelope-unsealed-fill.svg',
    +    'event-16.svg',
    +    'event-24.svg',
    +    'exit-point-16.svg',
    +    'exit-point-24.svg',
    +    'exit.svg',
    +    'expand-less.svg',
    +    'expand-more.svg',
    +    'external-link-16.svg',
    +    'external-link-24.svg',
    +    'eye-16.svg',
    +    'eye-24.svg',
    +    'eye-off-16.svg',
    +    'eye-off-24.svg',
    +    'f5-16.svg',
    +    'f5-24.svg',
    +    'f5-color-16.svg',
    +    'f5-color-24.svg',
    +    'fast-forward-16.svg',
    +    'fast-forward-24.svg',
    +    'file-16.svg',
    +    'file-24.svg',
    +    'file-change-16.svg',
    +    'file-change-24.svg',
    +    'file-check-16.svg',
    +    'file-check-24.svg',
    +    'file-diff-16.svg',
    +    'file-diff-24.svg',
    +    'file-fill.svg',
    +    'file-minus-16.svg',
    +    'file-minus-24.svg',
    +    'file-outline.svg',
    +    'file-plus-16.svg',
    +    'file-plus-24.svg',
    +    'file-source-16.svg',
    +    'file-source-24.svg',
    +    'file-text-16.svg',
    +    'file-text-24.svg',
    +    'file-x-16.svg',
    +    'file-x-24.svg',
    +    'files-16.svg',
    +    'files-24.svg',
    +    'film-16.svg',
    +    'film-24.svg',
    +    'filter-16.svg',
    +    'filter-24.svg',
    +    'filter-circle-16.svg',
    +    'filter-circle-24.svg',
    +    'filter-fill-16.svg',
    +    'filter-fill-24.svg',
    +    'filter.svg',
    +    'fingerprint-16.svg',
    +    'fingerprint-24.svg',
    +    'flag-16.svg',
    +    'flag-24.svg',
    +    'flag.svg',
    +    'folder-16.svg',
    +    'folder-24.svg',
    +    'folder-fill-16.svg',
    +    'folder-fill-24.svg',
    +    'folder-fill.svg',
    +    'folder-minus-16.svg',
    +    'folder-minus-24.svg',
    +    'folder-minus-fill-16.svg',
    +    'folder-minus-fill-24.svg',
    +    'folder-outline.svg',
    +    'folder-plus-16.svg',
    +    'folder-plus-24.svg',
    +    'folder-plus-fill-16.svg',
    +    'folder-plus-fill-24.svg',
    +    'folder-star-16.svg',
    +    'folder-star-24.svg',
    +    'folder-users-16.svg',
    +    'folder-users-24.svg',
    +    'frown-16.svg',
    +    'frown-24.svg',
    +    'gateway-16.svg',
    +    'gateway-24.svg',
    +    'gateway.svg',
    +    'gcp-16.svg',
    +    'gcp-24.svg',
    +    'gcp-color-16.svg',
    +    'gcp-color-24.svg',
    +    'gift-16.svg',
    +    'gift-24.svg',
    +    'gift-fill.svg',
    +    'gift-outline.svg',
    +    'git-branch-16.svg',
    +    'git-branch-24.svg',
    +    'git-branch.svg',
    +    'git-commit-16.svg',
    +    'git-commit-24.svg',
    +    'git-commit.svg',
    +    'git-merge-16.svg',
    +    'git-merge-24.svg',
    +    'git-pull-request-16.svg',
    +    'git-pull-request-24.svg',
    +    'git-pull-request.svg',
    +    'git-repo-16.svg',
    +    'git-repo-24.svg',
    +    'git-repository.svg',
    +    'github-16.svg',
    +    'github-24.svg',
    +    'github-color-16.svg',
    +    'github-color-24.svg',
    +    'gitlab-16.svg',
    +    'gitlab-24.svg',
    +    'gitlab-color-16.svg',
    +    'gitlab-color-24.svg',
    +    'globe-16.svg',
    +    'globe-24.svg',
    +    'globe-private-16.svg',
    +    'globe-private-24.svg',
    +    'google-16.svg',
    +    'google-24.svg',
    +    'google-color-16.svg',
    +    'google-color-24.svg',
    +    'grid-16.svg',
    +    'grid-24.svg',
    +    'grid-alt-16.svg',
    +    'grid-alt-24.svg',
    +    'guide-16.svg',
    +    'guide-24.svg',
    +    'guide-link-16.svg',
    +    'guide-link-24.svg',
    +    'guide.svg',
    +    'hammer-16.svg',
    +    'hammer-24.svg',
    +    'hard-drive-16.svg',
    +    'hard-drive-24.svg',
    +    'hash-16.svg',
    +    'hash-24.svg',
    +    'headphones-16.svg',
    +    'headphones-24.svg',
    +    'health.svg',
    +    'heart-16.svg',
    +    'heart-24.svg',
    +    'heart-fill-16.svg',
    +    'heart-fill-24.svg',
    +    'heart-off-16.svg',
    +    'heart-off-24.svg',
    +    'help-16.svg',
    +    'help-24.svg',
    +    'help-circle-fill.svg',
    +    'help-circle-outline.svg',
    +    'hexagon-16.svg',
    +    'hexagon-24.svg',
    +    'hexagon-fill-16.svg',
    +    'hexagon-fill-24.svg',
    +    'history-16.svg',
    +    'history-24.svg',
    +    'history.svg',
    +    'home-16.svg',
    +    'home-24.svg',
    +    'hourglass-16.svg',
    +    'hourglass-24.svg',
    +    'identity-service-16.svg',
    +    'identity-service-24.svg',
    +    'identity-user-16.svg',
    +    'identity-user-24.svg',
    +    'image-16.svg',
    +    'image-24.svg',
    +    'inbox-16.svg',
    +    'inbox-24.svg',
    +    'info-16.svg',
    +    'info-24.svg',
    +    'info-circle-fill.svg',
    +    'info-circle-outline.svg',
    +    'jump-link-16.svg',
    +    'jump-link-24.svg',
    +    'key-16.svg',
    +    'key-24.svg',
    +    'key-values-16.svg',
    +    'key-values-24.svg',
    +    'key.svg',
    +    'kubernetes-16.svg',
    +    'kubernetes-24.svg',
    +    'kubernetes-color-16.svg',
    +    'kubernetes-color-24.svg',
    +    'labyrinth-16.svg',
    +    'labyrinth-24.svg',
    +    'layers-16.svg',
    +    'layers-24.svg',
    +    'layers.svg',
    +    'layout-16.svg',
    +    'layout-24.svg',
    +    'learn-16.svg',
    +    'learn-24.svg',
    +    'learn-link-16.svg',
    +    'learn-link-24.svg',
    +    'learn.svg',
    +    'line-chart-16.svg',
    +    'line-chart-24.svg',
    +    'line-chart-up-16.svg',
    +    'line-chart-up-24.svg',
    +    'link-16.svg',
    +    'link-24.svg',
    +    'link.svg',
    +    'list-16.svg',
    +    'list-24.svg',
    +    'loading.svg',
    +    'lock-16.svg',
    +    'lock-24.svg',
    +    'lock-closed-fill.svg',
    +    'lock-closed-outline.svg',
    +    'lock-closed.svg',
    +    'lock-disabled.svg',
    +    'lock-fill-16.svg',
    +    'lock-fill-24.svg',
    +    'lock-off-16.svg',
    +    'lock-off-24.svg',
    +    'lock-open.svg',
    +    'logo-alicloud-color.svg',
    +    'logo-alicloud-monochrome.svg',
    +    'logo-auth0-color.svg',
    +    'logo-aws-color.svg',
    +    'logo-aws-monochrome.svg',
    +    'logo-azure-color.svg',
    +    'logo-azure-dev-ops-color.svg',
    +    'logo-azure-dev-ops-monochrome.svg',
    +    'logo-azure-monochrome.svg',
    +    'logo-bitbucket-color.svg',
    +    'logo-bitbucket-monochrome.svg',
    +    'logo-consul-color.svg',
    +    'logo-ember-circle-color.svg',
    +    'logo-gcp-color.svg',
    +    'logo-gcp-monochrome.svg',
    +    'logo-github-color.svg',
    +    'logo-github-monochrome.svg',
    +    'logo-gitlab-color.svg',
    +    'logo-gitlab-monochrome.svg',
    +    'logo-glimmer-color.svg',
    +    'logo-google-color.svg',
    +    'logo-google-monochrome.svg',
    +    'logo-hashicorp-color.svg',
    +    'logo-jwt-color.svg',
    +    'logo-kubernetes-color.svg',
    +    'logo-kubernetes-monochrome.svg',
    +    'logo-microsoft-color.svg',
    +    'logo-nomad-color.svg',
    +    'logo-oidc-color.svg',
    +    'logo-okta-color.svg',
    +    'logo-oracle-color.svg',
    +    'logo-oracle-monochrome.svg',
    +    'logo-slack-color.svg',
    +    'logo-slack-monochrome.svg',
    +    'logo-terraform-color.svg',
    +    'logo-vault-color.svg',
    +    'logo-vmware-color.svg',
    +    'logo-vmware-monochrome.svg',
    +    'mail-16.svg',
    +    'mail-24.svg',
    +    'mail-open-16.svg',
    +    'mail-open-24.svg',
    +    'map-16.svg',
    +    'map-24.svg',
    +    'map-pin-16.svg',
    +    'map-pin-24.svg',
    +    'maximize-16.svg',
    +    'maximize-24.svg',
    +    'maximize-alt-16.svg',
    +    'maximize-alt-24.svg',
    +    'meh-16.svg',
    +    'meh-24.svg',
    +    'menu-16.svg',
    +    'menu-24.svg',
    +    'menu.svg',
    +    'mesh-16.svg',
    +    'mesh-24.svg',
    +    'mesh.svg',
    +    'message-circle-16.svg',
    +    'message-circle-24.svg',
    +    'message-circle-fill-16.svg',
    +    'message-circle-fill-24.svg',
    +    'message-square-16.svg',
    +    'message-square-24.svg',
    +    'message-square-fill-16.svg',
    +    'message-square-fill-24.svg',
    +    'message.svg',
    +    'mic-16.svg',
    +    'mic-24.svg',
    +    'mic-off-16.svg',
    +    'mic-off-24.svg',
    +    'microsoft-16.svg',
    +    'microsoft-24.svg',
    +    'microsoft-color-16.svg',
    +    'microsoft-color-24.svg',
    +    'migrate-16.svg',
    +    'migrate-24.svg',
    +    'minimize-16.svg',
    +    'minimize-24.svg',
    +    'minimize-alt-16.svg',
    +    'minimize-alt-24.svg',
    +    'minus-16.svg',
    +    'minus-24.svg',
    +    'minus-circle-16.svg',
    +    'minus-circle-24.svg',
    +    'minus-circle-fill.svg',
    +    'minus-circle-outline.svg',
    +    'minus-plain.svg',
    +    'minus-plus-16.svg',
    +    'minus-plus-24.svg',
    +    'minus-plus-circle-16.svg',
    +    'minus-plus-circle-24.svg',
    +    'minus-plus-square-16.svg',
    +    'minus-plus-square-24.svg',
    +    'minus-square-16.svg',
    +    'minus-square-24.svg',
    +    'minus-square-fill.svg',
    +    'module-16.svg',
    +    'module-24.svg',
    +    'module.svg',
    +    'monitor-16.svg',
    +    'monitor-24.svg',
    +    'moon-16.svg',
    +    'moon-24.svg',
    +    'more-horizontal-16.svg',
    +    'more-horizontal-24.svg',
    +    'more-horizontal.svg',
    +    'more-vertical-16.svg',
    +    'more-vertical-24.svg',
    +    'more-vertical.svg',
    +    'mouse-pointer-16.svg',
    +    'mouse-pointer-24.svg',
    +    'move-16.svg',
    +    'move-24.svg',
    +    'music-16.svg',
    +    'music-24.svg',
    +    'navigation-16.svg',
    +    'navigation-24.svg',
    +    'navigation-alt-16.svg',
    +    'navigation-alt-24.svg',
    +    'network-16.svg',
    +    'network-24.svg',
    +    'network-alt-16.svg',
    +    'network-alt-24.svg',
    +    'newspaper-16.svg',
    +    'newspaper-24.svg',
    +    'node-16.svg',
    +    'node-24.svg',
    +    'notification-disabled.svg',
    +    'notification-fill.svg',
    +    'notification-outline.svg',
    +    'octagon-16.svg',
    +    'octagon-24.svg',
    +    'okta-16.svg',
    +    'okta-24.svg',
    +    'okta-color-16.svg',
    +    'okta-color-24.svg',
    +    'oracle-16.svg',
    +    'oracle-24.svg',
    +    'oracle-color-16.svg',
    +    'oracle-color-24.svg',
    +    'org-16.svg',
    +    'org-24.svg',
    +    'outline-16.svg',
    +    'outline-24.svg',
    +    'outline.svg',
    +    'package-16.svg',
    +    'package-24.svg',
    +    'page-outline.svg',
    +    'paperclip-16.svg',
    +    'paperclip-24.svg',
    +    'partner.svg',
    +    'path-16.svg',
    +    'path-24.svg',
    +    'path.svg',
    +    'pause-16.svg',
    +    'pause-24.svg',
    +    'pause-circle-16.svg',
    +    'pause-circle-24.svg',
    +    'pen-tool-16.svg',
    +    'pen-tool-24.svg',
    +    'pencil-tool-16.svg',
    +    'pencil-tool-24.svg',
    +    'phone-16.svg',
    +    'phone-24.svg',
    +    'phone-call-16.svg',
    +    'phone-call-24.svg',
    +    'phone-off-16.svg',
    +    'phone-off-24.svg',
    +    'pie-chart-16.svg',
    +    'pie-chart-24.svg',
    +    'pin-16.svg',
    +    'pin-24.svg',
    +    'play-16.svg',
    +    'play-24.svg',
    +    'play-circle-16.svg',
    +    'play-circle-24.svg',
    +    'play-fill.svg',
    +    'play-outline.svg',
    +    'play-plain.svg',
    +    'plus-16.svg',
    +    'plus-24.svg',
    +    'plus-circle-16.svg',
    +    'plus-circle-24.svg',
    +    'plus-circle-fill.svg',
    +    'plus-circle-outline.svg',
    +    'plus-plain.svg',
    +    'plus-square-16.svg',
    +    'plus-square-24.svg',
    +    'plus-square-fill.svg',
    +    'port.svg',
    +    'power-16.svg',
    +    'power-24.svg',
    +    'printer-16.svg',
    +    'printer-24.svg',
    +    'protocol.svg',
    +    'provider-16.svg',
    +    'provider-24.svg',
    +    'provider.svg',
    +    'public-default.svg',
    +    'public-locked.svg',
    +    'queue-16.svg',
    +    'queue-24.svg',
    +    'queue.svg',
    +    'radio-16.svg',
    +    'radio-24.svg',
    +    'radio-button-checked.svg',
    +    'radio-button-unchecked.svg',
    +    'random-16.svg',
    +    'random-24.svg',
    +    'random.svg',
    +    'redirect-16.svg',
    +    'redirect-24.svg',
    +    'redirect.svg',
    +    'refresh-alert.svg',
    +    'refresh-default.svg',
    +    'reload-16.svg',
    +    'reload-24.svg',
    +    'remix.svg',
    +    'repeat-16.svg',
    +    'repeat-24.svg',
    +    'replication-direct-16.svg',
    +    'replication-direct-24.svg',
    +    'replication-perf-16.svg',
    +    'replication-perf-24.svg',
    +    'rewind-16.svg',
    +    'rewind-24.svg',
    +    'ribbon.svg',
    +    'rocket-16.svg',
    +    'rocket-24.svg',
    +    'rotate-ccw-16.svg',
    +    'rotate-ccw-24.svg',
    +    'rotate-cw-16.svg',
    +    'rotate-cw-24.svg',
    +    'rss-16.svg',
    +    'rss-24.svg',
    +    'run.svg',
    +    'save-16.svg',
    +    'save-24.svg',
    +    'scissors-16.svg',
    +    'scissors-24.svg',
    +    'search-16.svg',
    +    'search-24.svg',
    +    'search-color.svg',
    +    'search.svg',
    +    'send-16.svg',
    +    'send-24.svg',
    +    'server-16.svg',
    +    'server-24.svg',
    +    'server-cluster-16.svg',
    +    'server-cluster-24.svg',
    +    'serverless-16.svg',
    +    'serverless-24.svg',
    +    'settings-16.svg',
    +    'settings-24.svg',
    +    'settings.svg',
    +    'share-16.svg',
    +    'share-24.svg',
    +    'shield-16.svg',
    +    'shield-24.svg',
    +    'shield-alert-16.svg',
    +    'shield-alert-24.svg',
    +    'shield-check-16.svg',
    +    'shield-check-24.svg',
    +    'shield-off-16.svg',
    +    'shield-off-24.svg',
    +    'shield-x-16.svg',
    +    'shield-x-24.svg',
    +    'shopping-bag-16.svg',
    +    'shopping-bag-24.svg',
    +    'shopping-cart-16.svg',
    +    'shopping-cart-24.svg',
    +    'shuffle-16.svg',
    +    'shuffle-24.svg',
    +    'sidebar-16.svg',
    +    'sidebar-24.svg',
    +    'sidebar-hide-16.svg',
    +    'sidebar-hide-24.svg',
    +    'sidebar-show-16.svg',
    +    'sidebar-show-24.svg',
    +    'sign-in-16.svg',
    +    'sign-in-24.svg',
    +    'sign-out-16.svg',
    +    'sign-out-24.svg',
    +    'skip-16.svg',
    +    'skip-24.svg',
    +    'skip-back-16.svg',
    +    'skip-back-24.svg',
    +    'skip-forward-16.svg',
    +    'skip-forward-24.svg',
    +    'slack-16.svg',
    +    'slack-24.svg',
    +    'slack-color-16.svg',
    +    'slack-color-24.svg',
    +    'slash-16.svg',
    +    'slash-24.svg',
    +    'slash-square-16.svg',
    +    'slash-square-24.svg',
    +    'sliders-16.svg',
    +    'sliders-24.svg',
    +    'smartphone-16.svg',
    +    'smartphone-24.svg',
    +    'smile-16.svg',
    +    'smile-24.svg',
    +    'socket-16.svg',
    +    'socket-24.svg',
    +    'socket.svg',
    +    'sort-asc-16.svg',
    +    'sort-asc-24.svg',
    +    'sort-desc-16.svg',
    +    'sort-desc-24.svg',
    +    'sort.svg',
    +    'source-file.svg',
    +    'speaker-16.svg',
    +    'speaker-24.svg',
    +    'square-16.svg',
    +    'square-24.svg',
    +    'square-fill-16.svg',
    +    'square-fill-24.svg',
    +    'star-16.svg',
    +    'star-24.svg',
    +    'star-circle-16.svg',
    +    'star-circle-24.svg',
    +    'star-fill-16.svg',
    +    'star-fill-24.svg',
    +    'star-fill.svg',
    +    'star-off-16.svg',
    +    'star-off-24.svg',
    +    'star-outline.svg',
    +    'stop-circle-16.svg',
    +    'stop-circle-24.svg',
    +    'sub-left.svg',
    +    'sub-right.svg',
    +    'sun-16.svg',
    +    'sun-24.svg',
    +    'support-16.svg',
    +    'support-24.svg',
    +    'support.svg',
    +    'swap-horizontal-16.svg',
    +    'swap-horizontal-24.svg',
    +    'swap-horizontal.svg',
    +    'swap-vertical-16.svg',
    +    'swap-vertical-24.svg',
    +    'swap-vertical.svg',
    +    'switcher-16.svg',
    +    'switcher-24.svg',
    +    'sync-16.svg',
    +    'sync-24.svg',
    +    'sync-alert-16.svg',
    +    'sync-alert-24.svg',
    +    'sync-reverse-16.svg',
    +    'sync-reverse-24.svg',
    +    'tablet-16.svg',
    +    'tablet-24.svg',
    +    'tag-16.svg',
    +    'tag-24.svg',
    +    'tag.svg',
    +    'target-16.svg',
    +    'target-24.svg',
    +    'terminal-16.svg',
    +    'terminal-24.svg',
    +    'terminal-screen-16.svg',
    +    'terminal-screen-24.svg',
    +    'thumbs-down-16.svg',
    +    'thumbs-down-24.svg',
    +    'thumbs-up-16.svg',
    +    'thumbs-up-24.svg',
    +    'toggle-left-16.svg',
    +    'toggle-left-24.svg',
    +    'toggle-right-16.svg',
    +    'toggle-right-24.svg',
    +    'token-16.svg',
    +    'token-24.svg',
    +    'tools-16.svg',
    +    'tools-24.svg',
    +    'top-16.svg',
    +    'top-24.svg',
    +    'trash-16.svg',
    +    'trash-24.svg',
    +    'trash.svg',
    +    'trend-down-16.svg',
    +    'trend-down-24.svg',
    +    'trend-up-16.svg',
    +    'trend-up-24.svg',
    +    'triangle-16.svg',
    +    'triangle-24.svg',
    +    'triangle-fill-16.svg',
    +    'triangle-fill-24.svg',
    +    'truck-16.svg',
    +    'truck-24.svg',
    +    'tune.svg',
    +    'tv-16.svg',
    +    'tv-24.svg',
    +    'type-16.svg',
    +    'type-24.svg',
    +    'unfold-close-16.svg',
    +    'unfold-close-24.svg',
    +    'unfold-less.svg',
    +    'unfold-more.svg',
    +    'unfold-open-16.svg',
    +    'unfold-open-24.svg',
    +    'unlock-16.svg',
    +    'unlock-24.svg',
    +    'upload-16.svg',
    +    'upload-24.svg',
    +    'upload.svg',
    +    'user-16.svg',
    +    'user-24.svg',
    +    'user-add.svg',
    +    'user-check-16.svg',
    +    'user-check-24.svg',
    +    'user-circle-16.svg',
    +    'user-circle-24.svg',
    +    'user-circle-fill-16.svg',
    +    'user-circle-fill-24.svg',
    +    'user-minus-16.svg',
    +    'user-minus-24.svg',
    +    'user-organization.svg',
    +    'user-plain.svg',
    +    'user-plus-16.svg',
    +    'user-plus-24.svg',
    +    'user-square-fill.svg',
    +    'user-square-outline.svg',
    +    'user-team.svg',
    +    'user-x-16.svg',
    +    'user-x-24.svg',
    +    'users-16.svg',
    +    'users-24.svg',
    +    'vault-16.svg',
    +    'vault-24.svg',
    +    'verified-16.svg',
    +    'verified-24.svg',
    +    'video-16.svg',
    +    'video-24.svg',
    +    'video-off-16.svg',
    +    'video-off-24.svg',
    +    'visibility-hide.svg',
    +    'visibility-show.svg',
    +    'vmware-16.svg',
    +    'vmware-24.svg',
    +    'vmware-color-16.svg',
    +    'vmware-color-24.svg',
    +    'volume-16.svg',
    +    'volume-2-16.svg',
    +    'volume-2-24.svg',
    +    'volume-24.svg',
    +    'volume-down-16.svg',
    +    'volume-down-24.svg',
    +    'volume-x-16.svg',
    +    'volume-x-24.svg',
    +    'wall-16.svg',
    +    'wall-24.svg',
    +    'watch-16.svg',
    +    'watch-24.svg',
    +    'webhook-16.svg',
    +    'webhook-24.svg',
    +    'webhook.svg',
    +    'wifi-16.svg',
    +    'wifi-24.svg',
    +    'wifi-off-16.svg',
    +    'wifi-off-24.svg',
    +    'wrench-16.svg',
    +    'wrench-24.svg',
    +    'x-16.svg',
    +    'x-24.svg',
    +    'x-circle-16.svg',
    +    'x-circle-24.svg',
    +    'x-circle-fill-16.svg',
    +    'x-circle-fill-24.svg',
    +    'x-diamond-16.svg',
    +    'x-diamond-24.svg',
    +    'x-diamond-fill-16.svg',
    +    'x-diamond-fill-24.svg',
    +    'x-hexagon-16.svg',
    +    'x-hexagon-24.svg',
    +    'x-hexagon-fill-16.svg',
    +    'x-hexagon-fill-24.svg',
    +    'x-square-16.svg',
    +    'x-square-24.svg',
    +    'x-square-fill-16.svg',
    +    'x-square-fill-24.svg',
    +    'zap-16.svg',
    +    'zap-24.svg',
    +    'zap-off-16.svg',
    +    'zap-off-24.svg',
    +    'zoom-in-16.svg',
    +    'zoom-in-24.svg',
    +    'zoom-out-16.svg',
    +    'zoom-out-24.svg',
    +  ]
    +    .filter((item) => {
    +      const sized = item.endsWith('-16.svg') || item.endsWith('-24.svg');
    +      return lib === 'structure' ? !sized : sized;
    +    })
    +    .map((item) => {
    +      const temp = item.split('.');
    +      temp.pop();
    +      const name = temp.join('.');
    +      let placeholder = `with-${name}`;
    +      if (name.indexOf('-color') !== -1) {
             placeholder = `${placeholder}-icon`;
           } else {
             placeholder = `${placeholder}-mask`;
           }
    -    return {
    -      placeholder: placeholder,
    -      name: name
    -    };
    -  })
    +      return {
    +        placeholder: placeholder,
    +        name: name,
    +      };
    +    });
     });
    diff --git a/ui/packages/consul-ui/app/helpers/is.js b/ui/packages/consul-ui/app/helpers/is.js
    index 0e2d9fcf3..2b7fee219 100644
    --- a/ui/packages/consul-ui/app/helpers/is.js
    +++ b/ui/packages/consul-ui/app/helpers/is.js
    @@ -6,17 +6,14 @@ export const is = (helper, [abilityString, model], properties) => {
       let { abilityName, propertyName } = helper.abilities.parse(abilityString);
       let ability = helper.abilities.abilityFor(abilityName, model, properties);
     
    -  if(typeof ability.getCharacteristicProperty === 'function') {
    +  if (typeof ability.getCharacteristicProperty === 'function') {
         propertyName = ability.getCharacteristicProperty(propertyName);
       } else {
         propertyName = camelize(`is-${propertyName}`);
       }
     
    -  helper._removeAbilityObserver();
    -  helper._addAbilityObserver(ability, propertyName);
    -
       return get(ability, propertyName);
    -}
    +};
     export default class extends Helper {
       compute([abilityString, model], properties) {
         return is(this, [abilityString, model], properties);
    diff --git a/ui/packages/consul-ui/app/helpers/json-stringify.js b/ui/packages/consul-ui/app/helpers/json-stringify.js
    index 2eaedcfe8..f60c61ddb 100644
    --- a/ui/packages/consul-ui/app/helpers/json-stringify.js
    +++ b/ui/packages/consul-ui/app/helpers/json-stringify.js
    @@ -1,9 +1,9 @@
     import { helper } from '@ember/component/helper';
     
    -export default helper(function(args, hash) {
    +export default helper(function (args, hash) {
       try {
         return JSON.stringify(...args);
    -  } catch(e) {
    -    return args[0].map(item => JSON.stringify(item, args[1], args[2]));
    +  } catch (e) {
    +    return args[0].map((item) => JSON.stringify(item, args[1], args[2]));
       }
     });
    diff --git a/ui/packages/consul-ui/app/helpers/left-trim.js b/ui/packages/consul-ui/app/helpers/left-trim.js
    index bb49e028f..893e7e2b4 100644
    --- a/ui/packages/consul-ui/app/helpers/left-trim.js
    +++ b/ui/packages/consul-ui/app/helpers/left-trim.js
    @@ -1,6 +1,6 @@
     import { helper } from '@ember/component/helper';
     import leftTrim from 'consul-ui/utils/left-trim';
     
    -export default helper(function([str = '', search = ''], hash) {
    +export default helper(function ([str = '', search = ''], hash) {
       return leftTrim(str, search);
     });
    diff --git a/ui/packages/consul-ui/app/helpers/merge-checks.js b/ui/packages/consul-ui/app/helpers/merge-checks.js
    index ffa764640..6944dc910 100644
    --- a/ui/packages/consul-ui/app/helpers/merge-checks.js
    +++ b/ui/packages/consul-ui/app/helpers/merge-checks.js
    @@ -1,9 +1,6 @@
     import { helper } from '@ember/component/helper';
     import mergeChecks from 'consul-ui/utils/merge-checks';
     
    -export default helper(function([checks, exposed], hash) {
    -  return mergeChecks(
    -    checks,
    -    exposed
    -  );
    +export default helper(function ([checks, exposed], hash) {
    +  return mergeChecks(checks, exposed);
     });
    diff --git a/ui/packages/consul-ui/app/helpers/percentage-of.js b/ui/packages/consul-ui/app/helpers/percentage-of.js
    index 0979455b0..5c5b2182a 100644
    --- a/ui/packages/consul-ui/app/helpers/percentage-of.js
    +++ b/ui/packages/consul-ui/app/helpers/percentage-of.js
    @@ -1,8 +1,8 @@
     import { helper } from '@ember/component/helper';
     
    -export default helper(function([of, num], hash) {
    -  const perc = (of / num * 100);
    -  if(isNaN(perc)) {
    +export default helper(function ([of, num], hash) {
    +  const perc = (of / num) * 100;
    +  if (isNaN(perc)) {
         return 0;
       }
       return perc.toFixed(2);
    diff --git a/ui/packages/consul-ui/app/helpers/policy/group.js b/ui/packages/consul-ui/app/helpers/policy/group.js
    index 1a2c829d0..5802f31bc 100644
    --- a/ui/packages/consul-ui/app/helpers/policy/group.js
    +++ b/ui/packages/consul-ui/app/helpers/policy/group.js
    @@ -4,7 +4,7 @@ import { MANAGEMENT_ID } from 'consul-ui/models/policy';
     
     export default helper(function policyGroup([items] /*, hash*/) {
       return items.reduce(
    -    function(prev, item) {
    +    function (prev, item) {
           let group;
           switch (true) {
             case get(item, 'ID') === MANAGEMENT_ID:
    diff --git a/ui/packages/consul-ui/app/helpers/require.js b/ui/packages/consul-ui/app/helpers/require.js
    index c3b39e869..d62481eac 100644
    --- a/ui/packages/consul-ui/app/helpers/require.js
    +++ b/ui/packages/consul-ui/app/helpers/require.js
    @@ -4,7 +4,6 @@ import require from 'require';
     import { css } from '@lit/reactive-element';
     import resolve from 'consul-ui/utils/path/resolve';
     
    -
     const appName = 'consul-ui';
     
     const container = new Map();
    @@ -14,24 +13,24 @@ const container = new Map();
     // need to
     export default helper(([path = ''], options) => {
       let fullPath = resolve(`${appName}${options.from}`, path);
    -  if(path.charAt(0) === '/') {
    +  if (path.charAt(0) === '/') {
         fullPath = `${appName}${fullPath}`;
       }
     
       let module;
    -  if(require.has(fullPath)) {
    +  if (require.has(fullPath)) {
         module = require(fullPath)[options.export || 'default'];
       } else {
    -    throw new Error(`Unable to resolve '${fullPath}' does the file exist?`)
    +    throw new Error(`Unable to resolve '${fullPath}' does the file exist?`);
       }
     
    -  switch(true) {
    +  switch (true) {
         case fullPath.endsWith('.css'):
           return module(css);
         case fullPath.endsWith('.xstate'):
           return module;
         case fullPath.endsWith('.element'): {
    -      if(container.has(fullPath)) {
    +      if (container.has(fullPath)) {
             return container.get(fullPath);
           }
           const component = module(HTMLElement);
    diff --git a/ui/packages/consul-ui/app/helpers/right-trim.js b/ui/packages/consul-ui/app/helpers/right-trim.js
    index 27c272137..823e6ad13 100644
    --- a/ui/packages/consul-ui/app/helpers/right-trim.js
    +++ b/ui/packages/consul-ui/app/helpers/right-trim.js
    @@ -2,6 +2,6 @@ import { helper } from '@ember/component/helper';
     
     import rightTrim from 'consul-ui/utils/right-trim';
     
    -export default helper(function([str = '', search = ''], hash) {
    +export default helper(function ([str = '', search = ''], hash) {
       return rightTrim(str, search);
     });
    diff --git a/ui/packages/consul-ui/app/helpers/route-match.js b/ui/packages/consul-ui/app/helpers/route-match.js
    index 370546245..665db1e95 100644
    --- a/ui/packages/consul-ui/app/helpers/route-match.js
    +++ b/ui/packages/consul-ui/app/helpers/route-match.js
    @@ -2,7 +2,7 @@ import { helper } from '@ember/component/helper';
     
     export default helper(function routeMatch([item], hash) {
       const prop = ['Present', 'Exact', 'Prefix', 'Suffix', 'Regex'].find(
    -    prop => typeof item[prop] !== 'undefined'
    +    (prop) => typeof item[prop] !== 'undefined'
       );
     
       switch (prop) {
    diff --git a/ui/packages/consul-ui/app/helpers/service/external-source.js b/ui/packages/consul-ui/app/helpers/service/external-source.js
    index c5ab7949b..97124316d 100644
    --- a/ui/packages/consul-ui/app/helpers/service/external-source.js
    +++ b/ui/packages/consul-ui/app/helpers/service/external-source.js
    @@ -9,7 +9,16 @@ export function serviceExternalSource(params, hash) {
       const prefix = typeof hash.prefix === 'undefined' ? '' : hash.prefix;
       if (
         source &&
    -    ['consul-api-gateway', 'vault', 'kubernetes', 'terraform', 'nomad', 'consul', 'aws'].includes(
    +    [
    +      'consul-api-gateway',
    +      'vault',
    +      'kubernetes',
    +      'terraform',
    +      'nomad',
    +      'consul',
    +      'aws',
    +      'lambda'
    +    ].includes(
           source
         )
       ) {
    diff --git a/ui/packages/consul-ui/app/helpers/svg-curve.js b/ui/packages/consul-ui/app/helpers/svg-curve.js
    index 4bdaa68ac..a9a9a2c87 100644
    --- a/ui/packages/consul-ui/app/helpers/svg-curve.js
    +++ b/ui/packages/consul-ui/app/helpers/svg-curve.js
    @@ -1,7 +1,7 @@
     import { helper } from '@ember/component/helper';
     // arguments should be a list of {x: numLike, y: numLike} points
     // numLike meaning they should be numbers (or numberlike strings i.e. "1" vs 1)
    -const curve = function() {
    +const curve = function () {
       const args = [...arguments];
       // our arguments are destination first control points last
       // SVGs are control points first destination last
    @@ -12,16 +12,16 @@ const curve = function() {
       // `Q|C x y, x y, x y` etc
       return `${arguments.length > 2 ? `C` : `Q`} ${args
         .concat(args.shift())
    -    .map(p => Object.values(p).join(' '))
    +    .map((p) => Object.values(p).join(' '))
         .join(',')}`;
     };
    -const move = function(d) {
    +const move = function (d) {
       return `
         M ${d.x} ${d.y}
       `;
     };
     
    -export default helper(function([dest], hash) {
    +export default helper(function ([dest], hash) {
       const src = hash.src || { x: 0, y: 0 };
       const type = hash.type || 'cubic';
       let args = [
    diff --git a/ui/packages/consul-ui/app/helpers/test.js b/ui/packages/consul-ui/app/helpers/test.js
    index e9dc9a00a..a57f137a8 100644
    --- a/ui/packages/consul-ui/app/helpers/test.js
    +++ b/ui/packages/consul-ui/app/helpers/test.js
    @@ -3,7 +3,7 @@ import { is } from './is';
     
     export default class extends Helper {
       compute([abilityString, model], properties) {
    -    switch(true) {
    +    switch (true) {
           case abilityString.startsWith('can '):
             return super.compute([abilityString.substr(4), model], properties);
           case abilityString.startsWith('is '):
    diff --git a/ui/packages/consul-ui/app/helpers/token/is-legacy.js b/ui/packages/consul-ui/app/helpers/token/is-legacy.js
    index 427ad11df..c66bbd7a1 100644
    --- a/ui/packages/consul-ui/app/helpers/token/is-legacy.js
    +++ b/ui/packages/consul-ui/app/helpers/token/is-legacy.js
    @@ -1,7 +1,7 @@
     import { helper } from '@ember/component/helper';
     import { get } from '@ember/object';
     
    -const _isLegacy = function(token) {
    +const _isLegacy = function (token) {
       // Empty Rules take priority over a Legacy: true
       // in order to make this decision
       const rules = get(token, 'Rules');
    @@ -18,7 +18,7 @@ export function isLegacy(params, hash) {
       const token = params[0];
       // is array like (RecordManager isn't an array)
       if (typeof token.length !== 'undefined') {
    -    return token.find(function(item) {
    +    return token.find(function (item) {
           return _isLegacy(item);
         });
       }
    diff --git a/ui/packages/consul-ui/app/instance-initializers/container.js b/ui/packages/consul-ui/app/instance-initializers/container.js
    index ee286c376..c35af601e 100644
    --- a/ui/packages/consul-ui/app/instance-initializers/container.js
    +++ b/ui/packages/consul-ui/app/instance-initializers/container.js
    @@ -5,12 +5,12 @@ import merge from 'deepmerge';
     const doc = document;
     
     export const services = merge.all(
    -  [...doc.querySelectorAll(`script[data-services]`)].map($item =>
    +  [...doc.querySelectorAll(`script[data-services]`)].map(($item) =>
         JSON.parse($item.dataset[`services`])
       )
     );
     
    -const inject = function(container, obj) {
    +const inject = function (container, obj) {
       // inject all the things
       Object.entries(obj).forEach(([key, value]) => {
         switch (true) {
    @@ -42,10 +42,10 @@ export default {
         let repositories = container
           .get('container-debug-adapter:main')
           .catalogEntriesByType('service')
    -      .filter(item => item.startsWith('repository/') || item === 'ui-config');
    +      .filter((item) => item.startsWith('repository/') || item === 'ui-config');
     
         // during testing we get -test files in here, filter those out but only in debug envs
    -    runInDebug(() => (repositories = repositories.filter(item => !item.endsWith('-test'))));
    +    runInDebug(() => (repositories = repositories.filter((item) => !item.endsWith('-test'))));
     
         // 'service' service is not returned by catalogEntriesByType, possibly
         // related to pods and the service being called 'service':
    @@ -53,7 +53,7 @@ export default {
         // so push it on the end
         repositories.push('repository/service');
         //
    -    repositories.forEach(item => {
    +    repositories.forEach((item) => {
           const key = `service:${item}`;
           container.set(key, container.resolveRegistration(key));
         });
    diff --git a/ui/packages/consul-ui/app/instance-initializers/href-to.js b/ui/packages/consul-ui/app/instance-initializers/href-to.js
    index 85f890e76..dbccbe163 100644
    --- a/ui/packages/consul-ui/app/instance-initializers/href-to.js
    +++ b/ui/packages/consul-ui/app/instance-initializers/href-to.js
    @@ -128,7 +128,7 @@ export default {
           const dom = container.lookup('service:dom');
           const doc = dom.document();
     
    -      const listener = e => {
    +      const listener = (e) => {
             const link = e.target.tagName === 'A' ? e.target : closestLink(e.target);
             if (link) {
               const hrefTo = new HrefTo(container, link);
    diff --git a/ui/packages/consul-ui/app/instance-initializers/ivy-codemirror.js b/ui/packages/consul-ui/app/instance-initializers/ivy-codemirror.js
    index 5b23f3987..719a23c64 100644
    --- a/ui/packages/consul-ui/app/instance-initializers/ivy-codemirror.js
    +++ b/ui/packages/consul-ui/app/instance-initializers/ivy-codemirror.js
    @@ -8,7 +8,7 @@ export function initialize(application) {
       );
       // configure syntax highlighting for CodeMirror
       CodeMirror.modeURL = {
    -    replace: function(n, mode) {
    +    replace: function (n, mode) {
           switch (mode.trim()) {
             case 'javascript':
               return fs.get(['codemirror', 'mode', 'javascript', 'javascript.js'].join('/'));
    diff --git a/ui/packages/consul-ui/app/instance-initializers/selection.js b/ui/packages/consul-ui/app/instance-initializers/selection.js
    index faa9e7533..2cf641344 100644
    --- a/ui/packages/consul-ui/app/instance-initializers/selection.js
    +++ b/ui/packages/consul-ui/app/instance-initializers/selection.js
    @@ -1,7 +1,7 @@
     import { env } from 'consul-ui/env';
     
     const SECONDARY_BUTTON = 2;
    -const isSelecting = function(win = window) {
    +const isSelecting = function (win = window) {
       const selection = win.getSelection();
       let selecting = false;
       try {
    @@ -21,10 +21,10 @@ export default {
         const dom = container.lookup('service:dom');
         const doc = dom.document();
         const $html = doc.getElementsByTagName('html')[0];
    -    const findAnchor = function(el) {
    +    const findAnchor = function (el) {
           return el.tagName === 'A' ? el : dom.closest('a', el);
         };
    -    const mousedown = function(e) {
    +    const mousedown = function (e) {
           if ($html.classList.contains('is-debug')) {
             return;
           }
    @@ -44,7 +44,7 @@ export default {
             }
           }
         };
    -    const mouseup = function(e) {
    +    const mouseup = function (e) {
           if ($html.classList.contains('is-debug')) {
             return;
           }
    @@ -61,7 +61,7 @@ export default {
         doc.body.addEventListener('mouseup', mouseup);
     
         container.reopen({
    -      willDestroy: function() {
    +      willDestroy: function () {
             doc.body.removeEventListener('mousedown', mousedown);
             doc.body.removeEventListener('mouseup', mouseup);
             return this._super(...arguments);
    diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js b/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js
    index 6c7e698ec..b2c3e6157 100644
    --- a/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js
    +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional-test.js
    @@ -33,11 +33,11 @@ export default class FSMWithOptionalTestLocation extends FSMWithOptionalLocation
     
         // taken from emberjs/application/instance:visit but cleaned up a little
         // https://github.com/emberjs/ember.js/blob/21bd70c773dcc4bfe4883d7943e8a68d203b5bad/packages/%40ember/application/instance.js#L236-L277
    -    const handleTransitionResolve = async _ => {
    +    const handleTransitionResolve = async (_) => {
           await settled();
    -      return new Promise(resolve => setTimeout(resolve(app), 0));
    +      return new Promise((resolve) => setTimeout(resolve(app), 0));
         };
    -    const handleTransitionReject = error => {
    +    const handleTransitionReject = (error) => {
           if (error.error) {
             throw error.error;
           } else if (error.name === 'TransitionAborted' && router._routerMicrolib.activeTransition) {
    diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional.js b/ui/packages/consul-ui/app/locations/fsm-with-optional.js
    index de2975fe4..12657737b 100644
    --- a/ui/packages/consul-ui/app/locations/fsm-with-optional.js
    +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional.js
    @@ -15,8 +15,8 @@ const trailingSlashRe = /\/$/;
     // see below re: ember double slashes
     // const moreThan1SlashRe = /\/{2,}/g;
     
    -const _uuid = function() {
    -  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    +const _uuid = function () {
    +  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
         const r = (Math.random() * 16) | 0;
         return (c === 'x' ? r : (r & 3) | 8).toString(16);
       });
    @@ -27,7 +27,7 @@ const _uuid = function() {
      * Register a callback to be invoked whenever the browser history changes,
      * including using forward and back buttons.
      */
    -const route = function(e) {
    +const route = function (e) {
       const path = e.state.path;
       const url = this.getURLForTransition(path);
       // Ignore initial page load popstate event in Chrome
    @@ -303,9 +303,11 @@ export default class FSMWithOptionalLocation {
           const temp = url.split('/');
           optional = {
             ...this.optional,
    -        ...(optional || {})
    +        ...(optional || {}),
           };
    -      optional = Object.values(optional).filter(item => Boolean(item)).map(item => item.value || item, []);
    +      optional = Object.values(optional)
    +        .filter((item) => Boolean(item))
    +        .map((item) => item.value || item, []);
           temp.splice(...[1, 0].concat(optional));
           url = temp.join('/');
         }
    diff --git a/ui/packages/consul-ui/app/mixins/policy/as-many.js b/ui/packages/consul-ui/app/mixins/policy/as-many.js
    index 95da1c848..b5620298d 100644
    --- a/ui/packages/consul-ui/app/mixins/policy/as-many.js
    +++ b/ui/packages/consul-ui/app/mixins/policy/as-many.js
    @@ -3,8 +3,8 @@ import { get } from '@ember/object';
     
     import minimizeModel from 'consul-ui/utils/minimizeModel';
     
    -const normalizeIdentities = function(items, template, name, dc) {
    -  return (items || []).map(function(item) {
    +const normalizeIdentities = function (items, template, name, dc) {
    +  return (items || []).map(function (item) {
         const policy = {
           template: template,
           Name: item[name],
    @@ -17,20 +17,20 @@ const normalizeIdentities = function(items, template, name, dc) {
     };
     // Sometimes we get `Policies: null`, make null equal an empty array
     // and add an empty template
    -const normalizePolicies = function(items) {
    -  return (items || []).map(function(item) {
    +const normalizePolicies = function (items) {
    +  return (items || []).map(function (item) {
         return {
           template: '',
           ...item,
         };
       });
     };
    -const serializeIdentities = function(items, template, name, dc) {
    +const serializeIdentities = function (items, template, name, dc) {
       return items
    -    .filter(function(item) {
    +    .filter(function (item) {
           return get(item, 'template') === template;
         })
    -    .map(function(item) {
    +    .map(function (item) {
           const identity = {
             [name]: get(item, 'Name'),
           };
    @@ -40,16 +40,16 @@ const serializeIdentities = function(items, template, name, dc) {
           return identity;
         });
     };
    -const serializePolicies = function(items) {
    -  return items.filter(function(item) {
    +const serializePolicies = function (items) {
    +  return items.filter(function (item) {
         return get(item, 'template') === '';
       });
     };
     
     export default Mixin.create({
       //TODO: what about update and create?
    -  respondForQueryRecord: function(respond, query) {
    -    return this._super(function(cb) {
    +  respondForQueryRecord: function (respond, query) {
    +    return this._super(function (cb) {
           return respond((headers, body) => {
             body.Policies = normalizePolicies(body.Policies)
               .concat(
    @@ -67,12 +67,12 @@ export default Mixin.create({
           });
         }, query);
       },
    -  respondForQuery: function(respond, query) {
    -    return this._super(function(cb) {
    -      return respond(function(headers, body) {
    +  respondForQuery: function (respond, query) {
    +    return this._super(function (cb) {
    +      return respond(function (headers, body) {
             return cb(
               headers,
    -          body.map(function(item) {
    +          body.map(function (item) {
                 item.Policies = normalizePolicies(item.Policies)
                   .concat(
                     normalizeIdentities(
    @@ -91,7 +91,7 @@ export default Mixin.create({
           });
         }, query);
       },
    -  serialize: function(snapshot, options) {
    +  serialize: function (snapshot, options) {
         const data = this._super(...arguments);
         data.ServiceIdentities = serializeIdentities(
           data.Policies,
    diff --git a/ui/packages/consul-ui/app/mixins/role/as-many.js b/ui/packages/consul-ui/app/mixins/role/as-many.js
    index b2e77f143..97b715942 100644
    --- a/ui/packages/consul-ui/app/mixins/role/as-many.js
    +++ b/ui/packages/consul-ui/app/mixins/role/as-many.js
    @@ -4,20 +4,20 @@ import minimizeModel from 'consul-ui/utils/minimizeModel';
     
     export default Mixin.create({
       // TODO: what about update and create?
    -  respondForQueryRecord: function(respond, query) {
    -    return this._super(function(cb) {
    +  respondForQueryRecord: function (respond, query) {
    +    return this._super(function (cb) {
           return respond((headers, body) => {
             body.Roles = typeof body.Roles === 'undefined' || body.Roles === null ? [] : body.Roles;
             return cb(headers, body);
           });
         }, query);
       },
    -  respondForQuery: function(respond, query) {
    -    return this._super(function(cb) {
    -      return respond(function(headers, body) {
    +  respondForQuery: function (respond, query) {
    +    return this._super(function (cb) {
    +      return respond(function (headers, body) {
             return cb(
               headers,
    -          body.map(function(item) {
    +          body.map(function (item) {
                 item.Roles = typeof item.Roles === 'undefined' || item.Roles === null ? [] : item.Roles;
                 return item;
               })
    @@ -25,7 +25,7 @@ export default Mixin.create({
           });
         }, query);
       },
    -  serialize: function(snapshot, options) {
    +  serialize: function (snapshot, options) {
         const data = this._super(...arguments);
         data.Roles = minimizeModel(data.Roles);
         return data;
    diff --git a/ui/packages/consul-ui/app/mixins/with-blocking-actions.js b/ui/packages/consul-ui/app/mixins/with-blocking-actions.js
    index 101d4a0d3..9f48a3a8a 100644
    --- a/ui/packages/consul-ui/app/mixins/with-blocking-actions.js
    +++ b/ui/packages/consul-ui/app/mixins/with-blocking-actions.js
    @@ -21,12 +21,12 @@ import { singularize } from 'ember-inflector';
     export default Mixin.create({
       _feedback: service('feedback'),
       settings: service('settings'),
    -  init: function() {
    +  init: function () {
         this._super(...arguments);
         const feedback = this._feedback;
         const route = this;
         set(this, 'feedback', {
    -      execute: function(cb, type, error) {
    +      execute: function (cb, type, error) {
             const temp = route.routeName.split('.');
             temp.pop();
             const routeName = singularize(temp.pop());
    @@ -35,11 +35,11 @@ export default Mixin.create({
           },
         });
       },
    -  afterCreate: function(item) {
    +  afterCreate: function (item) {
         // do the same as update
         return this.afterUpdate(...arguments);
       },
    -  afterUpdate: function(item) {
    +  afterUpdate: function (item) {
         // e.g. dc.intentions.index
         const parts = this.routeName.split('.');
         // e.g. index or edit
    @@ -47,7 +47,7 @@ export default Mixin.create({
         // e.g. dc.intentions, essentially go to the listings page
         return this.transitionTo(parts.join('.'));
       },
    -  afterDelete: function(item) {
    +  afterDelete: function (item) {
         // e.g. dc.intentions.index
         const parts = this.routeName.split('.');
         // e.g. index or edit
    @@ -61,24 +61,24 @@ export default Mixin.create({
             return this.transitionTo(parts.join('.'));
         }
       },
    -  errorCreate: function(type, e) {
    +  errorCreate: function (type, e) {
         return type;
       },
    -  errorUpdate: function(type, e) {
    +  errorUpdate: function (type, e) {
         return type;
       },
    -  errorDelete: function(type, e) {
    +  errorDelete: function (type, e) {
         return type;
       },
       actions: {
    -    cancel: function() {
    +    cancel: function () {
           // do the same as an update, or override
           return this.afterUpdate(...arguments);
         },
    -    create: function(item) {
    +    create: function (item) {
           return this.feedback.execute(
             () => {
    -          return this.repo.persist(item).then(item => {
    +          return this.repo.persist(item).then((item) => {
                 return this.afterCreate(...arguments);
               });
             },
    @@ -88,7 +88,7 @@ export default Mixin.create({
             }
           );
         },
    -    update: function(item) {
    +    update: function (item) {
           return this.feedback.execute(
             () => {
               return this.repo.persist(item).then(() => {
    @@ -101,7 +101,7 @@ export default Mixin.create({
             }
           );
         },
    -    delete: function(item) {
    +    delete: function (item) {
           return this.feedback.execute(
             () => {
               return this.repo.remove(item).then(() => {
    @@ -114,7 +114,7 @@ export default Mixin.create({
             }
           );
         },
    -    use: function(item) {
    +    use: function (item) {
           return this.repo
             .findBySlug({
               dc: get(item, 'Datacenter'),
    @@ -122,7 +122,7 @@ export default Mixin.create({
               partition: get(item, 'Partition'),
               id: get(item, 'AccessorID'),
             })
    -        .then(item => {
    +        .then((item) => {
               return this.settings.persist({
                 token: {
                   AccessorID: get(item, 'AccessorID'),
    @@ -133,15 +133,15 @@ export default Mixin.create({
               });
             });
         },
    -    logout: function(item) {
    +    logout: function (item) {
           return this.settings.delete('token');
         },
    -    clone: function(item) {
    +    clone: function (item) {
           let cloned;
           return this.feedback.execute(() => {
             return this.repo
               .clone(item)
    -          .then(item => {
    +          .then((item) => {
                 cloned = item;
                 // cloning is similar to delete in that
                 // if you clone from the listing page, stay on the listing page
    @@ -149,7 +149,7 @@ export default Mixin.create({
                 // so I can see it
                 return this.afterDelete(...arguments);
               })
    -          .then(function() {
    +          .then(function () {
                 return cloned;
               });
           }, 'clone');
    diff --git a/ui/packages/consul-ui/app/models/intention-permission-http-header.js b/ui/packages/consul-ui/app/models/intention-permission-http-header.js
    index 8921df935..f98a1b4e3 100644
    --- a/ui/packages/consul-ui/app/models/intention-permission-http-header.js
    +++ b/ui/packages/consul-ui/app/models/intention-permission-http-header.js
    @@ -26,6 +26,6 @@ export default class IntentionPermission extends Fragment {
     
       @computed(...schema.HeaderType.allowedValues)
       get HeaderType() {
    -    return schema.HeaderType.allowedValues.find(prop => typeof this[prop] !== 'undefined');
    +    return schema.HeaderType.allowedValues.find((prop) => typeof this[prop] !== 'undefined');
       }
     }
    diff --git a/ui/packages/consul-ui/app/models/intention-permission-http.js b/ui/packages/consul-ui/app/models/intention-permission-http.js
    index 93860c2e9..21728878b 100644
    --- a/ui/packages/consul-ui/app/models/intention-permission-http.js
    +++ b/ui/packages/consul-ui/app/models/intention-permission-http.js
    @@ -25,6 +25,6 @@ export default class IntentionPermissionHttp extends Fragment {
     
       @computed(...schema.PathType.allowedValues)
       get PathType() {
    -    return schema.PathType.allowedValues.find(prop => typeof this[prop] === 'string');
    +    return schema.PathType.allowedValues.find((prop) => typeof this[prop] === 'string');
       }
     }
    diff --git a/ui/packages/consul-ui/app/models/node.js b/ui/packages/consul-ui/app/models/node.js
    index 27563fbd1..157a2833b 100644
    --- a/ui/packages/consul-ui/app/models/node.js
    +++ b/ui/packages/consul-ui/app/models/node.js
    @@ -28,11 +28,11 @@ export default class Node extends Model {
       @fragmentArray('health-check') Checks;
       // MeshServiceInstances are all instances that aren't connect-proxies this
       // currently includes gateways as these need to show up in listings
    -  @filter('Services', item => item.Service.Kind !== 'connect-proxy') MeshServiceInstances;
    +  @filter('Services', (item) => item.Service.Kind !== 'connect-proxy') MeshServiceInstances;
       // ProxyServiceInstances are all instances that are connect-proxies
    -  @filter('Services', item => item.Service.Kind === 'connect-proxy') ProxyServiceInstances;
    +  @filter('Services', (item) => item.Service.Kind === 'connect-proxy') ProxyServiceInstances;
     
    -  @filter('Checks', item => item.ServiceID === '') NodeChecks;
    +  @filter('Checks', (item) => item.ServiceID === '') NodeChecks;
     
       @computed('ChecksCritical', 'ChecksPassing', 'ChecksWarning')
       get Status() {
    @@ -50,16 +50,16 @@ export default class Node extends Model {
     
       @computed('NodeChecks.[]')
       get ChecksCritical() {
    -    return this.NodeChecks.filter(item => item.Status === 'critical').length;
    +    return this.NodeChecks.filter((item) => item.Status === 'critical').length;
       }
     
       @computed('NodeChecks.[]')
       get ChecksPassing() {
    -    return this.NodeChecks.filter(item => item.Status === 'passing').length;
    +    return this.NodeChecks.filter((item) => item.Status === 'passing').length;
       }
     
       @computed('NodeChecks.[]')
       get ChecksWarning() {
    -    return this.NodeChecks.filter(item => item.Status === 'warning').length;
    +    return this.NodeChecks.filter((item) => item.Status === 'warning').length;
       }
     }
    diff --git a/ui/packages/consul-ui/app/models/peer.js b/ui/packages/consul-ui/app/models/peer.js
    index cd050524b..934c1bd46 100644
    --- a/ui/packages/consul-ui/app/models/peer.js
    +++ b/ui/packages/consul-ui/app/models/peer.js
    @@ -3,14 +3,7 @@ import Model, { attr } from '@ember-data/model';
     export const schema = {
       State: {
         defaultValue: 'PENDING',
    -    allowedValues: [
    -      'PENDING',
    -      'ESTABLISHING',
    -      'ACTIVE',
    -      'FAILING',
    -      'TERMINATED',
    -      'DELETING'
    -    ],
    +    allowedValues: ['PENDING', 'ESTABLISHING', 'ACTIVE', 'FAILING', 'TERMINATED', 'DELETING'],
       },
     };
     export default class Peer extends Model {
    diff --git a/ui/packages/consul-ui/app/models/service-instance.js b/ui/packages/consul-ui/app/models/service-instance.js
    index c69d30200..1863dd6bb 100644
    --- a/ui/packages/consul-ui/app/models/service-instance.js
    +++ b/ui/packages/consul-ui/app/models/service-instance.js
    @@ -15,7 +15,7 @@ export const Collection = class Collection {
       }
     
       get ExternalSources() {
    -    const sources = this.items.reduce(function(prev, item) {
    +    const sources = this.items.reduce(function (prev, item) {
           return prev.concat(item.ExternalSources || []);
         }, []);
         // unique, non-empty values, alpha sort
    @@ -99,17 +99,17 @@ export default class ServiceInstance extends Model {
     
       @computed('Checks.[]')
       get ChecksPassing() {
    -    return this.Checks.filter(item => item.Status === 'passing');
    +    return this.Checks.filter((item) => item.Status === 'passing');
       }
     
       @computed('Checks.[]')
       get ChecksWarning() {
    -    return this.Checks.filter(item => item.Status === 'warning');
    +    return this.Checks.filter((item) => item.Status === 'warning');
       }
     
       @computed('Checks.[]')
       get ChecksCritical() {
    -    return this.Checks.filter(item => item.Status === 'critical');
    +    return this.Checks.filter((item) => item.Status === 'critical');
       }
     
       @computed('Checks.[]', 'ChecksPassing')
    diff --git a/ui/packages/consul-ui/app/models/service.js b/ui/packages/consul-ui/app/models/service.js
    index 406b96b10..70a753f44 100644
    --- a/ui/packages/consul-ui/app/models/service.js
    +++ b/ui/packages/consul-ui/app/models/service.js
    @@ -15,7 +15,7 @@ export const Collection = class Collection {
       }
     
       get ExternalSources() {
    -    const items = this.items.reduce(function(prev, item) {
    +    const items = this.items.reduce(function (prev, item) {
           return prev.concat(item.ExternalSources || []);
         }, []);
         // unique, non-empty values, alpha sort
    @@ -25,7 +25,7 @@ export const Collection = class Collection {
       // when and when not somewhere in the docs
       get Partitions() {
         // unique, non-empty values, alpha sort
    -    return [...new Set(this.items.map(item => item.Partition))].sort();
    +    return [...new Set(this.items.map((item) => item.Partition))].sort();
       }
     };
     export default class Service extends Model {
    diff --git a/ui/packages/consul-ui/app/models/token.js b/ui/packages/consul-ui/app/models/token.js
    index 60a246556..c2b31a339 100644
    --- a/ui/packages/consul-ui/app/models/token.js
    +++ b/ui/packages/consul-ui/app/models/token.js
    @@ -37,7 +37,7 @@ export default class Token extends Model {
     
       @computed('Policies.[]')
       get isGlobalManagement() {
    -    return (this.Policies || []).find(item => item.ID === MANAGEMENT_ID);
    +    return (this.Policies || []).find((item) => item.ID === MANAGEMENT_ID);
       }
     
       @computed('SecretID')
    diff --git a/ui/packages/consul-ui/app/models/topology.js b/ui/packages/consul-ui/app/models/topology.js
    index 596b74cc6..7a4e165cc 100644
    --- a/ui/packages/consul-ui/app/models/topology.js
    +++ b/ui/packages/consul-ui/app/models/topology.js
    @@ -25,8 +25,11 @@ export default class Topology extends Model {
     
         undefinedDownstream =
           this.Downstreams.filter(
    -        item =>
    -          item.Source === 'specific-intention' && !item.TransparentProxy && !item.ConnectNative && item.Intention.Allowed
    +        (item) =>
    +          item.Source === 'specific-intention' &&
    +          !item.TransparentProxy &&
    +          !item.ConnectNative &&
    +          item.Intention.Allowed
           ).length !== 0;
     
         return undefinedDownstream;
    @@ -38,11 +41,11 @@ export default class Topology extends Model {
       // a wildcard intention
       get wildcardIntention() {
         const downstreamWildcard =
    -      this.Downstreams.filter(item => !item.Intention.HasExact && item.Intention.Allowed).length !==
    -      0;
    +      this.Downstreams.filter((item) => !item.Intention.HasExact && item.Intention.Allowed)
    +        .length !== 0;
     
         const upstreamWildcard =
    -      this.Upstreams.filter(item => !item.Intention.HasExact && item.Intention.Allowed).length !==
    +      this.Upstreams.filter((item) => !item.Intention.HasExact && item.Intention.Allowed).length !==
           0;
     
         return downstreamWildcard || upstreamWildcard;
    diff --git a/ui/packages/consul-ui/app/modifiers/aria-menu.js b/ui/packages/consul-ui/app/modifiers/aria-menu.js
    index f153cd8c2..61c1c1751 100644
    --- a/ui/packages/consul-ui/app/modifiers/aria-menu.js
    +++ b/ui/packages/consul-ui/app/modifiers/aria-menu.js
    @@ -45,7 +45,7 @@ export default class AriaMenuModifier extends Modifier {
           return;
         }
         const $items = [...this.element.querySelectorAll(MENU_ITEMS)];
    -    const pos = $items.findIndex($item => $item === this.doc.activeElement);
    +    const pos = $items.findIndex(($item) => $item === this.doc.activeElement);
         if (e.keyCode === TAB) {
           if (e.shiftKey) {
             if (pos === 0) {
    @@ -54,7 +54,7 @@ export default class AriaMenuModifier extends Modifier {
             }
           } else {
             if (pos === $items.length - 1) {
    -          await new Promise(resolve => setTimeout(resolve, 0));
    +          await new Promise((resolve) => setTimeout(resolve, 0));
               this.options.onclose(e);
             }
           }
    diff --git a/ui/packages/consul-ui/app/modifiers/css-props.js b/ui/packages/consul-ui/app/modifiers/css-props.js
    index adca9383a..a1826a7bb 100644
    --- a/ui/packages/consul-ui/app/modifiers/css-props.js
    +++ b/ui/packages/consul-ui/app/modifiers/css-props.js
    @@ -1,16 +1,16 @@
     import { modifier } from 'ember-modifier';
     const STYLE_RULE = 1;
    -const getCustomProperties = function() {
    +const getCustomProperties = function () {
       return Object.fromEntries(
         [...document.styleSheets].reduce(
           (prev, item) =>
             prev.concat(
               [...item.cssRules]
    -            .filter(item => item.type === STYLE_RULE)
    +            .filter((item) => item.type === STYLE_RULE)
                 .reduce((prev, rule) => {
                   const props = [...rule.style]
    -                .filter(prop => prop.startsWith('--'))
    -                .map(prop => [prop.trim(), rule.style.getPropertyValue(prop).trim()]);
    +                .filter((prop) => prop.startsWith('--'))
    +                .map((prop) => [prop.trim(), rule.style.getPropertyValue(prop).trim()]);
                   return [...prev, ...props];
                 }, [])
             ),
    @@ -19,25 +19,25 @@ const getCustomProperties = function() {
       );
     };
     const props = getCustomProperties();
    -export default modifier(function($element, [returns], hash) {
    -    const re = new RegExp(`^--${hash.prefix || '.'}${hash.group || ''}+`);
    -    const obj = {};
    -    Object.entries(props).forEach(([key, value]) => {
    -      const res = key.match(re);
    -      if(res) {
    -        let prop = res[0];
    -        if (prop.charAt(prop.length - 1) === '-') {
    -          prop = prop.substr(0, prop.length - 1);
    -        }
    -        if(hash.group) {
    -          if (typeof obj[prop] === 'undefined') {
    -            obj[prop] = {};
    -          }
    -          obj[prop][key] = value;
    -        } else {
    -          obj[key] = value;
    -        }
    +export default modifier(function ($element, [returns], hash) {
    +  const re = new RegExp(`^--${hash.prefix || '.'}${hash.group || ''}+`);
    +  const obj = {};
    +  Object.entries(props).forEach(([key, value]) => {
    +    const res = key.match(re);
    +    if (res) {
    +      let prop = res[0];
    +      if (prop.charAt(prop.length - 1) === '-') {
    +        prop = prop.substr(0, prop.length - 1);
           }
    -    });
    -    returns(obj);
    +      if (hash.group) {
    +        if (typeof obj[prop] === 'undefined') {
    +          obj[prop] = {};
    +        }
    +        obj[prop][key] = value;
    +      } else {
    +        obj[key] = value;
    +      }
    +    }
    +  });
    +  returns(obj);
     });
    diff --git a/ui/packages/consul-ui/app/modifiers/did-upsert.js b/ui/packages/consul-ui/app/modifiers/did-upsert.js
    index 0cfd0d5fb..7dcbf640f 100644
    --- a/ui/packages/consul-ui/app/modifiers/did-upsert.js
    +++ b/ui/packages/consul-ui/app/modifiers/did-upsert.js
    @@ -1,7 +1,7 @@
     import { setModifierManager, capabilities } from '@ember/modifier';
     import { gte } from 'ember-compatibility-helpers';
     
    -const createEventLike = state => {
    +const createEventLike = (state) => {
       return {
         target: state.element,
         currentTarget: state.element,
    diff --git a/ui/packages/consul-ui/app/modifiers/disabled.js b/ui/packages/consul-ui/app/modifiers/disabled.js
    index e3171496e..18d12449c 100644
    --- a/ui/packages/consul-ui/app/modifiers/disabled.js
    +++ b/ui/packages/consul-ui/app/modifiers/disabled.js
    @@ -13,7 +13,7 @@ export default modifier(function enabled($element, [bool = true], hash) {
         return;
       }
       for (const $el of $element.querySelectorAll('input,textarea,button')) {
    -    if(bool && $el.dataset.disabled !== 'false') {
    +    if (bool && $el.dataset.disabled !== 'false') {
           $element.setAttribute('disabled', bool);
           $element.setAttribute('aria-disabled', bool);
         } else {
    diff --git a/ui/packages/consul-ui/app/modifiers/notification.js b/ui/packages/consul-ui/app/modifiers/notification.js
    index 64ed41277..9645246bc 100644
    --- a/ui/packages/consul-ui/app/modifiers/notification.js
    +++ b/ui/packages/consul-ui/app/modifiers/notification.js
    @@ -16,13 +16,14 @@ export default class NotificationModifier extends Modifier {
         this.element.remove();
         this.notify.clearMessages();
         if (typeof options.after === 'function') {
    -      Promise.resolve().then(_ => options.after())
    -        .catch(e => {
    +      Promise.resolve()
    +        .then((_) => options.after())
    +        .catch((e) => {
               if (e.name !== 'TransitionAborted') {
                 throw e;
               }
             })
    -        .then(res => {
    +        .then((res) => {
               this.notify.add(options);
             });
         } else {
    @@ -30,7 +31,7 @@ export default class NotificationModifier extends Modifier {
         }
       }
       willDestroy() {
    -    if(this.args.named.sticky) {
    +    if (this.args.named.sticky) {
           this.notify.clearMessages();
         }
       }
    diff --git a/ui/packages/consul-ui/app/modifiers/on-outside.js b/ui/packages/consul-ui/app/modifiers/on-outside.js
    index fe32dee5a..614eb93bf 100644
    --- a/ui/packages/consul-ui/app/modifiers/on-outside.js
    +++ b/ui/packages/consul-ui/app/modifiers/on-outside.js
    @@ -10,7 +10,7 @@ export default class OnOutsideModifier extends Modifier {
         this.doc = this.dom.document();
       }
       async connect(params, options) {
    -    await new Promise(resolve => setTimeout(resolve, 0));
    +    await new Promise((resolve) => setTimeout(resolve, 0));
         try {
           this.doc.addEventListener(params[0], this.listen);
         } catch (e) {
    @@ -21,7 +21,7 @@ export default class OnOutsideModifier extends Modifier {
       @action
       listen(e) {
         if (this.dom.isOutside(this.element, e.target)) {
    -      const dispatch = typeof this.params[1] === 'function' ? this.params[1] : _ => {};
    +      const dispatch = typeof this.params[1] === 'function' ? this.params[1] : (_) => {};
           dispatch.apply(this.element, [e]);
         }
       }
    diff --git a/ui/packages/consul-ui/app/modifiers/style.js b/ui/packages/consul-ui/app/modifiers/style.js
    index 69a4b971f..8089743b2 100644
    --- a/ui/packages/consul-ui/app/modifiers/style.js
    +++ b/ui/packages/consul-ui/app/modifiers/style.js
    @@ -4,8 +4,8 @@ import { assert } from '@ember/debug';
     export default class StyleModifier extends Modifier {
       setStyles(newStyles = []) {
         const rulesToRemove = this._oldStyles || new Set();
    -    if(!Array.isArray(newStyles)) {
    -      newStyles = Object.entries(newStyles)
    +    if (!Array.isArray(newStyles)) {
    +      newStyles = Object.entries(newStyles);
         }
         newStyles.forEach(([property, value]) => {
           assert(
    @@ -36,15 +36,12 @@ export default class StyleModifier extends Modifier {
       }
     
       didReceiveArguments() {
    -    if(typeof this.args.named.delay !== 'undefined') {
    -      setTimeout(
    -        _ => {
    -          if(typeof this !== this.args.positional[0]) {
    -            this.setStyles(this.args.positional[0]);
    -          }
    -        },
    -        this.args.named.delay
    -      )
    +    if (typeof this.args.named.delay !== 'undefined') {
    +      setTimeout((_) => {
    +        if (typeof this !== this.args.positional[0]) {
    +          this.setStyles(this.args.positional[0]);
    +        }
    +      }, this.args.named.delay);
         } else {
           this.setStyles(this.args.positional[0]);
         }
    diff --git a/ui/packages/consul-ui/app/modifiers/tooltip.js b/ui/packages/consul-ui/app/modifiers/tooltip.js
    index 336222278..c2127da54 100644
    --- a/ui/packages/consul-ui/app/modifiers/tooltip.js
    +++ b/ui/packages/consul-ui/app/modifiers/tooltip.js
    @@ -8,68 +8,69 @@ import tippy, { followCursor } from 'tippy.js';
      * {{tooltip 'Text' options=(hash )}}
      */
     export default modifier(($element, [content], hash = {}) => {
    +  if (typeof content === 'string' && content.trim() === '') {
    +    return;
    +  }
       const options = hash.options || {};
     
    -  if (!options.hideTooltip) {
    -    let $anchor = $element;
    +  let $anchor = $element;
     
    -    // make it easy to specify the modified element as the actual tooltip
    -    if (typeof options.triggerTarget === 'string') {
    -      const $el = $anchor;
    -      switch (options.triggerTarget) {
    -        case 'parentNode':
    -          $anchor = $anchor.parentNode;
    -          break;
    -        default:
    -          $anchor = $anchor.querySelectorAll(options.triggerTarget);
    -      }
    -      content = $anchor.cloneNode(true);
    -      $el.remove();
    -      hash.options.triggerTarget = undefined;
    +  // make it easy to specify the modified element as the actual tooltip
    +  if (typeof options.triggerTarget === 'string') {
    +    const $el = $anchor;
    +    switch (options.triggerTarget) {
    +      case 'parentNode':
    +        $anchor = $anchor.parentNode;
    +        break;
    +      default:
    +        $anchor = $anchor.querySelectorAll(options.triggerTarget);
         }
    -    // {{tooltip}} will just use the HTML content
    -    if (typeof content === 'undefined') {
    -      content = $anchor.innerHTML;
    -      $anchor.innerHTML = '';
    -    }
    -    let interval;
    -    if (options.trigger === 'manual') {
    -      // if we are manually triggering, a out delay means only show for the
    -      // amount of time specified by the delay
    -      const delay = options.delay || [];
    -      if (typeof delay[1] !== 'undefined') {
    -        hash.options.onShown = tooltip => {
    -          clearInterval(interval);
    -          interval = setTimeout(() => {
    -            tooltip.hide();
    -          }, delay[1]);
    -        };
    -      }
    -    }
    -    let $trigger = $anchor;
    -    let needsTabIndex = false;
    -    if (!$trigger.hasAttribute('tabindex')) {
    -      needsTabIndex = true;
    -      $trigger.setAttribute('tabindex', '0');
    -    }
    -    const tooltip = tippy($anchor, {
    -      theme: 'tooltip',
    -      triggerTarget: $trigger,
    -      content: $anchor => content,
    -      // showOnCreate: true,
    -      // hideOnClick: false,
    -      plugins: [
    -        typeof options.followCursor !== 'undefined' ? followCursor : undefined,
    -      ].filter(item => Boolean(item)),
    -      ...hash.options,
    -    });
    -
    -    return () => {
    -      if (needsTabIndex) {
    -        $trigger.removeAttribute('tabindex');
    -      }
    -      clearInterval(interval);
    -      tooltip.destroy();
    -    };
    +    content = $anchor.cloneNode(true);
    +    $el.remove();
    +    hash.options.triggerTarget = undefined;
       }
    +  // {{tooltip}} will just use the HTML content
    +  if (typeof content === 'undefined') {
    +    content = $anchor.innerHTML;
    +    $anchor.innerHTML = '';
    +  }
    +  let interval;
    +  if (options.trigger === 'manual') {
    +    // if we are manually triggering, a out delay means only show for the
    +    // amount of time specified by the delay
    +    const delay = options.delay || [];
    +    if (typeof delay[1] !== 'undefined') {
    +      hash.options.onShown = (tooltip) => {
    +        clearInterval(interval);
    +        interval = setTimeout(() => {
    +          tooltip.hide();
    +        }, delay[1]);
    +      };
    +    }
    +  }
    +  let $trigger = $anchor;
    +  let needsTabIndex = false;
    +  if (!$trigger.hasAttribute('tabindex')) {
    +    needsTabIndex = true;
    +    $trigger.setAttribute('tabindex', '0');
    +  }
    +  const tooltip = tippy($anchor, {
    +    theme: 'tooltip',
    +    triggerTarget: $trigger,
    +    content: ($anchor) => content,
    +    // showOnCreate: true,
    +    // hideOnClick: false,
    +    plugins: [typeof options.followCursor !== 'undefined' ? followCursor : undefined].filter(
    +      (item) => Boolean(item)
    +    ),
    +    ...hash.options,
    +  });
    +
    +  return () => {
    +    if (needsTabIndex) {
    +      $trigger.removeAttribute('tabindex');
    +    }
    +    clearInterval(interval);
    +    tooltip.destroy();
    +  };
     });
    diff --git a/ui/packages/consul-ui/app/modifiers/tooltip.mdx b/ui/packages/consul-ui/app/modifiers/tooltip.mdx
    index 57f6463b9..cc689c052 100644
    --- a/ui/packages/consul-ui/app/modifiers/tooltip.mdx
    +++ b/ui/packages/consul-ui/app/modifiers/tooltip.mdx
    @@ -11,7 +11,21 @@ most common usage will be something like the below:
     
     ```
     
    -A `tabindex=0` is automatically added to the element that triggers the tooltip if it doesn't have one already to make sure the tooltip is in the natural tabbing order of the document.
    +A `tabindex=0` is automatically added to the element that triggers the tooltip
    +if it doesn't have one already to make sure the tooltip is in the natural
    +tabbing order of the document.
    +
    +If you specify and empty string as a content, then the tooltip will not show,
    +you can use this to conditionally disable/enable tooltips. It additionally
    +means empty tooltips can not been added by accident.
    +
    +```hbs preview-template
    +
    +  Hover over me
    +
    +```
     
     An additional options hash can be passed into the tooltip the contents of
     which are passed along to `tippy.js` [so all of the `tippy.js`
    diff --git a/ui/packages/consul-ui/app/modifiers/validate.js b/ui/packages/consul-ui/app/modifiers/validate.js
    index 72c0b8bb9..9bf996cb0 100644
    --- a/ui/packages/consul-ui/app/modifiers/validate.js
    +++ b/ui/packages/consul-ui/app/modifiers/validate.js
    @@ -4,12 +4,11 @@ import { action } from '@ember/object';
     class ValidationError extends Error {}
     
     export default class ValidateModifier extends Modifier {
    -
       item = null;
       hash = null;
     
       validate(value, validations = {}) {
    -    if(Object.keys(validations).length === 0) {
    +    if (Object.keys(validations).length === 0) {
           return;
         }
         const errors = {};
    @@ -18,66 +17,60 @@ export default class ValidateModifier extends Modifier {
           .filter(([key, value]) => typeof value !== 'string')
           .forEach(([key, item]) => {
             // optionally set things for you
    -        if(this.item) {
    +        if (this.item) {
               this.item[key] = value;
             }
             (item || []).forEach((validation) => {
               const re = new RegExp(validation.test);
    -          if(!re.test(value)) {
    +          if (!re.test(value)) {
                 errors[key] = new ValidationError(validation.error);
               }
             });
    -    });
    -    const state = this.hash.chart.state;
    -    if(state.context == null) {
    +      });
    +    const state = this.hash.chart.state || {};
    +    if (state.context == null) {
           state.context = {};
         }
    -    if(Object.keys(errors).length > 0) {
    +    if (Object.keys(errors).length > 0) {
           state.context.errors = errors;
    -      this.hash.chart.dispatch("ERROR", state.context);
    +      this.hash.chart.dispatch('ERROR', state.context);
         } else {
           state.context.errors = null;
    -      this.hash.chart.dispatch("RESET", state.context);
    +      this.hash.chart.dispatch('RESET', state.context);
         }
       }
     
       @action
       reset(e) {
    -    if(e.target.value.length === 0) {
    +    if (e.target.value.length === 0) {
           const state = this.hash.chart.state;
    -      if(!state.context) {
    +      if (!state.context) {
             state.context = {};
           }
    -      if(!state.context.errors) {
    +      if (!state.context.errors) {
             state.context.errors = {};
           }
           Object.entries(this.hash.validations)
             // filter out strings, for now these are helps, but ain't great if someone has a item.help
             .filter(([key, value]) => typeof value !== 'string')
             .forEach(([key, item]) => {
    -          if(typeof state.context.errors[key] !== 'undefined') {
    +          if (typeof state.context.errors[key] !== 'undefined') {
                 delete state.context.errors[key];
               }
    -      });
    -      if(Object.keys(state.context.errors).length === 0) {
    +        });
    +      if (Object.keys(state.context.errors).length === 0) {
             state.context.errors = null;
    -        this.hash.chart.dispatch("RESET", state.context);
    +        this.hash.chart.dispatch('RESET', state.context);
           }
         }
       }
     
       async connect([value], _hash) {
    -    this.element.addEventListener(
    -      'input',
    -      this.listen
    -    );
    -    this.element.addEventListener(
    -      'blur',
    -      this.reset
    -    );
    -    if(this.element.value.length > 0) {
    +    this.element.addEventListener('input', this.listen);
    +    this.element.addEventListener('blur', this.reset);
    +    if (this.element.value.length > 0) {
           await Promise.resolve();
    -      if(this && this.element) {
    +      if (this && this.element) {
             this.validate(this.element.value, this.hash.validations);
           }
         }
    @@ -91,17 +84,10 @@ export default class ValidateModifier extends Modifier {
       disconnect() {
         this.item = null;
         this.hash = null;
    -    this.element.removeEventListener(
    -      'input',
    -      this.listen
    -    )
    -    this.element.removeEventListener(
    -      'blur',
    -      this.reset
    -    )
    +    this.element.removeEventListener('input', this.listen);
    +    this.element.removeEventListener('blur', this.reset);
       }
     
    -
       didReceiveArguments() {
         const [value] = this.args.positional;
         const _hash = this.args.named;
    @@ -109,13 +95,13 @@ export default class ValidateModifier extends Modifier {
         this.item = value;
         this.hash = _hash;
     
    -    if(typeof _hash.chart === 'undefined') {
    +    if (typeof _hash.chart === 'undefined') {
           this.hash.chart = {
             state: {
    -          context: {}
    +          context: {},
             },
             dispatch: (state) => {
    -          switch(state) {
    +          switch (state) {
                 case 'ERROR':
                   _hash.onchange(this.hash.chart.state.context.errors);
                   break;
    @@ -123,9 +109,8 @@ export default class ValidateModifier extends Modifier {
                   _hash.onchange();
                   break;
               }
    -        }
    +        },
           };
    -
         }
       }
     
    diff --git a/ui/packages/consul-ui/app/modifiers/with-copyable.js b/ui/packages/consul-ui/app/modifiers/with-copyable.js
    index 562f05875..fc41a60e3 100644
    --- a/ui/packages/consul-ui/app/modifiers/with-copyable.js
    +++ b/ui/packages/consul-ui/app/modifiers/with-copyable.js
    @@ -14,18 +14,18 @@ export default class WithCopyableModifier extends Modifier {
       connect([value], _hash) {
         value = typeAssertion('string', value, this.element.innerText);
         const hash = {
    -      success: e => {
    -        runInDebug(_ => console.info(`with-copyable: Copied \`${value}\``));
    +      success: (e) => {
    +        runInDebug((_) => console.info(`with-copyable: Copied \`${value}\``));
             return typeAssertion('function', _hash.success, () => {})(e);
           },
    -      error: e => {
    -        runInDebug(_ => console.info(`with-copyable: Error copying \`${value}\``));
    +      error: (e) => {
    +        runInDebug((_) => console.info(`with-copyable: Error copying \`${value}\``));
             return typeAssertion('function', _hash.error, () => {})(e);
           },
         };
         this.source = this.clipboard
           .execute(this.element, {
    -        text: _ => value,
    +        text: (_) => value,
             ...hash.options,
           })
           .on('success', hash.success)
    diff --git a/ui/packages/consul-ui/app/modifiers/with-overlay.js b/ui/packages/consul-ui/app/modifiers/with-overlay.js
    index 74ba4a608..1248ef0c7 100644
    --- a/ui/packages/consul-ui/app/modifiers/with-overlay.js
    +++ b/ui/packages/consul-ui/app/modifiers/with-overlay.js
    @@ -40,7 +40,7 @@ export default modifier(($element, [content], hash = {}) => {
         // amount of time specified by the delay
         const delay = options.delay || [];
         if (typeof delay[1] !== 'undefined') {
    -      options.onShown = popover => {
    +      options.onShown = (popover) => {
             clearInterval(interval);
             interval = setTimeout(() => {
               popover.hide();
    @@ -51,12 +51,12 @@ export default modifier(($element, [content], hash = {}) => {
       let $trigger = $anchor;
       const popover = tippy($anchor, {
         triggerTarget: $trigger,
    -    content: $anchor => content,
    +    content: ($anchor) => content,
         // showOnCreate: true,
         // hideOnClick: false,
         interactive: true,
    -    plugins: [typeof options.followCursor !== 'undefined' ? followCursor : undefined].filter(item =>
    -      Boolean(item)
    +    plugins: [typeof options.followCursor !== 'undefined' ? followCursor : undefined].filter(
    +      (item) => Boolean(item)
         ),
         ...options,
       });
    diff --git a/ui/packages/consul-ui/app/router.js b/ui/packages/consul-ui/app/router.js
    index f89519e6f..f4b1f49c8 100644
    --- a/ui/packages/consul-ui/app/router.js
    +++ b/ui/packages/consul-ui/app/router.js
    @@ -10,7 +10,9 @@ const doc = document;
     const appName = config.modulePrefix;
     
     export const routes = merge.all(
    -  [...doc.querySelectorAll(`script[data-routes]`)].map($item => JSON.parse($item.dataset[`routes`]))
    +  [...doc.querySelectorAll(`script[data-routes]`)].map(($item) =>
    +    JSON.parse($item.dataset[`routes`])
    +  )
     );
     
     runInDebug(() => {
    @@ -27,7 +29,7 @@ runInDebug(() => {
               _options: { path: page.name },
             };
           }
    -      page.pages.forEach(page => {
    +      page.pages.forEach((page) => {
             const url = page.relativeUrl;
             if (typeof url === 'string') {
               if (url !== '') {
    @@ -37,7 +39,7 @@ runInDebug(() => {
               }
             }
           });
    -      page.children.forEach(child => {
    +      page.children.forEach((child) => {
             addPage(route, child);
           });
         })(routes, output.default.nested);
    diff --git a/ui/packages/consul-ui/app/routes/application.js b/ui/packages/consul-ui/app/routes/application.js
    index 89eb67c53..dd81c3498 100644
    --- a/ui/packages/consul-ui/app/routes/application.js
    +++ b/ui/packages/consul-ui/app/routes/application.js
    @@ -1,27 +1,57 @@
     import Route from 'consul-ui/routing/route';
     import { action } from '@ember/object';
     import { inject as service } from '@ember/service';
    +import { runInDebug } from '@ember/debug';
     
     import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
     
     export default class ApplicationRoute extends Route.extend(WithBlockingActions) {
       @service('client/http') client;
    +  @service('env') env;
    +  @service('repository/token') tokenRepo;
    +  @service('settings') settings;
     
       data;
     
    +  async model() {
    +    if (this.env.var('CONSUL_ACLS_ENABLED')) {
    +      const secret = this.env.var('CONSUL_HTTP_TOKEN');
    +      const existing = await this.settings.findBySlug('token');
    +      if (!existing.AccessorID && secret) {
    +        try {
    +          const token = await this.tokenRepo.self({
    +            secret: secret,
    +            dc: this.env.var('CONSUL_DATACENTER_LOCAL'),
    +          });
    +          await this.settings.persist({
    +            token: {
    +              AccessorID: token.AccessorID,
    +              SecretID: token.SecretID,
    +              Namespace: token.Namespace,
    +              Partition: token.Partition,
    +            },
    +          });
    +        } catch (e) {
    +          runInDebug((_) => console.error(e));
    +        }
    +      }
    +    }
    +    return {};
    +  }
    +
       @action
       onClientChanged(e) {
         let data = e.data;
    -    if(data === '') {
    +    if (data === '') {
           data = { blocking: true };
         }
         // this.data is always undefined first time round and its the 'first read'
         // of the value so we don't need to abort anything
    -    if(typeof this.data === 'undefined') {
    +    if (typeof this.data === 'undefined') {
           this.data = Object.assign({}, data);
           return;
         }
    -    if(this.data.blocking === true && data.blocking === false) {
    +    if (this.data.blocking === true && data.blocking === false) {
           this.client.abort();
         }
         this.data = Object.assign({}, data);
    diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/topology.js b/ui/packages/consul-ui/app/routes/dc/services/show/topology.js
    index e468ebd90..a90477a63 100644
    --- a/ui/packages/consul-ui/app/routes/dc/services/show/topology.js
    +++ b/ui/packages/consul-ui/app/routes/dc/services/show/topology.js
    @@ -15,7 +15,7 @@ export default class TopologyRoute extends Route {
         try {
           // intentions will be a proxy object
           let intentions = await this.intentions;
    -      let intention = intentions.find(item => {
    +      let intention = intentions.find((item) => {
             return (
               item.Datacenter === source.Datacenter &&
               item.SourceName === source.Name &&
    @@ -56,7 +56,7 @@ export default class TopologyRoute extends Route {
           ...this.paramsFor('dc.services.show'),
         };
         this.intentions = this.data.source(
    -      uri =>
    +      (uri) =>
             uri`/${params.partition}/${params.nspace}/${params.dc}/intentions/for-service/${params.name}`
         );
       }
    diff --git a/ui/packages/consul-ui/app/routing/application-debug.js b/ui/packages/consul-ui/app/routing/application-debug.js
    index 013dffa07..fb4d86de7 100644
    --- a/ui/packages/consul-ui/app/routing/application-debug.js
    +++ b/ui/packages/consul-ui/app/routing/application-debug.js
    @@ -1,7 +1,7 @@
     import ApplicationRoute from '../routes/application';
     
     let isDebugRoute = false;
    -const routeChange = function(transition) {
    +const routeChange = function (transition) {
       isDebugRoute = transition.to.name.startsWith('docs');
     };
     
    diff --git a/ui/packages/consul-ui/app/routing/route.js b/ui/packages/consul-ui/app/routing/route.js
    index c50b975a8..25d3dcdd4 100644
    --- a/ui/packages/consul-ui/app/routing/route.js
    +++ b/ui/packages/consul-ui/app/routing/route.js
    @@ -34,9 +34,7 @@ export default class BaseRoute extends Route {
           // which is why I didn't do it originally so be sure to look properly if
           // you feel like adding a return
           this.replaceWith(
    -        resolve(this.routeName.split('.').join('/'), to)
    -          .split('/')
    -          .join('.'),
    +        resolve(this.routeName.split('.').join('/'), to).split('/').join('.'),
             model
           );
         }
    @@ -65,7 +63,7 @@ export default class BaseRoute extends Route {
             if (Array.isArray(actual)) {
               actual = actual.split(',');
             }
    -        const diff = possible.filter(item => !actual.includes(item));
    +        const diff = possible.filter((item) => !actual.includes(item));
             if (diff.length === 0) {
               value = undefined;
             }
    diff --git a/ui/packages/consul-ui/app/routing/single.js b/ui/packages/consul-ui/app/routing/single.js
    index 2fe4c3955..ce38ddeb4 100644
    --- a/ui/packages/consul-ui/app/routing/single.js
    +++ b/ui/packages/consul-ui/app/routing/single.js
    @@ -3,10 +3,10 @@ import { assert } from '@ember/debug';
     import { Promise, hash } from 'rsvp';
     export default Route.extend({
       // repo: service('repositoryName'),
    -  isCreate: function(params, transition) {
    +  isCreate: function (params, transition) {
         return transition.targetName.split('.').pop() === 'create';
       },
    -  model: function(params, transition) {
    +  model: function (params, transition) {
         const repo = this.repo;
         assert(
           "`repo` is undefined, please define RepositoryService using `repo: service('repositoryName')`",
    diff --git a/ui/packages/consul-ui/app/search/predicates/acl.js b/ui/packages/consul-ui/app/search/predicates/acl.js
    index 6e3992e01..97450a10b 100644
    --- a/ui/packages/consul-ui/app/search/predicates/acl.js
    +++ b/ui/packages/consul-ui/app/search/predicates/acl.js
    @@ -1,4 +1,4 @@
     export default {
    -  ID: item => item.ID,
    -  Name: item => item.Name,
    +  ID: (item) => item.ID,
    +  Name: (item) => item.Name,
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/auth-method.js b/ui/packages/consul-ui/app/search/predicates/auth-method.js
    index 15a6cf351..514a421e7 100644
    --- a/ui/packages/consul-ui/app/search/predicates/auth-method.js
    +++ b/ui/packages/consul-ui/app/search/predicates/auth-method.js
    @@ -1,4 +1,4 @@
     export default {
    -  Name: item => item.Name,
    -  DisplayName: item => item.DisplayName,
    +  Name: (item) => item.Name,
    +  DisplayName: (item) => item.DisplayName,
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/health-check.js b/ui/packages/consul-ui/app/search/predicates/health-check.js
    index dfe69d313..f1146c377 100644
    --- a/ui/packages/consul-ui/app/search/predicates/health-check.js
    +++ b/ui/packages/consul-ui/app/search/predicates/health-check.js
    @@ -1,13 +1,13 @@
    -const asArray = function(arr) {
    +const asArray = function (arr) {
       return Array.isArray(arr) ? arr : arr.toArray();
     };
     export default {
    -  Name: item => item.Name,
    -  Node: item => item.Node,
    -  Service: item => item.ServiceName,
    -  CheckID: item => item.CheckID || '',
    -  ID: item => item.Service.ID || '',
    -  Notes: item => item.Notes,
    -  Output: item => item.Output,
    -  ServiceTags: item => asArray(item.ServiceTags),
    +  Name: (item) => item.Name,
    +  Node: (item) => item.Node,
    +  Service: (item) => item.ServiceName,
    +  CheckID: (item) => item.CheckID || '',
    +  ID: (item) => item.Service.ID || '',
    +  Notes: (item) => item.Notes,
    +  Output: (item) => item.Output,
    +  ServiceTags: (item) => asArray(item.ServiceTags),
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/intention.js b/ui/packages/consul-ui/app/search/predicates/intention.js
    index b4f2f3e51..dffd51438 100644
    --- a/ui/packages/consul-ui/app/search/predicates/intention.js
    +++ b/ui/packages/consul-ui/app/search/predicates/intention.js
    @@ -1,7 +1,7 @@
     const allLabel = 'All Services (*)';
     export default {
    -  SourceName: item =>
    +  SourceName: (item) =>
         [item.SourceName, item.SourceName === '*' ? allLabel : undefined].filter(Boolean),
    -  DestinationName: item =>
    +  DestinationName: (item) =>
         [item.DestinationName, item.DestinationName === '*' ? allLabel : undefined].filter(Boolean),
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/kv.js b/ui/packages/consul-ui/app/search/predicates/kv.js
    index 76d5fe5a3..ffdc035e1 100644
    --- a/ui/packages/consul-ui/app/search/predicates/kv.js
    +++ b/ui/packages/consul-ui/app/search/predicates/kv.js
    @@ -1,8 +1,8 @@
     import rightTrim from 'consul-ui/utils/right-trim';
     export default {
    -  Key: item =>
    +  Key: (item) =>
         rightTrim(item.Key.toLowerCase())
           .split('/')
    -      .filter(item => Boolean(item))
    +      .filter((item) => Boolean(item))
           .pop(),
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/node.js b/ui/packages/consul-ui/app/search/predicates/node.js
    index b780a6b1e..8db2ff828 100644
    --- a/ui/packages/consul-ui/app/search/predicates/node.js
    +++ b/ui/packages/consul-ui/app/search/predicates/node.js
    @@ -1,6 +1,6 @@
     export default {
    -  Node: item => item.Node,
    -  Address: item => item.Address,
    -  PeerName: item => item.PeerName,
    -  Meta: item => Object.entries(item.Meta || {}).reduce((prev, entry) => prev.concat(entry), []),
    +  Node: (item) => item.Node,
    +  Address: (item) => item.Address,
    +  PeerName: (item) => item.PeerName,
    +  Meta: (item) => Object.entries(item.Meta || {}).reduce((prev, entry) => prev.concat(entry), []),
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/nspace.js b/ui/packages/consul-ui/app/search/predicates/nspace.js
    index ada8ea8aa..cccbe018b 100644
    --- a/ui/packages/consul-ui/app/search/predicates/nspace.js
    +++ b/ui/packages/consul-ui/app/search/predicates/nspace.js
    @@ -1,12 +1,12 @@
     export default {
    -  Name: item => item.Name,
    -  Description: item => item.Description,
    -  Role: item => {
    +  Name: (item) => item.Name,
    +  Description: (item) => item.Description,
    +  Role: (item) => {
         const acls = item.ACLs || {};
    -    return (acls.RoleDefaults || []).map(item => item.Name);
    +    return (acls.RoleDefaults || []).map((item) => item.Name);
       },
    -  Policy: item => {
    +  Policy: (item) => {
         const acls = item.ACLs || {};
    -    return (acls.PolicyDefaults || []).map(item => item.Name);
    +    return (acls.PolicyDefaults || []).map((item) => item.Name);
       },
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/peer.js b/ui/packages/consul-ui/app/search/predicates/peer.js
    index 5f1606a8e..745efbaeb 100644
    --- a/ui/packages/consul-ui/app/search/predicates/peer.js
    +++ b/ui/packages/consul-ui/app/search/predicates/peer.js
    @@ -1,4 +1,4 @@
     export default {
    -  Name: item => item.Name,
    -  ID: item => item.ID,
    +  Name: (item) => item.Name,
    +  ID: (item) => item.ID,
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/policy.js b/ui/packages/consul-ui/app/search/predicates/policy.js
    index a9efe8582..f4f9cd641 100644
    --- a/ui/packages/consul-ui/app/search/predicates/policy.js
    +++ b/ui/packages/consul-ui/app/search/predicates/policy.js
    @@ -1,4 +1,4 @@
     export default {
    -  Name: item => item.Name,
    -  Description: item => item.Description,
    +  Name: (item) => item.Name,
    +  Description: (item) => item.Description,
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/role.js b/ui/packages/consul-ui/app/search/predicates/role.js
    index 397f2f61e..21fd3ec3d 100644
    --- a/ui/packages/consul-ui/app/search/predicates/role.js
    +++ b/ui/packages/consul-ui/app/search/predicates/role.js
    @@ -1,10 +1,10 @@
     export default {
    -  Name: item => item.Name,
    -  Description: item => item.Description,
    -  Policy: item => {
    +  Name: (item) => item.Name,
    +  Description: (item) => item.Description,
    +  Policy: (item) => {
         return (item.Policies || [])
    -      .map(item => item.Name)
    -      .concat((item.ServiceIdentities || []).map(item => item.ServiceName))
    -      .concat((item.NodeIdentities || []).map(item => item.NodeName));
    +      .map((item) => item.Name)
    +      .concat((item.ServiceIdentities || []).map((item) => item.ServiceName))
    +      .concat((item.NodeIdentities || []).map((item) => item.NodeName));
       },
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/service-instance.js b/ui/packages/consul-ui/app/search/predicates/service-instance.js
    index 0f059e580..7dbc5bad8 100644
    --- a/ui/packages/consul-ui/app/search/predicates/service-instance.js
    +++ b/ui/packages/consul-ui/app/search/predicates/service-instance.js
    @@ -1,12 +1,12 @@
     export default {
    -  Name: item => item.Name,
    -  Node: item => item.Node.Node,
    -  Tags: item => item.Service.Tags || [],
    -  ID: item => item.Service.ID || '',
    -  Address: item => item.Address || '',
    -  Port: item => (item.Service.Port || '').toString(),
    -  ['Service.Meta']: item =>
    +  Name: (item) => item.Name,
    +  Node: (item) => item.Node.Node,
    +  Tags: (item) => item.Service.Tags || [],
    +  ID: (item) => item.Service.ID || '',
    +  Address: (item) => item.Address || '',
    +  Port: (item) => (item.Service.Port || '').toString(),
    +  ['Service.Meta']: (item) =>
         Object.entries(item.Service.Meta || {}).reduce((prev, entry) => prev.concat(entry), []),
    -  ['Node.Meta']: item =>
    +  ['Node.Meta']: (item) =>
         Object.entries(item.Node.Meta || {}).reduce((prev, entry) => prev.concat(entry), []),
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/service.js b/ui/packages/consul-ui/app/search/predicates/service.js
    index 63d2d1790..d67e8a8d9 100644
    --- a/ui/packages/consul-ui/app/search/predicates/service.js
    +++ b/ui/packages/consul-ui/app/search/predicates/service.js
    @@ -1,6 +1,6 @@
     export default {
    -  Name: item => item.Name,
    -  Tags: item => item.Tags || [],
    -  PeerName: item => item.PeerName,
    -  Partition: item => item.Partition,
    +  Name: (item) => item.Name,
    +  Tags: (item) => item.Tags || [],
    +  PeerName: (item) => item.PeerName,
    +  Partition: (item) => item.Partition,
     };
    diff --git a/ui/packages/consul-ui/app/search/predicates/token.js b/ui/packages/consul-ui/app/search/predicates/token.js
    index 87dc7a75e..027722906 100644
    --- a/ui/packages/consul-ui/app/search/predicates/token.js
    +++ b/ui/packages/consul-ui/app/search/predicates/token.js
    @@ -1,12 +1,12 @@
     export default {
    -  Name: item => item.Name,
    -  Description: item => item.Description,
    -  AccessorID: item => item.AccessorID,
    -  Role: item => (item.Roles || []).map(item => item.Name),
    -  Policy: item => {
    +  Name: (item) => item.Name,
    +  Description: (item) => item.Description,
    +  AccessorID: (item) => item.AccessorID,
    +  Role: (item) => (item.Roles || []).map((item) => item.Name),
    +  Policy: (item) => {
         return (item.Policies || [])
    -      .map(item => item.Name)
    -      .concat((item.ServiceIdentities || []).map(item => item.ServiceName))
    -      .concat((item.NodeIdentities || []).map(item => item.NodeName));
    +      .map((item) => item.Name)
    +      .concat((item.ServiceIdentities || []).map((item) => item.ServiceName))
    +      .concat((item.NodeIdentities || []).map((item) => item.NodeName));
       },
     };
    diff --git a/ui/packages/consul-ui/app/serializers/application.js b/ui/packages/consul-ui/app/serializers/application.js
    index dc5b31383..9bc5ef506 100644
    --- a/ui/packages/consul-ui/app/serializers/application.js
    +++ b/ui/packages/consul-ui/app/serializers/application.js
    @@ -14,17 +14,17 @@ import { NSPACE_KEY } from 'consul-ui/models/nspace';
     import { PARTITION_KEY } from 'consul-ui/models/partition';
     import createFingerprinter from 'consul-ui/utils/create-fingerprinter';
     
    -const map = function(obj, cb) {
    +const map = function (obj, cb) {
       if (!Array.isArray(obj)) {
         return [obj].map(cb)[0];
       }
       return obj.map(cb);
     };
     
    -const attachHeaders = function(headers, body, query = {}) {
    +const attachHeaders = function (headers, body, query = {}) {
       // lowercase everything incase we get browser inconsistencies
       const lower = {};
    -  Object.keys(headers).forEach(function(key) {
    +  Object.keys(headers).forEach(function (key) {
         lower[key.toLowerCase()] = headers[key];
       });
       //
    @@ -200,7 +200,7 @@ export default class ApplicationSerializer extends Serializer {
         }
         if (requestType === 'query') {
           meta.date = this.timestamp();
    -      payload.forEach(function(item) {
    +      payload.forEach(function (item) {
             set(item, 'SyncTime', meta.date);
           });
         }
    diff --git a/ui/packages/consul-ui/app/serializers/discovery-chain.js b/ui/packages/consul-ui/app/serializers/discovery-chain.js
    index e8faa8b00..f3a9783ec 100644
    --- a/ui/packages/consul-ui/app/serializers/discovery-chain.js
    +++ b/ui/packages/consul-ui/app/serializers/discovery-chain.js
    @@ -6,8 +6,8 @@ export default class DiscoveryChainSerializer extends Serializer {
       slugKey = SLUG_KEY;
     
       respondForQueryRecord(respond, query) {
    -    return super.respondForQueryRecord(function(cb) {
    -      return respond(function(headers, body) {
    +    return super.respondForQueryRecord(function (cb) {
    +      return respond(function (headers, body) {
             return cb(headers, {
               ...body,
               [SLUG_KEY]: body.Chain[SLUG_KEY],
    diff --git a/ui/packages/consul-ui/app/serializers/intention.js b/ui/packages/consul-ui/app/serializers/intention.js
    index 6fbd35ad9..5102ec341 100644
    --- a/ui/packages/consul-ui/app/serializers/intention.js
    +++ b/ui/packages/consul-ui/app/serializers/intention.js
    @@ -35,11 +35,11 @@ export default class IntentionSerializer extends Serializer {
     
       respondForQuery(respond, query) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               return cb(
                 headers,
    -            body.map(item => this.ensureID(item))
    +            body.map((item) => this.ensureID(item))
               );
             }),
           query
    @@ -48,7 +48,7 @@ export default class IntentionSerializer extends Serializer {
     
       respondForQueryRecord(respond, query) {
         return super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               body = this.ensureID(body);
               return cb(headers, body);
    diff --git a/ui/packages/consul-ui/app/serializers/kv.js b/ui/packages/consul-ui/app/serializers/kv.js
    index b7dd2eaf9..b3c711b8d 100644
    --- a/ui/packages/consul-ui/app/serializers/kv.js
    +++ b/ui/packages/consul-ui/app/serializers/kv.js
    @@ -16,7 +16,7 @@ export default class KvSerializer extends Serializer {
     
       respondForQueryRecord(respond, query) {
         return super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               // If item.Session is not set make sure we overwrite any existing one.
               // Using @replace, defaultValue or similar model apporaches does not work
    @@ -34,11 +34,11 @@ export default class KvSerializer extends Serializer {
     
       respondForQuery(respond, query) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               return cb(
                 headers,
    -            body.map(item => {
    +            body.map((item) => {
                   return {
                     [this.slugKey]: item,
                   };
    diff --git a/ui/packages/consul-ui/app/serializers/node.js b/ui/packages/consul-ui/app/serializers/node.js
    index 5b74d4601..139445884 100644
    --- a/ui/packages/consul-ui/app/serializers/node.js
    +++ b/ui/packages/consul-ui/app/serializers/node.js
    @@ -5,7 +5,7 @@ import { classify } from '@ember/string';
     
     // TODO: Looks like ID just isn't used at all consider just using .Node for
     // the SLUG_KEY
    -const fillSlug = function(item) {
    +const fillSlug = function (item) {
       if (item[SLUG_KEY] === '') {
         item[SLUG_KEY] = item['Node'];
       }
    @@ -28,10 +28,10 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix
         switch (relationship.key) {
           case 'Services':
             (item.Checks || [])
    -          .filter(item => {
    +          .filter((item) => {
                 return item.ServiceID !== '';
               })
    -          .forEach(item => {
    +          .forEach((item) => {
                 if (typeof checks[item.ServiceID] === 'undefined') {
                   checks[item.ServiceID] = [];
                 }
    @@ -41,7 +41,7 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix
               item.PeerName = undefined;
             }
             serializer = this.store.serializerFor(relationship.type);
    -        item.Services = item.Services.map(service =>
    +        item.Services = item.Services.map((service) =>
               serializer.transformHasManyResponseFromNode(item, service, checks)
             );
             return item;
    @@ -51,11 +51,11 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix
     
       respondForQuery(respond, query, data, modelClass) {
         const body = super.respondForQuery(
    -      cb => respond((headers, body) => cb(headers, body.map(fillSlug))),
    +      (cb) => respond((headers, body) => cb(headers, body.map(fillSlug))),
           query
         );
         modelClass.eachRelationship((key, relationship) => {
    -      body.forEach(item =>
    +      body.forEach((item) =>
             this[`transform${classify(relationship.kind)}Response`](
               this.store,
               relationship,
    @@ -69,7 +69,7 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix
     
       respondForQueryRecord(respond, query, data, modelClass) {
         const body = super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               return cb(headers, fillSlug(body));
             }),
    diff --git a/ui/packages/consul-ui/app/serializers/nspace.js b/ui/packages/consul-ui/app/serializers/nspace.js
    index f3be66551..f30fd469d 100644
    --- a/ui/packages/consul-ui/app/serializers/nspace.js
    +++ b/ui/packages/consul-ui/app/serializers/nspace.js
    @@ -2,9 +2,9 @@ import Serializer from './application';
     import { get } from '@ember/object';
     import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/nspace';
     
    -const normalizeACLs = item => {
    +const normalizeACLs = (item) => {
       if (get(item, 'ACLs.PolicyDefaults')) {
    -    item.ACLs.PolicyDefaults = item.ACLs.PolicyDefaults.map(function(item) {
    +    item.ACLs.PolicyDefaults = item.ACLs.PolicyDefaults.map(function (item) {
           if (typeof item.template === 'undefined') {
             item.template = '';
           }
    @@ -14,7 +14,7 @@ const normalizeACLs = item => {
       // Both of these might come though unset so we make sure we at least
       // have an empty array here so we can add children to them if we
       // need to whilst saving nspaces
    -  ['PolicyDefaults', 'RoleDefaults'].forEach(function(prop) {
    +  ['PolicyDefaults', 'RoleDefaults'].forEach(function (prop) {
         if (typeof item.ACLs === 'undefined') {
           item.ACLs = [];
         }
    @@ -31,11 +31,11 @@ export default class NspaceSerializer extends Serializer {
     
       respondForQuery(respond, query, data, modelClass) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) =>
               cb(
                 headers,
    -            body.map(function(item) {
    +            body.map(function (item) {
                   item.Namespace = '*';
                   item.Datacenter = query.dc;
                   return normalizeACLs(item);
    @@ -48,7 +48,7 @@ export default class NspaceSerializer extends Serializer {
     
       respondForQueryRecord(respond, serialized, data) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               body.Datacenter = serialized.dc;
               body.Namespace = '*';
    @@ -61,7 +61,7 @@ export default class NspaceSerializer extends Serializer {
     
       respondForCreateRecord(respond, serialized, data) {
         return super.respondForCreateRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               body.Datacenter = serialized.dc;
               body.Namespace = '*';
    diff --git a/ui/packages/consul-ui/app/serializers/oidc-provider.js b/ui/packages/consul-ui/app/serializers/oidc-provider.js
    index 996b8c7ea..406c9ae4d 100644
    --- a/ui/packages/consul-ui/app/serializers/oidc-provider.js
    +++ b/ui/packages/consul-ui/app/serializers/oidc-provider.js
    @@ -17,7 +17,7 @@ export default class OidcSerializer extends Serializer {
         // add the name and nspace here so we can merge this
         // TODO: Look to see if we always want the merging functionality
         return super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) =>
               cb(headers, {
                 Name: query.id,
    diff --git a/ui/packages/consul-ui/app/serializers/partition.js b/ui/packages/consul-ui/app/serializers/partition.js
    index 067d98a1b..32671a671 100644
    --- a/ui/packages/consul-ui/app/serializers/partition.js
    +++ b/ui/packages/consul-ui/app/serializers/partition.js
    @@ -7,11 +7,11 @@ export default class PartitionSerializer extends Serializer {
     
       respondForQuery(respond, query, data, modelClass) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               return cb(
                 headers,
    -            body.map(item => {
    +            body.map((item) => {
                   item.Partition = '*';
                   item.Namespace = '*';
                   return item;
    diff --git a/ui/packages/consul-ui/app/serializers/service-instance.js b/ui/packages/consul-ui/app/serializers/service-instance.js
    index 422012171..f0835064a 100644
    --- a/ui/packages/consul-ui/app/serializers/service-instance.js
    +++ b/ui/packages/consul-ui/app/serializers/service-instance.js
    @@ -61,7 +61,7 @@ export default class ServiceInstanceSerializer extends Serializer {
       }
     
       respondForQuery(respond, query) {
    -    const body = super.respondForQuery(cb => {
    +    const body = super.respondForQuery((cb) => {
           return respond((headers, body) => {
             if (body.length === 0) {
               const e = new Error();
    @@ -73,7 +73,7 @@ export default class ServiceInstanceSerializer extends Serializer {
               ];
               throw e;
             }
    -        body.forEach(item => {
    +        body.forEach((item) => {
               item.Datacenter = query.dc;
               item.Namespace = query.ns || 'default';
               item.Partition = query.partition || 'default';
    @@ -86,15 +86,15 @@ export default class ServiceInstanceSerializer extends Serializer {
       }
     
       respondForQueryRecord(respond, query) {
    -    return super.respondForQueryRecord(cb => {
    +    return super.respondForQueryRecord((cb) => {
           return respond((headers, body) => {
    -        body.forEach(item => {
    +        body.forEach((item) => {
               item.Datacenter = query.dc;
               item.Namespace = query.ns || 'default';
               item.Partition = query.partition || 'default';
               item.uid = this.extractUid(item);
             });
    -        body = body.find(function(item) {
    +        body = body.find(function (item) {
               return item.Node.Node === query.node && item.Service.ID === query.serviceId;
             });
             if (typeof body === 'undefined') {
    diff --git a/ui/packages/consul-ui/app/serializers/service.js b/ui/packages/consul-ui/app/serializers/service.js
    index 3d1c92901..9caa9b2cd 100644
    --- a/ui/packages/consul-ui/app/serializers/service.js
    +++ b/ui/packages/consul-ui/app/serializers/service.js
    @@ -8,28 +8,28 @@ export default class ServiceSerializer extends Serializer {
     
       respondForQuery(respond, query) {
         return super.respondForQuery(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               // Services and proxies all come together in the same list. Here we
               // map the proxies to their related services on a Service.Proxy
               // property for easy access later on
               const services = {};
               body
    -            .filter(function(item) {
    +            .filter(function (item) {
                   return item.Kind !== 'connect-proxy';
                 })
    -            .forEach(item => {
    +            .forEach((item) => {
                   services[item.Name] = item;
                 });
               body
    -            .filter(function(item) {
    +            .filter(function (item) {
                   return item.Kind === 'connect-proxy';
                 })
    -            .forEach(item => {
    +            .forEach((item) => {
                   // Iterating to cover the usecase of a proxy being used by more
                   // than one service
                   if (item.ProxyFor) {
    -                item.ProxyFor.forEach(service => {
    +                item.ProxyFor.forEach((service) => {
                       if (typeof services[service] !== 'undefined') {
                         services[service].Proxy = item;
                       }
    @@ -47,7 +47,7 @@ export default class ServiceSerializer extends Serializer {
         // Name is added here from the query, which is used to make the uid
         // Datacenter gets added in the ApplicationSerializer
         return super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               return cb(headers, {
                 Name: query.id,
    diff --git a/ui/packages/consul-ui/app/serializers/session.js b/ui/packages/consul-ui/app/serializers/session.js
    index b37fe6ff2..65a8d1995 100644
    --- a/ui/packages/consul-ui/app/serializers/session.js
    +++ b/ui/packages/consul-ui/app/serializers/session.js
    @@ -7,7 +7,7 @@ export default class SessionSerializer extends Serializer {
     
       respondForQueryRecord(respond, query) {
         return super.respondForQueryRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               if (body.length === 0) {
                 const e = new Error();
    diff --git a/ui/packages/consul-ui/app/serializers/token.js b/ui/packages/consul-ui/app/serializers/token.js
    index 2264b52b8..ee57cf00b 100644
    --- a/ui/packages/consul-ui/app/serializers/token.js
    +++ b/ui/packages/consul-ui/app/serializers/token.js
    @@ -38,7 +38,7 @@ export default class TokenSerializer extends Serializer.extend(WithPolicies, Wit
     
       respondForUpdateRecord(respond, serialized, data) {
         return super.respondForUpdateRecord(
    -      cb =>
    +      (cb) =>
             respond((headers, body) => {
               // Sometimes we get `Policies: null`, make null equal an empty array
               if (typeof body.Policies === 'undefined' || body.Policies === null) {
    diff --git a/ui/packages/consul-ui/app/serializers/topology.js b/ui/packages/consul-ui/app/serializers/topology.js
    index 05ff6a289..cb7274fa7 100644
    --- a/ui/packages/consul-ui/app/serializers/topology.js
    +++ b/ui/packages/consul-ui/app/serializers/topology.js
    @@ -10,16 +10,16 @@ export default class TopologySerializer extends Serializer {
     
       respondForQueryRecord(respond, query) {
         const intentionSerializer = this.store.serializerFor('intention');
    -    return super.respondForQueryRecord(function(cb) {
    -      return respond(function(headers, body) {
    -        body.Downstreams.forEach(item => {
    +    return super.respondForQueryRecord(function (cb) {
    +      return respond(function (headers, body) {
    +        body.Downstreams.forEach((item) => {
               item.Intention.SourceName = item.Name;
               item.Intention.SourceNS = item.Namespace;
               item.Intention.DestinationName = query.id;
               item.Intention.DestinationNS = query.ns || 'default';
               intentionSerializer.ensureID(item.Intention);
             });
    -        body.Upstreams.forEach(item => {
    +        body.Upstreams.forEach((item) => {
               item.Intention.SourceName = query.id;
               item.Intention.SourceNS = query.ns || 'default';
               item.Intention.DestinationName = item.Name;
    diff --git a/ui/packages/consul-ui/app/services/auth-providers/oauth2-code-with-url-provider.js b/ui/packages/consul-ui/app/services/auth-providers/oauth2-code-with-url-provider.js
    index 69aa83f74..fca77fe4b 100644
    --- a/ui/packages/consul-ui/app/services/auth-providers/oauth2-code-with-url-provider.js
    +++ b/ui/packages/consul-ui/app/services/auth-providers/oauth2-code-with-url-provider.js
    @@ -15,14 +15,14 @@ export default class OAuth2CodeWithURLProvider extends OAuth2CodeProvider {
           responseType = 'code';
         return this.get('popup')
           .open(url, responseParams, options)
    -      .then(function(authData) {
    +      .then(function (authData) {
             // the same as the parent class but with an authorizationState added
             const creds = {
               authorizationState: authData.state,
               authorizationCode: decodeURIComponent(authData[responseType]),
               provider: name,
             };
    -        runInDebug(_ =>
    +        runInDebug((_) =>
               console.info('Retrieved the following creds from the OAuth Provider', creds)
             );
             return creds;
    diff --git a/ui/packages/consul-ui/app/services/client/connections.js b/ui/packages/consul-ui/app/services/client/connections.js
    index 45e7cb725..2b4b3e546 100644
    --- a/ui/packages/consul-ui/app/services/client/connections.js
    +++ b/ui/packages/consul-ui/app/services/client/connections.js
    @@ -26,7 +26,7 @@ export default class ConnectionsService extends Service {
       addVisibilityChange() {
         // when the user hides the tab, abort all connections
         this._listeners.add(this.dom.document(), {
    -      visibilitychange: e => {
    +      visibilitychange: (e) => {
             if (e.target.hidden) {
               this.purge(-1);
             }
    @@ -39,9 +39,9 @@ export default class ConnectionsService extends Service {
         // any aborted errors should restart
         const doc = this.dom.document();
         if (doc.hidden) {
    -      return new Promise(resolve => {
    +      return new Promise((resolve) => {
             const remove = this._listeners.add(doc, {
    -          visibilitychange: function(event) {
    +          visibilitychange: function (event) {
                 remove();
                 // we resolve with the event that comes from
                 // whenAvailable not visibilitychange
    @@ -54,7 +54,7 @@ export default class ConnectionsService extends Service {
       }
     
       purge(statusCode = 0) {
    -    [...this.connections].forEach(function(connection) {
    +    [...this.connections].forEach(function (connection) {
           // Cancelled
           connection.abort(statusCode);
         });
    @@ -64,7 +64,7 @@ export default class ConnectionsService extends Service {
       acquire(request) {
         if (this.connections.size >= this.env.var('CONSUL_HTTP_MAX_CONNECTIONS')) {
           const closed = this.data.closed();
    -      let connection = [...this.connections].find(item => {
    +      let connection = [...this.connections].find((item) => {
             const id = item.headers()['x-request-id'];
             if (id) {
               return closed.includes(item.headers()['x-request-id']);
    diff --git a/ui/packages/consul-ui/app/services/client/http.js b/ui/packages/consul-ui/app/services/client/http.js
    index 6d3659c22..d36c70260 100644
    --- a/ui/packages/consul-ui/app/services/client/http.js
    +++ b/ui/packages/consul-ui/app/services/client/http.js
    @@ -15,8 +15,8 @@ import createHeaders from 'consul-ui/utils/http/create-headers';
     import createQueryParams from 'consul-ui/utils/http/create-query-params';
     
     // reopen EventSources if a user changes tab
    -export const restartWhenAvailable = function(client) {
    -  return function(e) {
    +export const restartWhenAvailable = function (client) {
    +  return function (e) {
         // setup the aborted connection restarting
         // this should happen here to avoid cache deletion
         const status = get(e, 'errors.firstObject.status');
    @@ -36,13 +36,13 @@ const QueryParams = {
     };
     const parseHeaders = createHeaders();
     
    -const parseBody = function(strs, ...values) {
    +const parseBody = function (strs, ...values) {
       let body = {};
    -  const doubleBreak = strs.reduce(function(prev, item, i) {
    +  const doubleBreak = strs.reduce(function (prev, item, i) {
         // Ensure each line has no whitespace either end, including empty lines
         item = item
           .split('\n')
    -      .map(item => item.trim())
    +      .map((item) => item.trim())
           .join('\n');
         if (item.indexOf('\n\n') !== -1) {
           return i;
    @@ -56,7 +56,7 @@ const parseBody = function(strs, ...values) {
         // matters slightly as we assumed post bodies would be an object.
         // This actually works as it just uses the value of the first object, if its an array
         // it concats
    -    body = values.splice(doubleBreak).reduce(function(prev, item, i) {
    +    body = values.splice(doubleBreak).reduce(function (prev, item, i) {
           switch (true) {
             case Array.isArray(item):
               if (i === 0) {
    @@ -89,7 +89,9 @@ export default class HttpService extends Service {
       init() {
         super.init(...arguments);
         this._listeners = this.dom.listeners();
    -    this.parseURL = createURL(encodeURIComponent, obj => QueryParams.stringify(this.sanitize(obj)));
    +    this.parseURL = createURL(encodeURIComponent, (obj) =>
    +      QueryParams.stringify(this.sanitize(obj))
    +    );
         const uriTag = this.encoder.uriTag();
         this.cache = (data, id) => {
           // interpolate the URI
    @@ -97,17 +99,15 @@ export default class HttpService extends Service {
           // save the time we received it for cache management purposes
           data.SyncTime = new Date().getTime();
           // save the data to the cache
    -      return this.store.push(
    -        {
    -          data: {
    -            id: data.uri,
    -            // the model is encoded as the protocol in the URI
    -            type: new URL(data.uri).protocol.slice(0, -1),
    -            attributes: data
    -          }
    -        }
    -      );
    -    }
    +      return this.store.push({
    +        data: {
    +          id: data.uri,
    +          // the model is encoded as the protocol in the URI
    +          type: new URL(data.uri).protocol.slice(0, -1),
    +          attributes: data,
    +        },
    +      });
    +    };
       }
     
       sanitize(obj) {
    @@ -162,7 +162,7 @@ export default class HttpService extends Service {
           data: body,
         };
         // Remove and save things that shouldn't be sent in the request
    -    params.clientHeaders = CLIENT_HEADERS.reduce(function(prev, item) {
    +    params.clientHeaders = CLIENT_HEADERS.reduce(function (prev, item) {
           if (typeof params.headers[item] !== 'undefined') {
             prev[item.toLowerCase()] = params.headers[item];
             delete params.headers[item];
    @@ -203,13 +203,15 @@ export default class HttpService extends Service {
         // also see adapters/kv content-types in requestForCreate/UpdateRecord
         // also see https://github.com/hashicorp/consul/issues/3804
         params.headers[CONTENT_TYPE] = 'application/json; charset=utf-8';
    +    params.url = `${this.env.var('CONSUL_API_PREFIX')}${params.url}`;
         return params;
       }
     
       fetchWithToken(path, params) {
    -    return this.settings.findBySlug('token').then(token => {
    -      return fetch(`${path}`, {
    +    return this.settings.findBySlug('token').then((token) => {
    +      return fetch(`${this.env.var('CONSUL_API_PREFIX')}${path}`, {
             ...params,
    +        credentials: 'include',
             headers: {
               'X-Consul-Token': typeof token.SecretID === 'undefined' ? '' : token.SecretID,
               ...params.headers,
    @@ -220,9 +222,9 @@ export default class HttpService extends Service {
       request(cb) {
         const client = this;
         const cache = this.cache;
    -    return cb(function(strs, ...values) {
    +    return cb(function (strs, ...values) {
           const params = client.requestParams(...arguments);
    -      return client.settings.findBySlug('token').then(token => {
    +      return client.settings.findBySlug('token').then((token) => {
             const options = {
               ...params,
               headers: {
    @@ -233,12 +235,12 @@ export default class HttpService extends Service {
             const request = client.transport.request(options);
             return new Promise((resolve, reject) => {
               const remove = client._listeners.add(request, {
    -            open: e => {
    +            open: (e) => {
                   client.acquire(e.target);
                 },
    -            message: e => {
    +            message: (e) => {
                   const headers = {
    -                ...Object.entries(e.data.headers).reduce(function(prev, [key, value], i) {
    +                ...Object.entries(e.data.headers).reduce(function (prev, [key, value], i) {
                       if (!CLIENT_HEADERS.includes(key)) {
                         prev[key] = value;
                       }
    @@ -255,23 +257,20 @@ export default class HttpService extends Service {
                     [CONSUL_NAMESPACE]: params.data.ns || token.Namespace || 'default',
                     [CONSUL_PARTITION]: params.data.partition || token.Partition || 'default',
                   };
    -              const respond = function(cb) {
    +              const respond = function (cb) {
                     let res = cb(headers, e.data.response, cache);
                     const meta = res.meta || {};
    -                if(meta.version === 2) {
    -                  if(Array.isArray(res.body)) {
    -                    res = new Proxy(
    -                      res.body,
    -                      {
    -                        get: (target, prop) => {
    -                          switch(prop) {
    -                            case 'meta':
    -                              return meta;
    -                          }
    -                          return target[prop];
    +                if (meta.version === 2) {
    +                  if (Array.isArray(res.body)) {
    +                    res = new Proxy(res.body, {
    +                      get: (target, prop) => {
    +                        switch (prop) {
    +                          case 'meta':
    +                            return meta;
                             }
    -                      }
    -                    );
    +                        return target[prop];
    +                      },
    +                    });
                       } else {
                         res = res.body;
                         res.meta = meta;
    @@ -281,10 +280,10 @@ export default class HttpService extends Service {
                   };
                   next(() => resolve(respond));
                 },
    -            error: e => {
    +            error: (e) => {
                   next(() => reject(e.error));
                 },
    -            close: e => {
    +            close: (e) => {
                   client.release(e.target);
                   remove();
                 },
    diff --git a/ui/packages/consul-ui/app/services/client/transports/xhr.js b/ui/packages/consul-ui/app/services/client/transports/xhr.js
    index 5040e6478..3c09e955f 100644
    --- a/ui/packages/consul-ui/app/services/client/transports/xhr.js
    +++ b/ui/packages/consul-ui/app/services/client/transports/xhr.js
    @@ -19,11 +19,11 @@ export default class XhrService extends Service {
         });
         const options = {
           ...params,
    -      beforeSend: function(xhr) {
    +      beforeSend: function (xhr) {
             request.open(xhr);
           },
           converters: {
    -        'text json': function(response) {
    +        'text json': function (response) {
               try {
                 return JSON.parse(response);
               } catch (e) {
    @@ -31,7 +31,7 @@ export default class XhrService extends Service {
               }
             },
           },
    -      success: function(headers, response, status, statusText) {
    +      success: function (headers, response, status, statusText) {
             // Response-ish
             request.respond({
               headers: headers,
    @@ -40,7 +40,7 @@ export default class XhrService extends Service {
               statusText: statusText,
             });
           },
    -      error: function(headers, response, status, statusText, err) {
    +      error: function (headers, response, status, statusText, err) {
             let error;
             if (err instanceof Error) {
               error = err;
    @@ -49,7 +49,7 @@ export default class XhrService extends Service {
             }
             request.error(error);
           },
    -      complete: function(status) {
    +      complete: function (status) {
             request.close();
           },
         };
    diff --git a/ui/packages/consul-ui/app/services/clipboard/local-storage.js b/ui/packages/consul-ui/app/services/clipboard/local-storage.js
    index a35774b5a..7e431c19b 100644
    --- a/ui/packages/consul-ui/app/services/clipboard/local-storage.js
    +++ b/ui/packages/consul-ui/app/services/clipboard/local-storage.js
    @@ -20,7 +20,7 @@ export default class LocalStorageService extends Service {
       key = 'clipboard';
     
       execute(trigger, options) {
    -    return new ClipboardCallback(trigger, options, val => {
    +    return new ClipboardCallback(trigger, options, (val) => {
           this.doc.defaultView.localStorage.setItem(this.key, val);
         });
       }
    diff --git a/ui/packages/consul-ui/app/services/data-sink/protocols/http.js b/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
    index 9a99eb796..ed32a1762 100644
    --- a/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
    +++ b/ui/packages/consul-ui/app/services/data-sink/protocols/http.js
    @@ -27,16 +27,12 @@ export default class HttpService extends Service {
       persist(sink, instance) {
         const [, , , , model] = sink.split('/');
         const repo = this[model];
    -    return this.client.request(
    -      request => repo.persist(instance, request)
    -    );
    +    return this.client.request((request) => repo.persist(instance, request));
       }
     
       remove(sink, instance) {
         const [, , , , model] = sink.split('/');
         const repo = this[model];
    -    return this.client.request(
    -      request => repo.remove(instance, request)
    -    );
    +    return this.client.request((request) => repo.remove(instance, request));
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/data-sink/service.js b/ui/packages/consul-ui/app/services/data-sink/service.js
    index 6e64c558c..007a55a10 100644
    --- a/ui/packages/consul-ui/app/services/data-sink/service.js
    +++ b/ui/packages/consul-ui/app/services/data-sink/service.js
    @@ -1,6 +1,6 @@
     import Service, { inject as service } from '@ember/service';
     
    -const parts = function(uri) {
    +const parts = function (uri) {
       uri = uri.toString();
       if (uri.indexOf('://') === -1) {
         uri = `consul://${uri}`;
    diff --git a/ui/packages/consul-ui/app/services/data-source/protocols/http.js b/ui/packages/consul-ui/app/services/data-source/protocols/http.js
    index 2e47db97a..9d01ce297 100644
    --- a/ui/packages/consul-ui/app/services/data-source/protocols/http.js
    +++ b/ui/packages/consul-ui/app/services/data-source/protocols/http.js
    @@ -9,11 +9,9 @@ export default class HttpService extends Service {
       source(src, configuration) {
         const route = match(src);
         let find;
    -    this.client.request(
    -      request => {
    -        find = route.cb(route.params, getOwner(this), request);
    -      }
    -    );
    +    this.client.request((request) => {
    +      find = route.cb(route.params, getOwner(this), request);
    +    });
         return this.type.source(find, configuration);
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/data-source/protocols/http/blocking.js b/ui/packages/consul-ui/app/services/data-source/protocols/http/blocking.js
    index b315d77e0..f329d770d 100644
    --- a/ui/packages/consul-ui/app/services/data-source/protocols/http/blocking.js
    +++ b/ui/packages/consul-ui/app/services/data-source/protocols/http/blocking.js
    @@ -21,7 +21,7 @@ export default class BlockingService extends Service {
           return maybeCall(deleteCursor, ifNotBlocking(this.settings))().then(() => {
             return find(configuration)
               .then(maybeCall(close, ifNotBlocking(this.settings)))
    -          .then(function(res = {}) {
    +          .then(function (res = {}) {
                 const meta = get(res, 'meta') || {};
                 if (typeof meta.cursor === 'undefined' && typeof meta.interval === 'undefined') {
                   close();
    diff --git a/ui/packages/consul-ui/app/services/data-source/protocols/local-storage.js b/ui/packages/consul-ui/app/services/data-source/protocols/local-storage.js
    index e7046c82f..0c2b0a62f 100644
    --- a/ui/packages/consul-ui/app/services/data-source/protocols/local-storage.js
    +++ b/ui/packages/consul-ui/app/services/data-source/protocols/local-storage.js
    @@ -8,7 +8,7 @@ export default class LocalStorageService extends Service {
       source(src, configuration) {
         const slug = src.split(':').pop();
         return new StorageEventSource(
    -      configuration => {
    +      (configuration) => {
             return this.repo.findBySlug(slug);
           },
           {
    diff --git a/ui/packages/consul-ui/app/services/data-source/service.js b/ui/packages/consul-ui/app/services/data-source/service.js
    index 0d4612557..c07ae9749 100644
    --- a/ui/packages/consul-ui/app/services/data-source/service.js
    +++ b/ui/packages/consul-ui/app/services/data-source/service.js
    @@ -44,7 +44,7 @@ export default class DataSourceService extends Service {
         // called on them, schedule any destroying to fire after the final render
         schedule('afterRender', () => {
           this._listeners.remove();
    -      sources.forEach(function(item) {
    +      sources.forEach(function (item) {
             item.close();
           });
           cache = null;
    @@ -61,14 +61,14 @@ export default class DataSourceService extends Service {
           const source = this.open(src, ref, true);
           source.configuration.ref = ref;
           const remove = this._listeners.add(source, {
    -        message: e => {
    +        message: (e) => {
               remove();
               // the source only gets wrapped in the proxy
               // after the first message
               // but the proxy itself is resolve to the route
               resolve(proxy(e.target, e.data));
             },
    -        error: e => {
    +        error: (e) => {
               remove();
               this.close(source, ref);
               reject(e.error);
    @@ -96,7 +96,7 @@ export default class DataSourceService extends Service {
         if (!(uri instanceof URI) && typeof uri !== 'string') {
           return this.unwrap(uri, ref);
         }
    -    runInDebug(_ => {
    +    runInDebug((_) => {
           if (!(uri instanceof URI)) {
             console.error(
               new Error(
    @@ -122,7 +122,7 @@ export default class DataSourceService extends Service {
           configuration.uri = uri;
           source = provider.source(pathname, configuration);
           const remove = this._listeners.add(source, {
    -        close: e => {
    +        close: (e) => {
               // a close could be fired either by:
               // 1. A non-blocking query leaving the page
               // 2. A non-blocking query responding
    @@ -135,7 +135,8 @@ export default class DataSourceService extends Service {
               if (
                 typeof event !== 'undefined' &&
                 typeof cursor !== 'undefined' &&
    -            e.errors && e.errors[0].status !== '401'
    +            e.errors &&
    +            e.errors[0].status !== '401'
               ) {
                 cache.set(uri, {
                   currentEvent: event,
    @@ -197,6 +198,6 @@ export default class DataSourceService extends Service {
           .filter(([key, item]) => {
             return item.readyState > 1;
           })
    -      .map(item => item[0]);
    +      .map((item) => item[0]);
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/dom.js b/ui/packages/consul-ui/app/services/dom.js
    index b0618e565..26d5a6613 100644
    --- a/ui/packages/consul-ui/app/services/dom.js
    +++ b/ui/packages/consul-ui/app/services/dom.js
    @@ -76,10 +76,10 @@ export default class DomService extends Service {
       setEventTargetProperty(e, property, cb) {
         const target = e.target;
         return new Proxy(e, {
    -      get: function(obj, prop, receiver) {
    +      get: function (obj, prop, receiver) {
             if (prop === 'target') {
               return new Proxy(target, {
    -            get: function(obj, prop, receiver) {
    +            get: function (obj, prop, receiver) {
                   if (prop === property) {
                     return cb(e.target[property]);
                   }
    @@ -95,10 +95,10 @@ export default class DomService extends Service {
       setEventTargetProperties(e, propObj) {
         const target = e.target;
         return new Proxy(e, {
    -      get: function(obj, prop, receiver) {
    +      get: function (obj, prop, receiver) {
             if (prop === 'target') {
               return new Proxy(target, {
    -            get: function(obj, prop, receiver) {
    +            get: function (obj, prop, receiver) {
                   if (typeof propObj[prop] !== 'undefined') {
                     return propObj[prop](e.target);
                   }
    @@ -156,10 +156,10 @@ export default class DomService extends Service {
     
       components(selector, context) {
         return [...this.elements(selector, context)]
    -      .map(function(item) {
    +      .map(function (item) {
             return $_(item);
           })
    -      .filter(function(item) {
    +      .filter(function (item) {
             return item != null;
           });
       }
    @@ -168,7 +168,7 @@ export default class DomService extends Service {
         inViewportCallbacks.set($el, cb);
         let observer = new IntersectionObserver(
           (entries, observer) => {
    -        entries.map(item => {
    +        entries.map((item) => {
               const cb = inViewportCallbacks.get(item.target);
               if (typeof cb === 'function') {
                 cb(item.isIntersecting);
    diff --git a/ui/packages/consul-ui/app/services/encoder.js b/ui/packages/consul-ui/app/services/encoder.js
    index 12b2f71d9..95ec59042 100644
    --- a/ui/packages/consul-ui/app/services/encoder.js
    +++ b/ui/packages/consul-ui/app/services/encoder.js
    @@ -4,7 +4,7 @@ import { runInDebug } from '@ember/debug';
     import atob from 'consul-ui/utils/atob';
     import btoa from 'consul-ui/utils/btoa';
     
    -const createRegExpEncoder = function(re, encoder = str => str, strict = true) {
    +const createRegExpEncoder = function (re, encoder = (str) => str, strict = true) {
       return (template = '', vars = {}) => {
         if (template !== '') {
           return template.replace(re, (match, group) => {
    @@ -43,11 +43,13 @@ export default class EncoderService extends Service {
         return this.tag(this.uriJoin.bind(this));
       }
     
    -  joiner = (encoder, joiner = '', defaultValue = '') => (values, strs) =>
    -    (strs || Array(values.length).fill(joiner)).reduce(
    -      (prev, item, i) => `${prev}${item}${encoder(values[i] || defaultValue)}`,
    -      ''
    -    );
    +  joiner =
    +    (encoder, joiner = '', defaultValue = '') =>
    +    (values, strs) =>
    +      (strs || Array(values.length).fill(joiner)).reduce(
    +        (prev, item, i) => `${prev}${item}${encoder(values[i] || defaultValue)}`,
    +        ''
    +      );
     
       tag(join) {
         return (strs, ...values) => join(values, strs);
    diff --git a/ui/packages/consul-ui/app/services/feedback.js b/ui/packages/consul-ui/app/services/feedback.js
    index 62adc01f4..06e9cdcf9 100644
    --- a/ui/packages/consul-ui/app/services/feedback.js
    +++ b/ui/packages/consul-ui/app/services/feedback.js
    @@ -3,10 +3,10 @@ import callableType from 'consul-ui/utils/callable-type';
     
     const TYPE_SUCCESS = 'success';
     const TYPE_ERROR = 'error';
    -const defaultStatus = function(type, obj) {
    +const defaultStatus = function (type, obj) {
       return type;
     };
    -const notificationDefaults = function() {
    +const notificationDefaults = function () {
       return {
         timeout: 6000,
         extendedTimeout: 300,
    @@ -19,8 +19,8 @@ export default class FeedbackService extends Service {
     
       notification(action, modelName) {
         return {
    -      success: item => this.success(item, action, undefined, modelName),
    -      error: e => this.error(e, action, undefined, modelName),
    +      success: (item) => this.success(item, action, undefined, modelName),
    +      error: (e) => this.error(e, action, undefined, modelName),
         };
       }
     
    diff --git a/ui/packages/consul-ui/app/services/form.js b/ui/packages/consul-ui/app/services/form.js
    index d580ec911..86de153c9 100644
    --- a/ui/packages/consul-ui/app/services/form.js
    +++ b/ui/packages/consul-ui/app/services/form.js
    @@ -39,10 +39,10 @@ export default class FormService extends Service {
           // only do special things for our new things for the moment
           if (name === 'role' || name === 'policy') {
             const repo = this[name];
    -        form.clear(function(obj) {
    +        form.clear(function (obj) {
               return repo.create(obj);
             });
    -        form.submit(function(obj) {
    +        form.submit(function (obj) {
               return repo.persist(obj);
             });
           }
    diff --git a/ui/packages/consul-ui/app/services/i18n-debug.js b/ui/packages/consul-ui/app/services/i18n-debug.js
    index 06c19c959..8b9dbe921 100644
    --- a/ui/packages/consul-ui/app/services/i18n-debug.js
    +++ b/ui/packages/consul-ui/app/services/i18n-debug.js
    @@ -5,7 +5,7 @@ import faker from 'faker';
     
     // we currently use HTML in translations, so anything 'word-like' with these
     // chars won't get translated
    -const translator = cb => item => (!['<', '>', '='].includes(item) ? cb(item) : item);
    +const translator = (cb) => (item) => !['<', '>', '='].includes(item) ? cb(item) : item;
     
     export default class DebugI18nService extends I18nService {
       formatMessage(value, formatOptions) {
    @@ -17,10 +17,10 @@ export default class DebugI18nService extends I18nService {
               return text
                 .split(' ')
                 .map(
    -              translator(item =>
    +              translator((item) =>
                     item
                       .split('')
    -                  .map(item => '-')
    +                  .map((item) => '-')
                       .join('')
                   )
                 )
    @@ -33,7 +33,7 @@ export default class DebugI18nService extends I18nService {
               return text
                 .split(' ')
                 .map(
    -              translator(item => {
    +              translator((item) => {
                     const word = faker.lorem.word();
                     return item.charAt(0) === item.charAt(0).toUpperCase() ? ucfirst(word) : word;
                   })
    @@ -56,4 +56,3 @@ export default class DebugI18nService extends I18nService {
         return formatOptions;
       }
     }
    -
    diff --git a/ui/packages/consul-ui/app/services/repository.js b/ui/packages/consul-ui/app/services/repository.js
    index 0b0df8e08..44d33ed5c 100644
    --- a/ui/packages/consul-ui/app/services/repository.js
    +++ b/ui/packages/consul-ui/app/services/repository.js
    @@ -70,7 +70,7 @@ export default class RepositoryService extends Service {
         // inspect the permissions for this segment/slug remotely, if we have zero
         // permissions fire a fake 403 so we don't even request the model/resource
         if (params.resources.length > 0) {
    -      const resource = params.resources.find(item => item.Access === access);
    +      const resource = params.resources.find((item) => item.Access === access);
           if (resource && resource.Allow === false) {
             // TODO: Here we temporarily make a hybrid HTTPError/ember-data HTTP error
             // we should eventually use HTTPError's everywhere
    @@ -114,7 +114,7 @@ export default class RepositoryService extends Service {
       reconcile(meta = {}, params = {}, configuration = {}) {
         // unload anything older than our current sync date/time
         if (typeof meta.date !== 'undefined') {
    -      this.store.peekAll(this.getModelName()).forEach(item => {
    +      this.store.peekAll(this.getModelName()).forEach((item) => {
             const date = get(item, 'SyncTime');
             if (
               !item.isDeleted &&
    @@ -138,7 +138,7 @@ export default class RepositoryService extends Service {
     
       cached(params) {
         const entries = Object.entries(params);
    -    return this.store.peekAll(this.getModelName()).filter(item => {
    +    return this.store.peekAll(this.getModelName()).filter((item) => {
           return entries.every(([key, value]) => item[key] === value);
         });
       }
    @@ -230,7 +230,7 @@ export default class RepositoryService extends Service {
         if (typeOf(item) === 'object') {
           item = this.store.peekRecord(this.getModelName(), item[this.getPrimaryKey()]);
         }
    -    return item.destroyRecord().then(item => {
    +    return item.destroyRecord().then((item) => {
           return this.store.unloadRecord(item);
         });
       }
    diff --git a/ui/packages/consul-ui/app/services/repository/dc.js b/ui/packages/consul-ui/app/services/repository/dc.js
    index 59887967e..fa896c627 100644
    --- a/ui/packages/consul-ui/app/services/repository/dc.js
    +++ b/ui/packages/consul-ui/app/services/repository/dc.js
    @@ -2,10 +2,7 @@ import Error from '@ember/error';
     import { inject as service } from '@ember/service';
     import RepositoryService from 'consul-ui/services/repository';
     import dataSource from 'consul-ui/decorators/data-source';
    -import {
    -  HEADERS_DEFAULT_ACL_POLICY as DEFAULT_ACL_POLICY,
    -} from 'consul-ui/utils/http/consul';
    -
    +import { HEADERS_DEFAULT_ACL_POLICY as DEFAULT_ACL_POLICY } from 'consul-ui/utils/http/consul';
     
     const SECONDS = 1000;
     const MODEL_NAME = 'dc';
    @@ -14,25 +11,22 @@ const zero = {
       Total: 0,
       Passing: 0,
       Warning: 0,
    -  Critical: 0
    +  Critical: 0,
     };
     const aggregate = (prev, body, type) => {
    -
       return body[type].reduce((prev, item) => {
    -
         // for each Partitions, Namespaces
    -    ['Partition', 'Namespace'].forEach(bucket => {
    -
    +    ['Partition', 'Namespace'].forEach((bucket) => {
           // lazily initialize
           let obj = prev[bucket][item[bucket]];
    -      if(typeof obj === 'undefined') {
    +      if (typeof obj === 'undefined') {
             obj = prev[bucket][item[bucket]] = {
               Name: item[bucket],
             };
           }
    -      if(typeof obj[type] === 'undefined') {
    +      if (typeof obj[type] === 'undefined') {
             obj[type] = {
    -          ...zero
    +          ...zero,
             };
           }
           //
    @@ -42,7 +36,6 @@ const aggregate = (prev, body, type) => {
           obj[type].Passing += item.Passing;
           obj[type].Warning += item.Warning;
           obj[type].Critical += item.Critical;
    -
         });
     
         // also aggregate the Datacenter, without doubling up
    @@ -53,8 +46,7 @@ const aggregate = (prev, body, type) => {
         prev.Datacenter[type].Critical += item.Critical;
         return prev;
       }, prev);
    -}
    -
    +};
     
     export default class DcService extends RepositoryService {
       @service('env') env;
    @@ -64,141 +56,141 @@ export default class DcService extends RepositoryService {
       }
     
       @dataSource('/:partition/:ns/:dc/datacenters')
    -  async fetchAll({partition, ns, dc}, { uri }, request) {
    +  async fetchAll({ partition, ns, dc }, { uri }, request) {
         const Local = this.env.var('CONSUL_DATACENTER_LOCAL');
         const Primary = this.env.var('CONSUL_DATACENTER_PRIMARY');
    -    return (await request`
    +    return (
    +      await request`
           GET /v1/catalog/datacenters
           X-Request-ID: ${uri}
    -    `)(
    -      (headers, body, cache) => {
    -        // TODO: Not sure nowadays whether we need to keep lowercasing everything
    -        // I vaguely remember when I last looked it was not needed for browsers anymore
    -        // but I also vaguely remember something about Pretender lowercasing things still
    -        // so if we can work around Pretender I think we can remove all the header lowercasing
    -        // For the moment we lowercase here so as to not effect the ember-data-flavoured-v1 fork
    -        const entry = Object.entries(headers)
    -          .find(([key, value]) => key.toLowerCase() === DEFAULT_ACL_POLICY.toLowerCase());
    -        //
    -        const DefaultACLPolicy = entry[1] || 'allow';
    -        return {
    -          meta: {
    -            version: 2,
    -            uri: uri,
    -          },
    -          body: body.map(dc => {
    -            return cache(
    -              {
    -                Name: dc,
    -                Datacenter: '',
    -                Local: dc === Local,
    -                Primary: dc === Primary,
    -                DefaultACLPolicy: DefaultACLPolicy,
    -              },
    -              uri => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter`
    -            );
    -          })
    -        };
    -      });
    +    `
    +    )((headers, body, cache) => {
    +      // TODO: Not sure nowadays whether we need to keep lowercasing everything
    +      // I vaguely remember when I last looked it was not needed for browsers anymore
    +      // but I also vaguely remember something about Pretender lowercasing things still
    +      // so if we can work around Pretender I think we can remove all the header lowercasing
    +      // For the moment we lowercase here so as to not effect the ember-data-flavoured-v1 fork
    +      const entry = Object.entries(headers).find(
    +        ([key, value]) => key.toLowerCase() === DEFAULT_ACL_POLICY.toLowerCase()
    +      );
    +      //
    +      const DefaultACLPolicy = entry[1] || 'allow';
    +      return {
    +        meta: {
    +          version: 2,
    +          uri: uri,
    +        },
    +        body: body.map((dc) => {
    +          return cache(
    +            {
    +              Name: dc,
    +              Datacenter: '',
    +              Local: dc === Local,
    +              Primary: dc === Primary,
    +              DefaultACLPolicy: DefaultACLPolicy,
    +            },
    +            (uri) => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter`
    +          );
    +        }),
    +      };
    +    });
       }
     
       @dataSource('/:partition/:ns/:dc/datacenter')
    -  async fetch({partition, ns, dc}, { uri }, request) {
    -    return (await request`
    +  async fetch({ partition, ns, dc }, { uri }, request) {
    +    return (
    +      await request`
           GET /v1/operator/autopilot/state?${{ dc }}
           X-Request-ID: ${uri}
    -    `)(
    -      (headers, body, cache) => {
    -        // turn servers into an array instead of a map/object
    -        const servers = Object.values(body.Servers);
    -        const grouped = [];
    -        return {
    -          meta: {
    -            version: 2,
    -            uri: uri,
    -          },
    -          body: cache(
    -            {
    -              ...body,
    -              // all servers
    -              Servers: servers,
    -              RedundancyZones: Object.entries(body.RedundancyZones || {}).map(([key, value]) => {
    -                const zone = {
    -                  ...value,
    -                  Name: key,
    -                  Healthy: true,
    -                  // convert the string[] to Server[]
    -                  Servers: value.Servers.reduce((prev, item) => {
    -                    const server = body.Servers[item];
    -                    // keep a record of things
    -                    grouped.push(server.ID);
    -                    prev.push(server);
    -                    return prev;
    -                  }, []),
    -                }
    -                return zone;
    -              }),
    -              ReadReplicas: (body.ReadReplicas || []).map(item => {
    -                // keep a record of things
    -                grouped.push(item);
    -                return body.Servers[item];
    -              }),
    -              Default: {
    -                Servers: servers.filter(item => !grouped.includes(item.ID))
    -              }
    +    `
    +    )((headers, body, cache) => {
    +      // turn servers into an array instead of a map/object
    +      const servers = Object.values(body.Servers);
    +      const grouped = [];
    +      return {
    +        meta: {
    +          version: 2,
    +          uri: uri,
    +        },
    +        body: cache(
    +          {
    +            ...body,
    +            // all servers
    +            Servers: servers,
    +            RedundancyZones: Object.entries(body.RedundancyZones || {}).map(([key, value]) => {
    +              const zone = {
    +                ...value,
    +                Name: key,
    +                Healthy: true,
    +                // convert the string[] to Server[]
    +                Servers: value.Servers.reduce((prev, item) => {
    +                  const server = body.Servers[item];
    +                  // keep a record of things
    +                  grouped.push(server.ID);
    +                  prev.push(server);
    +                  return prev;
    +                }, []),
    +              };
    +              return zone;
    +            }),
    +            ReadReplicas: (body.ReadReplicas || []).map((item) => {
    +              // keep a record of things
    +              grouped.push(item);
    +              return body.Servers[item];
    +            }),
    +            Default: {
    +              Servers: servers.filter((item) => !grouped.includes(item.ID)),
                 },
    -            uri => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter`
    -          )
    -        }
    -      }
    -    );
    +          },
    +          (uri) => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter`
    +        ),
    +      };
    +    });
       }
     
       @dataSource('/:partition/:ns/:dc/catalog/health')
    -  async fetchCatalogHealth({partition, ns, dc}, { uri }, request) {
    -    return (await request`
    +  async fetchCatalogHealth({ partition, ns, dc }, { uri }, request) {
    +    return (
    +      await request`
           GET /v1/internal/ui/catalog-overview?${{ dc, stale: null }}
           X-Request-ID: ${uri}
    -    `)(
    -      (headers, body, cache) => {
    -
    -
    -        // for each Services/Nodes/Checks aggregate
    -        const agg = ['Nodes', 'Services', 'Checks']
    -          .reduce((prev, item) => aggregate(prev, body, item), {
    -            Datacenter: {
    -              Name: dc,
    -              Nodes: {
    -                ...zero
    -              },
    -              Services: {
    -                ...zero
    -              },
    -              Checks: {
    -                ...zero
    -              }
    +    `
    +    )((headers, body, cache) => {
    +      // for each Services/Nodes/Checks aggregate
    +      const agg = ['Nodes', 'Services', 'Checks'].reduce(
    +        (prev, item) => aggregate(prev, body, item),
    +        {
    +          Datacenter: {
    +            Name: dc,
    +            Nodes: {
    +              ...zero,
    +            },
    +            Services: {
    +              ...zero,
    +            },
    +            Checks: {
    +              ...zero,
                 },
    -            Partition: {},
    -            Namespace: {}
    -          });
    -
    -
    -        return {
    -          meta: {
    -            version: 2,
    -            uri: uri,
    -            interval: 30 * SECONDS
               },
    -          body: {
    -            Datacenter: agg.Datacenter,
    -            Partitions: Object.values(agg.Partition),
    -            Namespaces: Object.values(agg.Namespace),
    -            ...body
    -          }
    -        };
    -      }
    +          Partition: {},
    +          Namespace: {},
    +        }
    +      );
     
    -    );
    +      return {
    +        meta: {
    +          version: 2,
    +          uri: uri,
    +          interval: 30 * SECONDS,
    +        },
    +        body: {
    +          Datacenter: agg.Datacenter,
    +          Partitions: Object.values(agg.Partition),
    +          Namespaces: Object.values(agg.Namespace),
    +          ...body,
    +        },
    +      };
    +    });
       }
     
       @dataSource('/:partition/:ns/:dc/datacenter-cache/:name')
    @@ -214,5 +206,4 @@ export default class DcService extends RepositoryService {
         }
         return item;
       }
    -
     }
    diff --git a/ui/packages/consul-ui/app/services/repository/discovery-chain.js b/ui/packages/consul-ui/app/services/repository/discovery-chain.js
    index 3d2e2207b..dd8e6267b 100644
    --- a/ui/packages/consul-ui/app/services/repository/discovery-chain.js
    +++ b/ui/packages/consul-ui/app/services/repository/discovery-chain.js
    @@ -21,7 +21,7 @@ export default class DiscoveryChainService extends RepositoryService {
         if (typeof datacenter !== 'undefined' && !get(datacenter, 'MeshEnabled')) {
           return Promise.resolve();
         }
    -    return super.findBySlug(...arguments).catch(e => {
    +    return super.findBySlug(...arguments).catch((e) => {
           const code = get(e, 'errors.firstObject.status');
           const body = (get(e, 'errors.firstObject.detail') || '').trim();
           switch (code) {
    diff --git a/ui/packages/consul-ui/app/services/repository/intention.js b/ui/packages/consul-ui/app/services/repository/intention.js
    index 89b211e16..012c4c448 100644
    --- a/ui/packages/consul-ui/app/services/repository/intention.js
    +++ b/ui/packages/consul-ui/app/services/repository/intention.js
    @@ -30,7 +30,7 @@ export default class IntentionRepository extends RepositoryService {
           this.managedByCRDs = this.store
             .peekAll(this.getModelName())
             .toArray()
    -        .some(item => item.IsManagedByCRD);
    +        .some((item) => item.IsManagedByCRD);
         }
         return this.managedByCRDs;
       }
    diff --git a/ui/packages/consul-ui/app/services/repository/kv.js b/ui/packages/consul-ui/app/services/repository/kv.js
    index 9f771c061..68bb6ec52 100644
    --- a/ui/packages/consul-ui/app/services/repository/kv.js
    +++ b/ui/packages/consul-ui/app/services/repository/kv.js
    @@ -79,7 +79,7 @@ export default class KvService extends RepositoryService {
         //   async () => {
         let items = await this.findAll(...arguments);
         const meta = items.meta;
    -    items = items.filter(item => params.id !== get(item, 'Key'));
    +    items = items.filter((item) => params.id !== get(item, 'Key'));
         items.meta = meta;
         return items;
         // },
    diff --git a/ui/packages/consul-ui/app/services/repository/license.js b/ui/packages/consul-ui/app/services/repository/license.js
    index c9cbe52c8..d53f00888 100644
    --- a/ui/packages/consul-ui/app/services/repository/license.js
    +++ b/ui/packages/consul-ui/app/services/repository/license.js
    @@ -3,35 +3,35 @@ import dataSource from 'consul-ui/decorators/data-source';
     
     const MODEL_NAME = 'license';
     
    -const bucket = function(item, { dc, ns = 'default', partition = 'default' }) {
    +const bucket = function (item, { dc, ns = 'default', partition = 'default' }) {
       return {
         ...item,
         Datacenter: dc,
         Namespace: typeof item.Namespace === 'undefined' ? ns : item.Namespace,
         Partition: typeof item.Partition === 'undefined' ? partition : item.Partition,
       };
    -}
    +};
     
     const SECONDS = 1000;
     
     export default class LicenseService extends RepositoryService {
       @dataSource('/:partition/:ns/:dc/license')
    -  async find({partition, ns, dc}, { uri }, request) {
    -    return (await request`
    +  async find({ partition, ns, dc }, { uri }, request) {
    +    return (
    +      await request`
           GET /v1/operator/license?${{ dc }}
           X-Request-ID: ${uri}
    -    `)(
    -      (headers, body, cache) => ({
    -        meta: {
    -          version: 2,
    -          uri: uri,
    -          interval: 30 * SECONDS
    -        },
    -        body: cache(
    -          bucket(body, { dc }),
    -          uri => uri`${MODEL_NAME}:///${partition}/${ns}/${dc}/license/${body.License.license_id}`
    -        )
    -      })
    -    );
    +    `
    +    )((headers, body, cache) => ({
    +      meta: {
    +        version: 2,
    +        uri: uri,
    +        interval: 30 * SECONDS,
    +      },
    +      body: cache(
    +        bucket(body, { dc }),
    +        (uri) => uri`${MODEL_NAME}:///${partition}/${ns}/${dc}/license/${body.License.license_id}`
    +      ),
    +    }));
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/repository/metrics.js b/ui/packages/consul-ui/app/services/repository/metrics.js
    index bb3171e35..125209821 100644
    --- a/ui/packages/consul-ui/app/services/repository/metrics.js
    +++ b/ui/packages/consul-ui/app/services/repository/metrics.js
    @@ -60,7 +60,7 @@ export default class MetricsService extends RepositoryService {
             {}
           ),
         ];
    -    return Promise.all(promises).then(results => {
    +    return Promise.all(promises).then((results) => {
           return {
             meta: {
               interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
    @@ -78,7 +78,7 @@ export default class MetricsService extends RepositoryService {
         }
         return this.provider
           .upstreamRecentSummaryStats(params.slug, params.dc, params.ns, {})
    -      .then(result => {
    +      .then((result) => {
             result.meta = {
               interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
             };
    @@ -93,7 +93,7 @@ export default class MetricsService extends RepositoryService {
         }
         return this.provider
           .downstreamRecentSummaryStats(params.slug, params.dc, params.ns, {})
    -      .then(result => {
    +      .then((result) => {
             result.meta = {
               interval: this.env.var('CONSUL_METRICS_POLL_INTERVAL') || 10000,
             };
    diff --git a/ui/packages/consul-ui/app/services/repository/nspace.js b/ui/packages/consul-ui/app/services/repository/nspace.js
    index 51fac2f92..4bb266ba3 100644
    --- a/ui/packages/consul-ui/app/services/repository/nspace.js
    +++ b/ui/packages/consul-ui/app/services/repository/nspace.js
    @@ -6,16 +6,16 @@ import dataSource from 'consul-ui/decorators/data-source';
     
     import { defaultChangeset as changeset } from 'consul-ui/utils/form/builder';
     
    -const findActiveNspace = function(nspaces, nspace) {
    -  let found = nspaces.find(function(item) {
    +const findActiveNspace = function (nspaces, nspace) {
    +  let found = nspaces.find(function (item) {
         return item.Name === nspace.Name;
       });
       if (typeof found === 'undefined') {
    -    runInDebug(_ =>
    -      console.info(`${nspace.Name} not found in [${nspaces.map(item => item.Name).join(', ')}]`)
    +    runInDebug((_) =>
    +      console.info(`${nspace.Name} not found in [${nspaces.map((item) => item.Name).join(', ')}]`)
         );
         // if we can't find the nspace that was specified try default
    -    found = nspaces.find(function(item) {
    +    found = nspaces.find(function (item) {
           return item.Name === 'default';
         });
         // if there is no default just choose the first
    @@ -87,7 +87,7 @@ export default class NspaceService extends RepositoryService {
             },
           ]);
         }
    -    return this.store.authorize(this.getModelName(), { dc: dc, ns: nspace }).catch(function(e) {
    +    return this.store.authorize(this.getModelName(), { dc: dc, ns: nspace }).catch(function (e) {
           return [];
         });
       }
    diff --git a/ui/packages/consul-ui/app/services/repository/oidc-provider.js b/ui/packages/consul-ui/app/services/repository/oidc-provider.js
    index 2fbd1be31..c44960564 100644
    --- a/ui/packages/consul-ui/app/services/repository/oidc-provider.js
    +++ b/ui/packages/consul-ui/app/services/repository/oidc-provider.js
    @@ -82,7 +82,7 @@ export default class OidcProviderService extends RepositoryService {
       findCodeByURL(src) {
         // TODO: Maybe move this to the provider itself
         set(this.provider, 'baseUrl', src);
    -    return this.manager.open(OAUTH_PROVIDER_NAME, {}).catch(e => {
    +    return this.manager.open(OAUTH_PROVIDER_NAME, {}).catch((e) => {
           let err;
           switch (true) {
             case e.message.startsWith('remote was closed'):
    diff --git a/ui/packages/consul-ui/app/services/repository/partition.js b/ui/packages/consul-ui/app/services/repository/partition.js
    index 118e7e2be..53fb6d665 100644
    --- a/ui/packages/consul-ui/app/services/repository/partition.js
    +++ b/ui/packages/consul-ui/app/services/repository/partition.js
    @@ -6,16 +6,16 @@ import dataSource from 'consul-ui/decorators/data-source';
     
     import { defaultChangeset as changeset } from 'consul-ui/utils/form/builder';
     
    -const findActive = function(items, item) {
    -  let found = items.find(function(i) {
    +const findActive = function (items, item) {
    +  let found = items.find(function (i) {
         return i.Name === item.Name;
       });
       if (typeof found === 'undefined') {
    -    runInDebug(_ =>
    -      console.info(`${item.Name} not found in [${items.map(item => item.Name).join(', ')}]`)
    +    runInDebug((_) =>
    +      console.info(`${item.Name} not found in [${items.map((item) => item.Name).join(', ')}]`)
         );
         // if we can't find the nspace that was specified try default
    -    found = items.find(function(item) {
    +    found = items.find(function (item) {
           return item.Name === 'default';
         });
         // if there is no default just choose the first
    diff --git a/ui/packages/consul-ui/app/services/repository/peer.js b/ui/packages/consul-ui/app/services/repository/peer.js
    index 5f66708e2..961fcd79b 100644
    --- a/ui/packages/consul-ui/app/services/repository/peer.js
    +++ b/ui/packages/consul-ui/app/services/repository/peer.js
    @@ -37,14 +37,14 @@ export default class PeerService extends RepositoryService {
               interval: 10000,
               uri: uri,
             },
    -        body: body.map(item => {
    +        body: body.map((item) => {
               return cache(
                 {
                   ...item,
                   Datacenter: dc,
                   Partition: partition,
                 },
    -            uri => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
    +            (uri) => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
               );
             }),
           };
    @@ -61,7 +61,7 @@ export default class PeerService extends RepositoryService {
             Namespace: '',
             Partition: partition,
           });
    -      item.meta = {cacheControl: 'no-store'};
    +      item.meta = { cacheControl: 'no-store' };
           return item;
         }
         return (
    @@ -85,7 +85,7 @@ export default class PeerService extends RepositoryService {
                 Datacenter: dc,
                 Partition: partition,
               },
    -          uri => uri`peer:///${partition}/${ns}/${dc}/peer/${body.Name}`
    +          (uri) => uri`peer:///${partition}/${ns}/${dc}/peer/${body.Name}`
             ),
           };
         });
    @@ -117,7 +117,7 @@ export default class PeerService extends RepositoryService {
                 ...item,
                 State: 'ESTABLISHING',
               },
    -          uri => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
    +          (uri) => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
             ),
           };
         });
    @@ -145,7 +145,7 @@ export default class PeerService extends RepositoryService {
                 ...item,
                 State: 'DELETING',
               },
    -          uri => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
    +          (uri) => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
             ),
           };
         });
    diff --git a/ui/packages/consul-ui/app/services/repository/permission.js b/ui/packages/consul-ui/app/services/repository/permission.js
    index 8b15ddd1b..8a99ad0c3 100644
    --- a/ui/packages/consul-ui/app/services/repository/permission.js
    +++ b/ui/packages/consul-ui/app/services/repository/permission.js
    @@ -83,8 +83,8 @@ export default class PermissionService extends RepositoryService {
     
       has(permission) {
         const keys = Object.keys(permission);
    -    return this.permissions.some(item => {
    -      return keys.every(key => item[key] === permission[key]) && item.Allow === true;
    +    return this.permissions.some((item) => {
    +      return keys.every((key) => item[key] === permission[key]) && item.Allow === true;
         });
       }
     
    @@ -114,7 +114,7 @@ export default class PermissionService extends RepositoryService {
        */
       async authorize(params) {
         if (!this.env.var('CONSUL_ACLS_ENABLED')) {
    -      return params.resources.map(item => {
    +      return params.resources.map((item) => {
             return {
               ...item,
               Allow: true,
    @@ -164,7 +164,7 @@ export default class PermissionService extends RepositoryService {
         // still enforced on the backend.
         // This temporary measure should be removed again once https://github.com/hashicorp/consul/issues/11098
         // has been resolved
    -    this.permissions.forEach(item => {
    +    this.permissions.forEach((item) => {
           if (['key', 'node', 'service', 'intention', 'session'].includes(item.Resource)) {
             item.Allow = true;
           }
    diff --git a/ui/packages/consul-ui/app/services/repository/policy.js b/ui/packages/consul-ui/app/services/repository/policy.js
    index 4fdbfd811..a14b8a71f 100644
    --- a/ui/packages/consul-ui/app/services/repository/policy.js
    +++ b/ui/packages/consul-ui/app/services/repository/policy.js
    @@ -37,10 +37,7 @@ export default class PolicyService extends RepositoryService {
         } else {
           item = await super.findBySlug(...arguments);
         }
    -    return this.form
    -      .form(this.getModelName())
    -      .setData(item)
    -      .getData();
    +    return this.form.form(this.getModelName()).setData(item).getData();
       }
     
       persist(item) {
    diff --git a/ui/packages/consul-ui/app/services/repository/proxy.js b/ui/packages/consul-ui/app/services/repository/proxy.js
    index 4032945aa..3c91e1a37 100644
    --- a/ui/packages/consul-ui/app/services/repository/proxy.js
    +++ b/ui/packages/consul-ui/app/services/repository/proxy.js
    @@ -19,8 +19,8 @@ export default class ProxyService extends RepositoryService {
           params.index = configuration.cursor;
           params.uri = configuration.uri;
         }
    -    return this.store.query(this.getModelName(), params).then(items => {
    -      items.forEach(item => {
    +    return this.store.query(this.getModelName(), params).then((items) => {
    +      items.forEach((item) => {
             // swap out the id for the services id
             // so we can then assign the proxy to it if it exists
             const id = JSON.parse(item.uid);
    diff --git a/ui/packages/consul-ui/app/services/repository/role.js b/ui/packages/consul-ui/app/services/repository/role.js
    index 9fe0f7ea3..792ca80db 100644
    --- a/ui/packages/consul-ui/app/services/repository/role.js
    +++ b/ui/packages/consul-ui/app/services/repository/role.js
    @@ -36,9 +36,6 @@ export default class RoleService extends RepositoryService {
         } else {
           item = await super.findBySlug(...arguments);
         }
    -    return this.form
    -      .form(this.getModelName())
    -      .setData(item)
    -      .getData();
    +    return this.form.form(this.getModelName()).setData(item).getData();
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/repository/service-instance.js b/ui/packages/consul-ui/app/services/repository/service-instance.js
    index 93fd3f64d..8680e62a7 100644
    --- a/ui/packages/consul-ui/app/services/repository/service-instance.js
    +++ b/ui/packages/consul-ui/app/services/repository/service-instance.js
    @@ -20,7 +20,7 @@ export default class ServiceInstanceService extends RepositoryService {
           params.uri = configuration.uri;
         }
         return this.authorizeBySlug(
    -      async resources => {
    +      async (resources) => {
             const instances = await this.query(params);
             set(instances, 'firstObject.Service.Resources', resources);
             return instances;
    diff --git a/ui/packages/consul-ui/app/services/repository/token.js b/ui/packages/consul-ui/app/services/repository/token.js
    index 1497140e4..eb6d670f4 100644
    --- a/ui/packages/consul-ui/app/services/repository/token.js
    +++ b/ui/packages/consul-ui/app/services/repository/token.js
    @@ -37,10 +37,7 @@ export default class TokenService extends RepositoryService {
         } else {
           item = await super.findBySlug(...arguments);
         }
    -    return this.form
    -      .form(this.getModelName())
    -      .setData(item)
    -      .getData();
    +    return this.form.form(this.getModelName()).setData(item).getData();
       }
     
       @dataSource('/:partition/:ns/:dc/token/self/:secret')
    @@ -52,7 +49,7 @@ export default class TokenService extends RepositoryService {
             secret: params.secret,
             dc: params.dc,
           })
    -      .catch(e => {
    +      .catch((e) => {
             return Promise.reject(e);
           });
       }
    diff --git a/ui/packages/consul-ui/app/services/repository/topology.js b/ui/packages/consul-ui/app/services/repository/topology.js
    index 3f9b30430..17dfd704f 100644
    --- a/ui/packages/consul-ui/app/services/repository/topology.js
    +++ b/ui/packages/consul-ui/app/services/repository/topology.js
    @@ -24,7 +24,7 @@ export default class TopologyService extends RepositoryService {
           params.index = configuration.cursor;
           params.uri = configuration.uri;
         }
    -    return this.store.queryRecord(this.getModelName(), params).catch(e => {
    +    return this.store.queryRecord(this.getModelName(), params).catch((e) => {
           const code = get(e, 'errors.firstObject.status');
           const body = (get(e, 'errors.firstObject.detail') || '').trim();
           switch (code) {
    diff --git a/ui/packages/consul-ui/app/services/routlet.js b/ui/packages/consul-ui/app/services/routlet.js
    index 2fb805b54..adec9bed8 100644
    --- a/ui/packages/consul-ui/app/services/routlet.js
    +++ b/ui/packages/consul-ui/app/services/routlet.js
    @@ -65,7 +65,7 @@ export default class RoutletService extends Service {
       }
     
       exists(routeName) {
    -    if(get(routes, routeName)) {
    +    if (get(routes, routeName)) {
           return this.allowed(routeName);
         }
         return false;
    @@ -74,7 +74,7 @@ export default class RoutletService extends Service {
       allowed(routeName) {
         const abilities = get(routes, `${routeName}._options.abilities`) || [];
         if (abilities.length > 0) {
    -      if (!abilities.every(ability => this.permissions.can(ability))) {
    +      if (!abilities.every((ability) => this.permissions.can(ability))) {
             return false;
           }
         }
    @@ -83,7 +83,7 @@ export default class RoutletService extends Service {
     
       transition() {
         let endTransition;
    -    this._transition = new Promise(resolve => {
    +    this._transition = new Promise((resolve) => {
           endTransition = resolve;
         });
         return endTransition;
    @@ -91,7 +91,7 @@ export default class RoutletService extends Service {
     
       findOutlet(name) {
         const keys = [...outlets.keys()];
    -    const key = keys.find(item => name.indexOf(item) !== -1);
    +    const key = keys.find((item) => name.indexOf(item) !== -1);
         return key;
       }
     
    @@ -107,7 +107,7 @@ export default class RoutletService extends Service {
        */
       normalizeParamsFor(name, params = {}) {
         if (isWildcard(name)) {
    -      return Object.keys(params).reduce(function(prev, item) {
    +      return Object.keys(params).reduce(function (prev, item) {
             if (typeof params[item] !== 'undefined') {
               prev[item] = decodeURIComponent(params[item]);
             } else {
    @@ -159,7 +159,6 @@ export default class RoutletService extends Service {
         };
       }
     
    -
       // modelFor gets the model for Outlet specified by `name`, not the Route
       modelFor(name) {
         const outlet = outlets.get(name);
    diff --git a/ui/packages/consul-ui/app/services/settings.js b/ui/packages/consul-ui/app/services/settings.js
    index ced7243f2..c4b7c0b19 100644
    --- a/ui/packages/consul-ui/app/services/settings.js
    +++ b/ui/packages/consul-ui/app/services/settings.js
    @@ -3,8 +3,8 @@ import getStorage from 'consul-ui/utils/storage/local-storage';
     const SCHEME = 'consul';
     const storage = getStorage(SCHEME);
     // promise aware assertion
    -export const ifNotBlocking = function(repo) {
    -  return repo.findBySlug('client').then(function(settings) {
    +export const ifNotBlocking = function (repo) {
    +  return repo.findBySlug('client').then(function (settings) {
         return typeof settings.blocking !== 'undefined' && !settings.blocking;
       });
     };
    @@ -33,7 +33,7 @@ export default class SettingsService extends Service {
           obj = [obj];
         }
         const storage = this.storage;
    -    const item = obj.reduce(function(prev, item, i, arr) {
    +    const item = obj.reduce(function (prev, item, i, arr) {
           storage.removeValue(item);
           return prev;
         }, {});
    diff --git a/ui/packages/consul-ui/app/services/sort.js b/ui/packages/consul-ui/app/services/sort.js
    index 3784932c2..96c6884b3 100644
    --- a/ui/packages/consul-ui/app/services/sort.js
    +++ b/ui/packages/consul-ui/app/services/sort.js
    @@ -14,16 +14,18 @@ import peer from 'consul-ui/sort/comparators/peer';
     import node from 'consul-ui/sort/comparators/node';
     
     // returns an array of Property:asc, Property:desc etc etc
    -const directionify = arr => {
    +const directionify = (arr) => {
       return arr.reduce((prev, item) => prev.concat([`${item}:asc`, `${item}:desc`]), []);
     };
     // Specify a list of sortable properties, when called with a property
     // returns an array ready to be passed to ember @sort
     // properties(['Potential', 'Sortable', 'Properties'])('Sortable:asc') => ['Sortable:asc']
    -export const properties = (props = []) => key => {
    -  const comparables = directionify(props);
    -  return [comparables.find(item => item === key) || comparables[0]];
    -};
    +export const properties =
    +  (props = []) =>
    +  (key) => {
    +    const comparables = directionify(props);
    +    return [comparables.find((item) => item === key) || comparables[0]];
    +  };
     const options = {
       properties,
       directionify,
    diff --git a/ui/packages/consul-ui/app/services/state.js b/ui/packages/consul-ui/app/services/state.js
    index 76a98bef4..98b9f69dd 100644
    --- a/ui/packages/consul-ui/app/services/state.js
    +++ b/ui/packages/consul-ui/app/services/state.js
    @@ -4,7 +4,6 @@ import flat from 'flat';
     import { createMachine, interpret } from '@xstate/fsm';
     
     export default class StateService extends Service {
    -
       stateCharts = {};
     
       @service('logger') logger;
    @@ -19,9 +18,9 @@ export default class StateService extends Service {
       }
     
       addGuards(chart, options) {
    -    this.guards(chart).forEach(function([path, name]) {
    +    this.guards(chart).forEach(function ([path, name]) {
           // xstate/fsm has no guard lookup
    -      set(chart, path, function() {
    +      set(chart, path, function () {
             return !!options.onGuard(...[name, ...arguments]);
           });
         });
    @@ -40,11 +39,11 @@ export default class StateService extends Service {
         // xstate/fsm doesn't seem to interpret toplevel/global events
         // artificially add them here instead
         if (typeof chart.on !== 'undefined') {
    -      Object.values(chart.states).forEach(function(state) {
    +      Object.values(chart.states).forEach(function (state) {
             if (typeof state.on === 'undefined') {
               state.on = chart.on;
             } else {
    -          Object.keys(chart.on).forEach(function(key) {
    +          Object.keys(chart.on).forEach(function (key) {
                 if (typeof state.on[key] === 'undefined') {
                   state.on[key] = chart.on[key];
                 }
    @@ -61,7 +60,7 @@ export default class StateService extends Service {
           return false;
         }
         const values = Array.isArray(matches) ? matches : [matches];
    -    return values.some(item => {
    +    return values.some((item) => {
           return state.matches(item);
         });
       }
    @@ -76,7 +75,7 @@ export default class StateService extends Service {
         chart = this.prepareChart(chart);
         const service = interpret(this.machine(chart, options));
         // returns subscription
    -    service.subscribe(state => {
    +    service.subscribe((state) => {
           if (state.changed) {
             this.log(chart, state);
             options.onTransition(state);
    diff --git a/ui/packages/consul-ui/app/services/store.js b/ui/packages/consul-ui/app/services/store.js
    index fe31a1f69..2bf971f6e 100644
    --- a/ui/packages/consul-ui/app/services/store.js
    +++ b/ui/packages/consul-ui/app/services/store.js
    @@ -49,7 +49,7 @@ export default class StoreService extends Store {
         // TODO: Carry this over to the other methods ^
         return adapter
           .self(this, modelClass, token.secret, token)
    -      .then(payload => serializer.normalizeResponse(this, modelClass, payload, token, 'self'));
    +      .then((payload) => serializer.normalizeResponse(this, modelClass, payload, token, 'self'));
       }
     
       //
    @@ -59,7 +59,7 @@ export default class StoreService extends Store {
         const adapter = this.adapterFor(modelName);
         const serializer = this.serializerFor(modelName);
         const modelClass = { modelName: modelName };
    -    return adapter.queryLeader(this, modelClass, null, query).then(payload => {
    +    return adapter.queryLeader(this, modelClass, null, query).then((payload) => {
           payload.meta = serializer.normalizeMeta(this, modelClass, payload, null, 'leader');
           return payload;
         });
    @@ -73,7 +73,7 @@ export default class StoreService extends Store {
         const modelClass = { modelName: modelName };
         return adapter
           .authorize(this, modelClass, null, query)
    -      .then(payload =>
    +      .then((payload) =>
             serializer.normalizeResponse(this, modelClass, payload, undefined, 'authorize')
           );
       }
    diff --git a/ui/packages/consul-ui/app/services/temporal.js b/ui/packages/consul-ui/app/services/temporal.js
    index 82778ddaf..23ee5b103 100644
    --- a/ui/packages/consul-ui/app/services/temporal.js
    +++ b/ui/packages/consul-ui/app/services/temporal.js
    @@ -8,10 +8,9 @@ import Service from '@ember/service';
     dayjs.extend(relativeTime);
     
     export default class TemporalService extends Service {
    -
       format(value, options) {
         const djs = dayjs(value);
    -    if(dayjs().isBefore(djs)) {
    +    if (dayjs().isBefore(djs)) {
           return dayjs().to(djs, true);
         } else {
           return dayjs().from(djs, true);
    @@ -41,7 +40,6 @@ export default class TemporalService extends Service {
           default:
             assert(`${value} is not a valid type`, false);
             return value;
    -
         }
       }
     }
    diff --git a/ui/packages/consul-ui/app/services/timeout.js b/ui/packages/consul-ui/app/services/timeout.js
    index baa498756..f54ff9189 100644
    --- a/ui/packages/consul-ui/app/services/timeout.js
    +++ b/ui/packages/consul-ui/app/services/timeout.js
    @@ -12,7 +12,7 @@ export default class TimeoutService extends Service {
       }
     
       tick() {
    -    return new Promise(function(resolve, reject) {
    +    return new Promise(function (resolve, reject) {
           next(resolve);
         });
       }
    diff --git a/ui/packages/consul-ui/app/sort/comparators/auth-method.js b/ui/packages/consul-ui/app/sort/comparators/auth-method.js
    index 56bbc1c2b..957a87679 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/auth-method.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/auth-method.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => (key = 'MethodName:asc') => {
    -  return properties(['MethodName', 'TokenTTL'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'MethodName:asc') => {
    +    return properties(['MethodName', 'TokenTTL'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/health-check.js b/ui/packages/consul-ui/app/sort/comparators/health-check.js
    index be70e9fcf..5f5345c3b 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/health-check.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/health-check.js
    @@ -1,42 +1,43 @@
    -export default ({ properties }) => (key = 'Status:asc') => {
    -  if (key.startsWith('Status:')) {
    -    return function(itemA, itemB) {
    -      const [, dir] = key.split(':');
    -      let a, b;
    -      if (dir === 'asc') {
    -        a = itemA;
    -        b = itemB;
    -      } else {
    -        b = itemA;
    -        a = itemB;
    -      }
    -      const statusA = a.Status;
    -      const statusB = b.Status;
    -      switch (statusA) {
    -        case 'passing':
    -          // a = passing
    -          // unless b is also passing then a is less important
    -          return statusB === 'passing' ? 0 : 1;
    -        case 'critical':
    -          // a = critical
    -          // unless b is also critical then a is more important
    -          return statusB === 'critical' ? 0 : -1;
    -        case 'warning':
    -          // a = warning
    -          switch (statusB) {
    -            // b is passing so a is more important
    -            case 'passing':
    -              return -1;
    -            // b is critical so a is less important
    -            case 'critical':
    -              return 1;
    -            // a and b are both warning, therefore equal
    -            default:
    -              return 0;
    -          }
    -      }
    -      return 0;
    -    };
    -  }
    -  return properties(['Name', 'Kind'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'Status:asc') => {
    +    if (key.startsWith('Status:')) {
    +      return function (itemA, itemB) {
    +        const [, dir] = key.split(':');
    +        let a, b;
    +        if (dir === 'asc') {
    +          a = itemA;
    +          b = itemB;
    +        } else {
    +          b = itemA;
    +          a = itemB;
    +        }
    +        const statusA = a.Status;
    +        const statusB = b.Status;
    +        switch (statusA) {
    +          case 'passing':
    +            // a = passing
    +            // unless b is also passing then a is less important
    +            return statusB === 'passing' ? 0 : 1;
    +          case 'critical':
    +            // a = critical
    +            // unless b is also critical then a is more important
    +            return statusB === 'critical' ? 0 : -1;
    +          case 'warning':
    +            // a = warning
    +            switch (statusB) {
    +              // b is passing so a is more important
    +              case 'passing':
    +                return -1;
    +              // b is critical so a is less important
    +              case 'critical':
    +                return 1;
    +              // a and b are both warning, therefore equal
    +              default:
    +                return 0;
    +            }
    +        }
    +        return 0;
    +      };
    +    }
    +    return properties(['Name', 'Kind'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/intention.js b/ui/packages/consul-ui/app/sort/comparators/intention.js
    index 8d491a949..54329e940 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/intention.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/intention.js
    @@ -1,3 +1,3 @@
    -export default () => key => {
    +export default () => (key) => {
       return [key];
     };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/kv.js b/ui/packages/consul-ui/app/sort/comparators/kv.js
    index fe4612d15..f7df78255 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/kv.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/kv.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => key => {
    -  return properties(['Key', 'Kind'])(key);
    -};
    +export default ({ properties }) =>
    +  (key) => {
    +    return properties(['Key', 'Kind'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/node.js b/ui/packages/consul-ui/app/sort/comparators/node.js
    index 2d338a6b4..1bac46e1c 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/node.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/node.js
    @@ -1,37 +1,38 @@
    -export default ({ properties }) => (key = 'Name:asc') => {
    -  if (key.startsWith('Status:')) {
    -    return function(serviceA, serviceB) {
    -      const [, dir] = key.split(':');
    -      let a, b;
    -      if (dir === 'asc') {
    -        b = serviceA;
    -        a = serviceB;
    -      } else {
    -        a = serviceA;
    -        b = serviceB;
    -      }
    -      switch (true) {
    -        case a.ChecksCritical > b.ChecksCritical:
    -          return 1;
    -        case a.ChecksCritical < b.ChecksCritical:
    -          return -1;
    -        default:
    -          switch (true) {
    -            case a.ChecksWarning > b.ChecksWarning:
    -              return 1;
    -            case a.ChecksWarning < b.ChecksWarning:
    -              return -1;
    -            default:
    -              switch (true) {
    -                case a.ChecksPassing < b.ChecksPassing:
    -                  return 1;
    -                case a.ChecksPassing > b.ChecksPassing:
    -                  return -1;
    -              }
    -          }
    -          return 0;
    -      }
    -    };
    -  }
    -  return properties(['Node'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'Name:asc') => {
    +    if (key.startsWith('Status:')) {
    +      return function (serviceA, serviceB) {
    +        const [, dir] = key.split(':');
    +        let a, b;
    +        if (dir === 'asc') {
    +          b = serviceA;
    +          a = serviceB;
    +        } else {
    +          a = serviceA;
    +          b = serviceB;
    +        }
    +        switch (true) {
    +          case a.ChecksCritical > b.ChecksCritical:
    +            return 1;
    +          case a.ChecksCritical < b.ChecksCritical:
    +            return -1;
    +          default:
    +            switch (true) {
    +              case a.ChecksWarning > b.ChecksWarning:
    +                return 1;
    +              case a.ChecksWarning < b.ChecksWarning:
    +                return -1;
    +              default:
    +                switch (true) {
    +                  case a.ChecksPassing < b.ChecksPassing:
    +                    return 1;
    +                  case a.ChecksPassing > b.ChecksPassing:
    +                    return -1;
    +                }
    +            }
    +            return 0;
    +        }
    +      };
    +    }
    +    return properties(['Node'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/nspace.js b/ui/packages/consul-ui/app/sort/comparators/nspace.js
    index 1ec2b1a99..fbbdbb1eb 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/nspace.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/nspace.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => key => {
    -  return properties(['Name'])(key);
    -};
    +export default ({ properties }) =>
    +  (key) => {
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/partition.js b/ui/packages/consul-ui/app/sort/comparators/partition.js
    index 1ec2b1a99..fbbdbb1eb 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/partition.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/partition.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => key => {
    -  return properties(['Name'])(key);
    -};
    +export default ({ properties }) =>
    +  (key) => {
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/peer.js b/ui/packages/consul-ui/app/sort/comparators/peer.js
    index 912d23140..b91da9d0b 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/peer.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/peer.js
    @@ -1,26 +1,30 @@
     import { schema } from 'consul-ui/models/peer';
     
    -export default ({ properties }) => (key = 'State:asc') => {
    -  if (key.startsWith('State:')) {
    -    return function(itemA, itemB) {
    -      const [, dir] = key.split(':');
    -      let a, b;
    -      if (dir === 'asc') {
    -        b = itemA;
    -        a = itemB;
    -      } else {
    -        a = itemA;
    -        b = itemB;
    -      }
    -      switch (true) {
    -        case schema.State.allowedValues.indexOf(a.State) < schema.State.allowedValues.indexOf(b.State):
    -          return 1;
    -        case schema.State.allowedValues.indexOf(a.State) > schema.State.allowedValues.indexOf(b.State):
    -          return -1;
    -        case schema.State.allowedValues.indexOf(a.State) === schema.State.allowedValues.indexOf(b.State):
    -          return 0;
    -      }
    -    };
    -  }
    -  return properties(['Name'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'State:asc') => {
    +    if (key.startsWith('State:')) {
    +      return function (itemA, itemB) {
    +        const [, dir] = key.split(':');
    +        let a, b;
    +        if (dir === 'asc') {
    +          b = itemA;
    +          a = itemB;
    +        } else {
    +          a = itemA;
    +          b = itemB;
    +        }
    +        switch (true) {
    +          case schema.State.allowedValues.indexOf(a.State) <
    +            schema.State.allowedValues.indexOf(b.State):
    +            return 1;
    +          case schema.State.allowedValues.indexOf(a.State) >
    +            schema.State.allowedValues.indexOf(b.State):
    +            return -1;
    +          case schema.State.allowedValues.indexOf(a.State) ===
    +            schema.State.allowedValues.indexOf(b.State):
    +            return 0;
    +        }
    +      };
    +    }
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/policy.js b/ui/packages/consul-ui/app/sort/comparators/policy.js
    index 93a22f098..d4dc64116 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/policy.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/policy.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => (key = 'Name:asc') => {
    -  return properties(['Name'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'Name:asc') => {
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/role.js b/ui/packages/consul-ui/app/sort/comparators/role.js
    index fc036177f..89307fb5c 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/role.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/role.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => (key = 'Name:asc') => {
    -  return properties(['Name', 'CreateIndex'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'Name:asc') => {
    +    return properties(['Name', 'CreateIndex'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/service-instance.js b/ui/packages/consul-ui/app/sort/comparators/service-instance.js
    index ff1fe39f0..23ffec0ca 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/service-instance.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/service-instance.js
    @@ -1,23 +1,24 @@
    -export default ({ properties }) => key => {
    -  if (key.startsWith('Status:')) {
    -    const [, dir] = key.split(':');
    -    const props = [
    -      'PercentageChecksPassing',
    -      'PercentageChecksWarning',
    -      'PercentageChecksCritical',
    -    ];
    -    if (dir === 'asc') {
    -      props.reverse();
    -    }
    -    return function(a, b) {
    -      for (let i in props) {
    -        let prop = props[i];
    -        if (a[prop] === b[prop]) {
    -          continue;
    -        }
    -        return a[prop] > b[prop] ? -1 : 1;
    +export default ({ properties }) =>
    +  (key) => {
    +    if (key.startsWith('Status:')) {
    +      const [, dir] = key.split(':');
    +      const props = [
    +        'PercentageChecksPassing',
    +        'PercentageChecksWarning',
    +        'PercentageChecksCritical',
    +      ];
    +      if (dir === 'asc') {
    +        props.reverse();
           }
    -    };
    -  }
    -  return properties(['Name'])(key);
    -};
    +      return function (a, b) {
    +        for (let i in props) {
    +          let prop = props[i];
    +          if (a[prop] === b[prop]) {
    +            continue;
    +          }
    +          return a[prop] > b[prop] ? -1 : 1;
    +        }
    +      };
    +    }
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/service.js b/ui/packages/consul-ui/app/sort/comparators/service.js
    index 1b06fea92..90d7d36ea 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/service.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/service.js
    @@ -1,37 +1,38 @@
    -export default ({ properties }) => (key = 'Status:asc') => {
    -  if (key.startsWith('Status:')) {
    -    return function(serviceA, serviceB) {
    -      const [, dir] = key.split(':');
    -      let a, b;
    -      if (dir === 'asc') {
    -        b = serviceA;
    -        a = serviceB;
    -      } else {
    -        a = serviceA;
    -        b = serviceB;
    -      }
    -      switch (true) {
    -        case a.MeshChecksCritical > b.MeshChecksCritical:
    -          return 1;
    -        case a.MeshChecksCritical < b.MeshChecksCritical:
    -          return -1;
    -        default:
    -          switch (true) {
    -            case a.MeshChecksWarning > b.MeshChecksWarning:
    -              return 1;
    -            case a.MeshChecksWarning < b.MeshChecksWarning:
    -              return -1;
    -            default:
    -              switch (true) {
    -                case a.MeshChecksPassing < b.MeshChecksPassing:
    -                  return 1;
    -                case a.MeshChecksPassing > b.MeshChecksPassing:
    -                  return -1;
    -              }
    -          }
    -          return 0;
    -      }
    -    };
    -  }
    -  return properties(['Name'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'Status:asc') => {
    +    if (key.startsWith('Status:')) {
    +      return function (serviceA, serviceB) {
    +        const [, dir] = key.split(':');
    +        let a, b;
    +        if (dir === 'asc') {
    +          b = serviceA;
    +          a = serviceB;
    +        } else {
    +          a = serviceA;
    +          b = serviceB;
    +        }
    +        switch (true) {
    +          case a.MeshChecksCritical > b.MeshChecksCritical:
    +            return 1;
    +          case a.MeshChecksCritical < b.MeshChecksCritical:
    +            return -1;
    +          default:
    +            switch (true) {
    +              case a.MeshChecksWarning > b.MeshChecksWarning:
    +                return 1;
    +              case a.MeshChecksWarning < b.MeshChecksWarning:
    +                return -1;
    +              default:
    +                switch (true) {
    +                  case a.MeshChecksPassing < b.MeshChecksPassing:
    +                    return 1;
    +                  case a.MeshChecksPassing > b.MeshChecksPassing:
    +                    return -1;
    +                }
    +            }
    +            return 0;
    +        }
    +      };
    +    }
    +    return properties(['Name'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/token.js b/ui/packages/consul-ui/app/sort/comparators/token.js
    index 32aac9edc..04fc174f8 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/token.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/token.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => key => {
    -  return properties(['CreateTime'])(key);
    -};
    +export default ({ properties }) =>
    +  (key) => {
    +    return properties(['CreateTime'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/sort/comparators/upstream-instance.js b/ui/packages/consul-ui/app/sort/comparators/upstream-instance.js
    index 8e3bd0cd7..3037de848 100644
    --- a/ui/packages/consul-ui/app/sort/comparators/upstream-instance.js
    +++ b/ui/packages/consul-ui/app/sort/comparators/upstream-instance.js
    @@ -1,3 +1,4 @@
    -export default ({ properties }) => (key = 'DestinationName:asc') => {
    -  return properties(['DestinationName'])(key);
    -};
    +export default ({ properties }) =>
    +  (key = 'DestinationName:asc') => {
    +    return properties(['DestinationName'])(key);
    +  };
    diff --git a/ui/packages/consul-ui/app/styles/base/decoration/visually-hidden.css.js b/ui/packages/consul-ui/app/styles/base/decoration/visually-hidden.css.js
    index 13122599a..7c0a511c3 100644
    --- a/ui/packages/consul-ui/app/styles/base/decoration/visually-hidden.css.js
    +++ b/ui/packages/consul-ui/app/styles/base/decoration/visually-hidden.css.js
    @@ -1,5 +1,5 @@
     export default (css) => {
    -/*%visually-hidden {*/
    +  /*%visually-hidden {*/
       return css`
         @keyframes visually-hidden {
           100% {
    @@ -14,5 +14,5 @@ export default (css) => {
           }
         }
       `;
    -/*}*/
    -}
    +  /*}*/
    +};
    diff --git a/ui/packages/consul-ui/app/styles/base/icons/base-keyframes.css.js b/ui/packages/consul-ui/app/styles/base/icons/base-keyframes.css.js
    index 0a142062e..20f61c4ca 100644
    --- a/ui/packages/consul-ui/app/styles/base/icons/base-keyframes.css.js
    +++ b/ui/packages/consul-ui/app/styles/base/icons/base-keyframes.css.js
    @@ -1,91 +1,92 @@
    -export default css => css`
    -*::before, *::after {
    -  display: inline-block;
    -  animation-play-state: paused;
    -  animation-fill-mode: forwards;
    -  animation-iteration-count: var(--icon-resolution, 1);
    -  vertical-align: text-top;
    -}
    -*::before {
    -  animation-name: var(--icon-name-start, var(--icon-name)),
    -                  var(--icon-size-start, var(--icon-size, icon-000));
    -  background-color: var(--icon-color-start, var(--icon-color));
    -}
    -*::after {
    -  animation-name: var(--icon-name-end, var(--icon-name)),
    -                  var(--icon-size-end, var(--icon-size, icon-000));
    -  background-color: var(--icon-color-end, var(--icon-color));
    -}
    +export default (css) => css`
    +  *::before,
    +  *::after {
    +    display: inline-block;
    +    animation-play-state: paused;
    +    animation-fill-mode: forwards;
    +    animation-iteration-count: var(--icon-resolution, 1);
    +    vertical-align: text-top;
    +  }
    +  *::before {
    +    animation-name: var(--icon-name-start, var(--icon-name)),
    +      var(--icon-size-start, var(--icon-size, icon-000));
    +    background-color: var(--icon-color-start, var(--icon-color));
    +  }
    +  *::after {
    +    animation-name: var(--icon-name-end, var(--icon-name)),
    +      var(--icon-size-end, var(--icon-size, icon-000));
    +    background-color: var(--icon-color-end, var(--icon-color));
    +  }
     
    -[style*='--icon-color-start']::before {
    -  color: var(--icon-color-start);
    -}
    -[style*='--icon-color-end']::after {
    -  color: var(--icon-color-end);
    -}
    -[style*='--icon-name-start']::before,
    -[style*='--icon-name-end']::after {
    -  content: '';
    -}
    +  [style*='--icon-color-start']::before {
    +    color: var(--icon-color-start);
    +  }
    +  [style*='--icon-color-end']::after {
    +    color: var(--icon-color-end);
    +  }
    +  [style*='--icon-name-start']::before,
    +  [style*='--icon-name-end']::after {
    +    content: '';
    +  }
     
    -@keyframes icon-000 {
    -  100% {
    -    width: 1.2em;
    -    height: 1.2em;
    +  @keyframes icon-000 {
    +    100% {
    +      width: 1.2em;
    +      height: 1.2em;
    +    }
       }
    -}
    -@keyframes icon-100 {
    -  100% {
    -    width: 0.625rem; /* 10px */
    -    height: 0.625rem; /* 10px */
    +  @keyframes icon-100 {
    +    100% {
    +      width: 0.625rem; /* 10px */
    +      height: 0.625rem; /* 10px */
    +    }
       }
    -}
    -@keyframes icon-200 {
    -  100% {
    -    width: 0.75rem; /* 12px */
    -    height: 0.75rem; /* 12px */
    +  @keyframes icon-200 {
    +    100% {
    +      width: 0.75rem; /* 12px */
    +      height: 0.75rem; /* 12px */
    +    }
       }
    -}
    -@keyframes icon-300 {
    -  100% {
    -    width: 1rem; /* 16px */
    -    height: 1rem; /* 16px */
    +  @keyframes icon-300 {
    +    100% {
    +      width: 1rem; /* 16px */
    +      height: 1rem; /* 16px */
    +    }
       }
    -}
    -@keyframes icon-400 {
    -  100% {
    -    width: 1.125rem; /* 18px */
    -    height: 1.125rem; /* 18px */
    +  @keyframes icon-400 {
    +    100% {
    +      width: 1.125rem; /* 18px */
    +      height: 1.125rem; /* 18px */
    +    }
       }
    -}
    -@keyframes icon-500 {
    -  100% {
    -    width: 1.250rem; /* 20px */
    -    height: 1.250rem; /* 20px */
    +  @keyframes icon-500 {
    +    100% {
    +      width: 1.25rem; /* 20px */
    +      height: 1.25rem; /* 20px */
    +    }
       }
    -}
    -@keyframes icon-600 {
    -  100% {
    -    width: 1.375rem; /* 22px */
    -    height: 1.375rem; /* 22px */
    +  @keyframes icon-600 {
    +    100% {
    +      width: 1.375rem; /* 22px */
    +      height: 1.375rem; /* 22px */
    +    }
       }
    -}
    -@keyframes icon-700 {
    -  100% {
    -    width: 1.500rem; /* 24px */
    -    height: 1.500rem; /* 24px */
    +  @keyframes icon-700 {
    +    100% {
    +      width: 1.5rem; /* 24px */
    +      height: 1.5rem; /* 24px */
    +    }
       }
    -}
    -@keyframes icon-800 {
    -  100% {
    -    width: 1.625rem; /* 26px */
    -    height: 1.625rem; /* 26px */
    +  @keyframes icon-800 {
    +    100% {
    +      width: 1.625rem; /* 26px */
    +      height: 1.625rem; /* 26px */
    +    }
       }
    -}
    -@keyframes icon-900 {
    -  100% {
    -    width: 1.750rem; /* 28px */
    -    height: 1.750rem; /* 28px */
    +  @keyframes icon-900 {
    +    100% {
    +      width: 1.75rem; /* 28px */
    +      height: 1.75rem; /* 28px */
    +    }
       }
    -}
     `;
    diff --git a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss
    index 9d1a5efe3..8f8663c4c 100644
    --- a/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss
    +++ b/ui/packages/consul-ui/app/styles/base/icons/icons/index.scss
    @@ -2,7 +2,7 @@
     @import './alert-circle-outline/index.scss';
     @import './alert-triangle/index.scss';
     // @import './arrow-down/index.scss';
    -// @import './arrow-left/index.scss';
    +@import './arrow-left/index.scss';
     @import './arrow-right/index.scss';
     // @import './arrow-up/index.scss';
     // @import './bolt/index.scss';
    @@ -330,7 +330,7 @@
     // @import './file-minus/index.scss';
     // @import './file-plus/index.scss';
     // @import './file-source/index.scss';
    -// @import './file-text/index.scss';
    +@import './file-text/index.scss';
     // @import './file-x/index.scss';
     // @import './files/index.scss';
     // @import './film/index.scss';
    diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss
    index f94f14d44..4a7e7b9e0 100644
    --- a/ui/packages/consul-ui/app/styles/components.scss
    +++ b/ui/packages/consul-ui/app/styles/components.scss
    @@ -109,3 +109,4 @@
     @import 'consul-ui/components/consul/node/peer-info';
     @import 'consul-ui/components/consul/peer/info';
     @import 'consul-ui/components/consul/peer/form';
    +@import 'consul-ui/components/consul/hcp/home';
    diff --git a/ui/packages/consul-ui/app/templates/application.hbs b/ui/packages/consul-ui/app/templates/application.hbs
    index feace296f..5db69c1d2 100644
    --- a/ui/packages/consul-ui/app/templates/application.hbs
    +++ b/ui/packages/consul-ui/app/templates/application.hbs
    @@ -1,191 +1,133 @@
    -
    +
     
    -{{! Add the a11y route announcer }}
    -
    -{{! Tell CSS what we have enabled }}
    -{{#if (can "use acls")}}
    +  {{! Add the a11y route announcer }}
    +  
    +  {{! Tell CSS what we have enabled }}
    +  {{#if (can "use acls")}}
       {{document-attrs class="has-acls"}}
    -{{/if}}
    -{{#if (can "use nspaces")}}
    -  {{document-attrs class="has-nspaces"}}
    -{{/if}}
    -{{#if (can "use partitions")}}
    -  {{document-attrs class="has-partitions"}}
    -{{/if}}
    -
    -{{! Listen out for blocking query/client setting changes }}
    -
    -
    -{{! Tell CSS about our theme }}
    -
    -{{#each-in source.data as |key value|}}
    -  {{#if (and value (includes key (array "color-scheme" "contrast")))}}
    -    {{document-attrs class=(concat 'prefers-' key '-' value)}}
       {{/if}}
    -{{/each-in}}
    -
    +  {{#if (can "use nspaces")}}
    +  {{document-attrs class="has-nspaces"}}
    +  {{/if}}
    +  {{#if (can "use partitions")}}
    +  {{document-attrs class="has-partitions"}}
    +  {{/if}}
     
    -{{! If ACLs are enabled try get a token }}
    -{{#if (can "use acls")}}
    -
    -{{/if}}
    +  {{! Listen out for blocking query/client setting changes }}
    +  
    +
    +  {{! Tell CSS about our theme }}
    +  
    +    {{#each-in source.data as |key value|}}
    +    {{#if (and value (includes key (array "color-scheme" "contrast")))}}
    +    {{document-attrs class=(concat 'prefers-' key '-' value)}}
    +    {{/if}}
    +    {{/each-in}}
    +  
    +
    +  {{! If ACLs are enabled try get a token }}
    +  {{#if (can "use acls")}}
    +  
    +  {{/if}}
     
     
    -{{#if (not-eq route.currentName 'oauth-provider-debug')}}
    +  {{#if (not-eq route.currentName 'oauth-provider-debug')}}
     
    -{{! redirect if we aren't on a URL with dc information }}
    -{{#if (eq route.currentName 'index')}}
    -{{! until we get to the dc route we don't know any permissions }}
    -{{! as we don't know the dc, any inital permission based }}
    -{{! redirects are in the dc.show route}}
    +  {{! redirect if we aren't on a URL with dc information }}
    +  {{#if (eq route.currentName 'index')}}
    +  {{! until we get to the dc route we don't know any permissions }}
    +  {{! as we don't know the dc, any inital permission based }}
    +  {{! redirects are in the dc.show route}}
     
    -{{! 2022-04-15: Temporarily reverting the services page to the default }}
    +  {{! 2022-04-15: Temporarily reverting the services page to the default }}
       {{did-insert (route-action 'replaceWith' 'dc.services.index'
    -    (hash
    -      dc=(env 'CONSUL_DATACENTER_LOCAL')
    -    )
    +  (hash
    +  dc=(env 'CONSUL_DATACENTER_LOCAL')
    +  )
       )}}
    -{{else}}
    -{{! If we are notfound, guess the params we need }}
    -{{#if (eq route.currentName 'notfound')}}
    -  
    -{{/if}}
    +  {{else}}
    +  {{! If we are notfound, guess the params we need }}
    +  {{#if (eq route.currentName 'notfound')}}
    +  
    +  {{/if}}
     
    -{{! Make sure we guess and default to the right params when not found }}
    -{{#let
    +  {{! Make sure we guess and default to the right params when not found }}
    +  {{#let
       (if (can "use partitions") (or route.params.partition notfound.partition token.Partition '') '')
       (if (can "use nspaces") (or route.params.nspace notfound.nspace token.Namespace '') '')
    -as |partition nspace|}}
    +  as |partition nspace|}}
     
    -{{! Make sure we have enough to show the app chrome}}
    -{{! Don't show anything until we have a list of DCs }}
    -
    -{{! Once we have a list of DCs make sure the DC we are asking for exists }}
    -{{! If not use the DC that the UI is running in }}
    -{{#let
    +  {{! Make sure we have enough to show the app chrome}}
    +  {{! Don't show anything until we have a list of DCs }}
    +  
    +    {{! Once we have a list of DCs make sure the DC we are asking for exists }}
    +    {{! If not use the DC that the UI is running in }}
    +    {{#let
     
    -  (or
    +    (or
     
         (if nofound.dc
    -        (object-at 0 (cached-model
    -          'dc'
    -          (hash
    -            Name=notfound.dc
    -          )
    -        )
    -      )
    +    (object-at 0 (cached-model
    +    'dc'
    +    (hash
    +    Name=notfound.dc
    +    )
    +    )
    +    )
         )
     
         (object-at 0 (cached-model
    -        'dc'
    -        (hash
    -          Name=route.params.dc
    -        )
    -      )
    +    'dc'
    +    (hash
    +    Name=route.params.dc
    +    )
    +    )
         )
     
         (hash
    -      Name=(env "CONSUL_DATACENTER_LOCAL")
    +    Name=(env "CONSUL_DATACENTER_LOCAL")
         )
     
    -  )
    +    )
     
    -  dcs.data
    +    dcs.data
     
    -as |dc dcs|}}
    -  {{#if (and (gt dc.Name.length 0) dcs)}}
    +    as |dc dcs|}}
    +    {{#if (and (gt dc.Name.length 0) dcs)}}
     
         {{! figure out our current DC and convert it to a model }}
    -    
    -    {{#if dc.data}}
    -      
    +    
    +      {{#if dc.data}}
    +      
     
    -{{#if error}}
    +        {{#if error}}
             {{! If we got an error from anything, show an error page }}
    -        
    -{{else}}
    +        
    +        {{else}}
             {{! Otherwise show the rest of the app}}
    -        
    +        
               {{outlet}}
             
     
             {{! loading component for when we need it}}
    -        
    -{{/if}}
    +        
    +        {{/if}}
     
           
    -{{/if}}
    +      {{/if}}
         
    +    {{/if}}
    +    {{/let}}
    +  
    +  {{/let}}
       {{/if}}
    -{{/let}}
    -
    -{{/let}}
    -{{/if}}
    -{{else}}
    +  {{else}}
       {{! Routes with no main navigation }}
    -  
    +  
         {{outlet}}
       
    -{{/if}}
    +  {{/if}}
     
    diff --git a/ui/packages/consul-ui/app/utils/ascend.js b/ui/packages/consul-ui/app/utils/ascend.js
    index 761c37594..1a4a34909 100644
    --- a/ui/packages/consul-ui/app/utils/ascend.js
    +++ b/ui/packages/consul-ui/app/utils/ascend.js
    @@ -1,9 +1,4 @@
    -export default function(path, num) {
    +export default function (path, num) {
       const parts = path.split('/');
    -  return parts.length > num
    -    ? parts
    -        .slice(0, -num)
    -        .concat('')
    -        .join('/')
    -    : '';
    +  return parts.length > num ? parts.slice(0, -num).concat('').join('/') : '';
     }
    diff --git a/ui/packages/consul-ui/app/utils/atob.js b/ui/packages/consul-ui/app/utils/atob.js
    index d930c5e31..3315790c3 100644
    --- a/ui/packages/consul-ui/app/utils/atob.js
    +++ b/ui/packages/consul-ui/app/utils/atob.js
    @@ -1,5 +1,5 @@
     import base64js from 'base64-js';
    -export default function(str, encoding = 'utf-8') {
    +export default function (str, encoding = 'utf-8') {
       // decode
       const bytes = base64js.toByteArray(str);
       return new TextDecoder(encoding).decode(bytes);
    diff --git a/ui/packages/consul-ui/app/utils/btoa.js b/ui/packages/consul-ui/app/utils/btoa.js
    index 5da689afd..0ac200d01 100644
    --- a/ui/packages/consul-ui/app/utils/btoa.js
    +++ b/ui/packages/consul-ui/app/utils/btoa.js
    @@ -1,5 +1,5 @@
     import base64js from 'base64-js';
    -export default function(str) {
    +export default function (str) {
       // encode
       const bytes = new TextEncoder().encode(str);
       return base64js.fromByteArray(bytes);
    diff --git a/ui/packages/consul-ui/app/utils/callable-type.js b/ui/packages/consul-ui/app/utils/callable-type.js
    index 20ef16420..6023bc92c 100644
    --- a/ui/packages/consul-ui/app/utils/callable-type.js
    +++ b/ui/packages/consul-ui/app/utils/callable-type.js
    @@ -1,6 +1,6 @@
    -export default function(obj) {
    +export default function (obj) {
       if (typeof obj !== 'function') {
    -    return function() {
    +    return function () {
           return obj;
         };
       } else {
    diff --git a/ui/packages/consul-ui/app/utils/create-fingerprinter.js b/ui/packages/consul-ui/app/utils/create-fingerprinter.js
    index d0f103dcf..bc6481647 100644
    --- a/ui/packages/consul-ui/app/utils/create-fingerprinter.js
    +++ b/ui/packages/consul-ui/app/utils/create-fingerprinter.js
    @@ -1,8 +1,8 @@
     import { get } from '@ember/object';
     
    -export default function(foreignKey, nspaceKey, partitionKey, hash = JSON.stringify) {
    -  return function(primaryKey, slugKey, foreignKeyValue, nspaceValue, partitionValue) {
    -    return function(item) {
    +export default function (foreignKey, nspaceKey, partitionKey, hash = JSON.stringify) {
    +  return function (primaryKey, slugKey, foreignKeyValue, nspaceValue, partitionValue) {
    +    return function (item) {
           foreignKeyValue = foreignKeyValue == null ? item[foreignKey] : foreignKeyValue;
           if (foreignKeyValue == null) {
             throw new Error(
    @@ -10,7 +10,7 @@ export default function(foreignKey, nspaceKey, partitionKey, hash = JSON.stringi
             );
           }
           const slugKeys = slugKey.split(',');
    -      const slugValues = slugKeys.map(function(slugKey) {
    +      const slugValues = slugKeys.map(function (slugKey) {
             const slug = get(item, slugKey);
             if (slug == null || slug.length < 1) {
               throw new Error(
    diff --git a/ui/packages/consul-ui/app/utils/distance.js b/ui/packages/consul-ui/app/utils/distance.js
    index 67c914e64..1d1e33ac7 100644
    --- a/ui/packages/consul-ui/app/utils/distance.js
    +++ b/ui/packages/consul-ui/app/utils/distance.js
    @@ -1,7 +1,7 @@
     // TODO: Istanbul is ignored for the moment as it's not mine,
     // once I come here properly and 100% follow unignore
     /* istanbul ignore file */
    -export default function(a, b) {
    +export default function (a, b) {
       a = a.Coord;
       b = b.Coord;
       let sum = 0;
    diff --git a/ui/packages/consul-ui/app/utils/dom/click-first-anchor.js b/ui/packages/consul-ui/app/utils/dom/click-first-anchor.js
    index 04cde2e9e..8d6012501 100644
    --- a/ui/packages/consul-ui/app/utils/dom/click-first-anchor.js
    +++ b/ui/packages/consul-ui/app/utils/dom/click-first-anchor.js
    @@ -1,24 +1,24 @@
    -const clickEvent = function($el) {
    +const clickEvent = function ($el) {
       ['mousedown', 'mouseup', 'click']
    -    .map(function(type) {
    +    .map(function (type) {
           return new MouseEvent(type, {
             bubbles: true,
             cancelable: true,
             view: window,
           });
         })
    -    .forEach(function(event) {
    +    .forEach(function (event) {
           $el.dispatchEvent(event);
         });
     };
    -export default function(closest, click = clickEvent) {
    +export default function (closest, click = clickEvent) {
       // TODO: Decide whether we should use `e` for ease
       // or `target`/`el`
       // TODO: currently, using a string stopElement to tell the func
       // where to stop looking and currenlty default is 'tr' because
       // it's backwards compatible.
       // Long-term this func shouldn't default to 'tr'
    -  return function(e, stopElement = 'tr') {
    +  return function (e, stopElement = 'tr') {
         // click on row functionality
         // so if you click the actual row but not a link
         // find the first link and fire that instead
    diff --git a/ui/packages/consul-ui/app/utils/dom/closest.js b/ui/packages/consul-ui/app/utils/dom/closest.js
    index d66380a04..db984b0c7 100644
    --- a/ui/packages/consul-ui/app/utils/dom/closest.js
    +++ b/ui/packages/consul-ui/app/utils/dom/closest.js
    @@ -1,4 +1,4 @@
    -export default function(sel, el) {
    +export default function (sel, el) {
       // basic DOM closest utility to cope with no support
       // TODO: instead of degrading gracefully
       // add a while polyfill for closest
    diff --git a/ui/packages/consul-ui/app/utils/dom/create-listeners.js b/ui/packages/consul-ui/app/utils/dom/create-listeners.js
    index c8ffe26c4..c603dfe19 100644
    --- a/ui/packages/consul-ui/app/utils/dom/create-listeners.js
    +++ b/ui/packages/consul-ui/app/utils/dom/create-listeners.js
    @@ -21,10 +21,10 @@ class Listeners {
               [event]: handler,
             };
           }
    -      const removers = Object.keys(obj).map(function(key) {
    -        return (function(event, handler) {
    +      const removers = Object.keys(obj).map(function (key) {
    +        return (function (event, handler) {
               target[addEventListener](event, handler);
    -          return function() {
    +          return function () {
                 target[removeEventListener](event, handler);
                 return handler;
               };
    @@ -33,22 +33,22 @@ class Listeners {
           // TODO: if event was a string only return the first
           // although the problem remains that it could sometimes return
           // a function, sometimes an array, so this needs some more thought
    -      remove = () => removers.map(item => item());
    +      remove = () => removers.map((item) => item());
         }
         this.listeners.push(remove);
         return () => {
    -      const pos = this.listeners.findIndex(function(item) {
    +      const pos = this.listeners.findIndex(function (item) {
             return item === remove;
           });
           return this.listeners.splice(pos, 1)[0]();
         };
       }
       remove() {
    -    const handlers = this.listeners.map(item => item());
    +    const handlers = this.listeners.map((item) => item());
         this.listeners.splice(0, this.listeners.length);
         return handlers;
       }
     }
    -export default function(listeners = []) {
    +export default function (listeners = []) {
       return new Listeners(listeners);
     }
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/blocking.js b/ui/packages/consul-ui/app/utils/dom/event-source/blocking.js
    index 8ff1ad539..ae2a25bb8 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/blocking.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/blocking.js
    @@ -4,9 +4,9 @@ const pause = 2000;
     // native EventSource retry is ~3s wait
     // any specified errors here will mean that the blocking query will attempt
     // a reconnection every 3s until it reconnects to Consul
    -export const createErrorBackoff = function(ms = 3000, P = Promise, wait = setTimeout) {
    +export const createErrorBackoff = function (ms = 3000, P = Promise, wait = setTimeout) {
       // This expects an ember-data like error
    -  return function(err) {
    +  return function (err) {
         // expect and ember-data error or a http-like error (e.statusCode)
         let status = get(err, 'errors.firstObject.status') || get(err, 'statusCode');
         if (typeof status !== 'undefined') {
    @@ -20,8 +20,8 @@ export const createErrorBackoff = function(ms = 3000, P = Promise, wait = setTim
               // TODO: Move this to the view layer so we can show a connection error
               // and reconnection success to the user
               // Any 0 aborted connections should back off and try again
    -          return new P(function(resolve) {
    -            wait(function() {
    +          return new P(function (resolve) {
    +            wait(function () {
                   resolve(err);
                 }, ms);
               });
    @@ -31,7 +31,7 @@ export const createErrorBackoff = function(ms = 3000, P = Promise, wait = setTim
         throw err;
       };
     };
    -export const validateCursor = function(current, prev = null) {
    +export const validateCursor = function (current, prev = null) {
       let cursor = parseInt(current);
       if (!isNaN(cursor)) {
         // if cursor is less than the current cursor, reset to zero
    @@ -42,16 +42,16 @@ export const validateCursor = function(current, prev = null) {
         return Math.max(cursor, 1);
       }
     };
    -const throttle = function(configuration, prev, current) {
    -  return function(obj) {
    -    return new Promise(function(resolve, reject) {
    -      setTimeout(function() {
    +const throttle = function (configuration, prev, current) {
    +  return function (obj) {
    +    return new Promise(function (resolve, reject) {
    +      setTimeout(function () {
             resolve(obj);
           }, configuration.interval || pause);
         });
       };
     };
    -const defaultCreateEvent = function(result, configuration) {
    +const defaultCreateEvent = function (result, configuration) {
       return {
         type: 'message',
         data: result,
    @@ -63,7 +63,7 @@ const defaultCreateEvent = function(result, configuration) {
      * @param {Class} [CallableEventSource] - CallableEventSource Class
      * @param {Function} [backoff] - Default backoff function for all instances, defaults to createErrorBackoff
      */
    -export default function(EventSource, backoff = createErrorBackoff()) {
    +export default function (EventSource, backoff = createErrorBackoff()) {
       /**
        * An EventSource implementation to add native EventSource-like functionality with just callbacks (`cursor` and 5xx backoff)
        *
    @@ -77,15 +77,15 @@ export default function(EventSource, backoff = createErrorBackoff()) {
        *   `cursor` - Cursor position of the EventSource
        *   `createEvent` - A data filter, giving you the opportunity to filter or replace the event data, such as removing/replacing records
        */
    -  const BlockingEventSource = function(source, configuration = {}) {
    +  const BlockingEventSource = function (source, configuration = {}) {
         const { currentEvent, ...config } = configuration;
         EventSource.apply(this, [
    -      configuration => {
    +      (configuration) => {
             const { createEvent, ...superConfiguration } = configuration;
             return source
               .apply(this, [superConfiguration, this])
               .catch(backoff)
    -          .then(result => {
    +          .then((result) => {
                 if (result instanceof Error) {
                   return result;
                 }
    @@ -126,7 +126,7 @@ export default function(EventSource, backoff = createErrorBackoff()) {
         // only on initialization
         // if we already have an currentEvent set via configuration
         // dispatch the event so things are populated immediately
    -    this.addEventListener('open', e => {
    +    this.addEventListener('open', (e) => {
           const currentEvent = e.target.getCurrentEvent();
           if (typeof currentEvent !== 'undefined') {
             this.dispatchEvent(currentEvent);
    @@ -143,10 +143,10 @@ export default function(EventSource, backoff = createErrorBackoff()) {
         }),
         {
           // if we are having these props, at least make getters
    -      getCurrentEvent: function() {
    +      getCurrentEvent: function () {
             return this.currentEvent;
           },
    -      getPreviousEvent: function() {
    +      getPreviousEvent: function () {
             return this.previousEvent;
           },
         }
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/cache.js b/ui/packages/consul-ui/app/utils/dom/event-source/cache.js
    index 4e7d9dee5..34e2cb7ed 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/cache.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/cache.js
    @@ -1,6 +1,6 @@
    -export default function(source, DefaultEventSource, P = Promise) {
    -  return function(sources) {
    -    return function(cb, configuration) {
    +export default function (source, DefaultEventSource, P = Promise) {
    +  return function (sources) {
    +    return function (cb, configuration) {
           const key = configuration.key;
           if (typeof sources[key] !== 'undefined' && configuration.settings.enabled) {
             if (typeof sources[key].configuration === 'undefined') {
    @@ -12,12 +12,12 @@ export default function(source, DefaultEventSource, P = Promise) {
             const EventSource = configuration.type || DefaultEventSource;
             const eventSource = (sources[key] = new EventSource(cb, configuration));
             return source(eventSource)
    -          .catch(function(e) {
    +          .catch(function (e) {
                 // any errors, delete from the cache for next time
                 delete sources[key];
                 return P.reject(e);
               })
    -          .then(function(eventSource) {
    +          .then(function (eventSource) {
                 // make sure we cancel everything out if there is no cursor
                 if (typeof eventSource.configuration.cursor === 'undefined') {
                   eventSource.close();
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/callable.js b/ui/packages/consul-ui/app/utils/dom/event-source/callable.js
    index 86e8af079..4d3ccd091 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/callable.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/callable.js
    @@ -1,4 +1,4 @@
    -export const defaultRunner = function(target, configuration, isClosed) {
    +export const defaultRunner = function (target, configuration, isClosed) {
       if (isClosed(target)) {
         target.dispatchEvent({ type: 'close' });
         return;
    @@ -6,17 +6,17 @@ export const defaultRunner = function(target, configuration, isClosed) {
       // TODO Consider wrapping this is a promise for none thenable returns
       return target.source
         .bind(target)(configuration, target)
    -    .then(function(res) {
    +    .then(function (res) {
           return defaultRunner(target, configuration, isClosed);
         });
     };
    -const errorEvent = function(e) {
    +const errorEvent = function (e) {
       return new ErrorEvent('error', {
         error: e,
         message: e.message,
       });
     };
    -const isClosed = function(target) {
    +const isClosed = function (target) {
       switch (target.readyState) {
         case 2: // CLOSED
         case 3: // CLOSING
    @@ -24,18 +24,18 @@ const isClosed = function(target) {
       }
       return false;
     };
    -export default function(
    +export default function (
       EventTarget,
       P = Promise,
       run = defaultRunner,
       createErrorEvent = errorEvent
     ) {
    -  const CallableEventSource = function(source, configuration = {}) {
    +  const CallableEventSource = function (source, configuration = {}) {
         EventTarget.call(this);
         this.readyState = 2;
         this.source =
           typeof source !== 'function'
    -        ? function(configuration, target) {
    +        ? function (configuration, target) {
                 this.close();
                 return P.resolve();
               }
    @@ -52,7 +52,7 @@ export default function(
             this.dispatchEvent({ type: 'open' });
             return run(this, configuration, isClosed);
           })
    -      .catch(e => {
    +      .catch((e) => {
             this.dispatchEvent(createErrorEvent(e));
             // close after the dispatch so we can tell if it was an error whilst closed or not
             // but make sure its before the promise tick
    @@ -74,7 +74,7 @@ export default function(
           },
         }),
         {
    -      close: function() {
    +      close: function () {
             // additional readyState 3 = CLOSING
             switch (this.readyState) {
               case 0: // CONNECTING
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/index.js b/ui/packages/consul-ui/app/utils/dom/event-source/index.js
    index 06d6e3182..80919d6e0 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/index.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/index.js
    @@ -22,7 +22,7 @@ import { env } from 'consul-ui/env';
     let runner;
     switch (env('CONSUL_UI_REALTIME_RUNNER')) {
       case 'ec':
    -    runner = function(target, configuration, isClosed) {
    +    runner = function (target, configuration, isClosed) {
           return EmberObject.extend({
             task: task(function* run() {
               while (!isClosed(target)) {
    @@ -36,8 +36,8 @@ switch (env('CONSUL_UI_REALTIME_RUNNER')) {
         };
         break;
       case 'generator':
    -    runner = async function(target, configuration, isClosed) {
    -      const run = function*() {
    +    runner = async function (target, configuration, isClosed) {
    +      const run = function* () {
             while (!isClosed(target)) {
               yield target.source.bind(target)(configuration);
             }
    @@ -52,8 +52,8 @@ switch (env('CONSUL_UI_REALTIME_RUNNER')) {
         };
         break;
       case 'async':
    -    runner = async function(target, configuration, isClosed) {
    -      const run = function() {
    +    runner = async function (target, configuration, isClosed) {
    +      const run = function () {
             return target.source.bind(target)(configuration);
           };
           let res;
    @@ -77,49 +77,49 @@ export const StorageEventSource = StorageEventSourceFactory(EventTarget, Promise
     export const proxy = proxyFactory(ObjectProxy, ArrayProxy, createListeners);
     export const resolve = firstResolverFactory(Promise);
     
    -export const source = function(source) {
    +export const source = function (source) {
       // create API needed for conventional promise blocked, loading, Routes
       // i.e. resolve/reject on first response
    -  return resolve(source, createListeners()).then(function(data) {
    +  return resolve(source, createListeners()).then(function (data) {
         // create API needed for conventional DD/computed and Controllers
         return proxy(source, data);
       });
     };
     export const cache = cacheFactory(source, BlockingEventSource, Promise);
     
    -const errorEvent = function(e) {
    +const errorEvent = function (e) {
       return new ErrorEvent('error', {
         error: e,
         message: e.message,
       });
     };
    -export const fromPromise = function(promise) {
    -  return new CallableEventSource(function(configuration) {
    +export const fromPromise = function (promise) {
    +  return new CallableEventSource(function (configuration) {
         const dispatch = this.dispatchEvent.bind(this);
         const close = () => {
           this.close();
         };
         return promise
    -      .then(function(result) {
    +      .then(function (result) {
             close();
             dispatch({ type: 'message', data: result });
           })
    -      .catch(function(e) {
    +      .catch(function (e) {
             close();
             dispatch(errorEvent(e));
           });
       });
     };
    -export const toPromise = function(target, cb, eventName = 'message', errorName = 'error') {
    -  return new Promise(function(resolve, reject) {
    +export const toPromise = function (target, cb, eventName = 'message', errorName = 'error') {
    +  return new Promise(function (resolve, reject) {
         // TODO: e.target.data
    -    const message = function(e) {
    +    const message = function (e) {
           resolve(e.data);
         };
    -    const error = function(e) {
    +    const error = function (e) {
           reject(e.error);
         };
    -    const remove = function() {
    +    const remove = function () {
           if (typeof target.close === 'function') {
             target.close();
           }
    @@ -131,14 +131,14 @@ export const toPromise = function(target, cb, eventName = 'message', errorName =
         cb(remove);
       });
     };
    -export const once = function(cb, configuration, Source = OpenableEventSource) {
    -  return new Source(function(configuration, source) {
    +export const once = function (cb, configuration, Source = OpenableEventSource) {
    +  return new Source(function (configuration, source) {
         return cb(configuration, source)
    -      .then(function(data) {
    +      .then(function (data) {
             source.dispatchEvent({ type: 'message', data: data });
             source.close();
           })
    -      .catch(function(e) {
    +      .catch(function (e) {
             source.dispatchEvent({ type: 'error', error: e });
             source.close();
           });
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/openable.js b/ui/packages/consul-ui/app/utils/dom/event-source/openable.js
    index a74502205..516f74ea1 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/openable.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/openable.js
    @@ -3,8 +3,8 @@
      *
      * @param {Class} eventSource - EventSource class to extend from
      */
    -export default function(eventSource = EventSource) {
    -  const OpenableEventSource = function(source, configuration = {}) {
    +export default function (eventSource = EventSource) {
    +  const OpenableEventSource = function (source, configuration = {}) {
         eventSource.apply(this, arguments);
         this.configuration = configuration;
       };
    @@ -17,7 +17,7 @@ export default function(eventSource = EventSource) {
           },
         }),
         {
    -      open: function() {
    +      open: function () {
             switch (this.readyState) {
               case 3: // CLOSING
                 this.readyState = 1;
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/proxy.js b/ui/packages/consul-ui/app/utils/dom/event-source/proxy.js
    index eca66e184..333e1322b 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/proxy.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/proxy.js
    @@ -1,44 +1,44 @@
     import { get, set } from '@ember/object';
     
     const proxies = {};
    -export default function(ObjProxy, ArrProxy, createListeners) {
    -  return function(source, data = []) {
    +export default function (ObjProxy, ArrProxy, createListeners) {
    +  return function (source, data = []) {
         let Proxy = ObjProxy;
         // TODO: When is data ever a string?
         let type = 'object';
         if (typeof data !== 'string' && typeof get(data, 'length') !== 'undefined') {
           Proxy = ArrProxy;
           type = 'array';
    -      data = data.filter(function(item) {
    +      data = data.filter(function (item) {
             return !get(item, 'isDestroyed') && !get(item, 'isDeleted') && get(item, 'isLoaded');
           });
         }
         if (typeof proxies[type] === 'undefined') {
           proxies[type] = Proxy.extend({
    -        init: function() {
    +        init: function () {
               this.listeners = createListeners();
    -          this.listeners.add(this._source, 'message', e => set(this, 'content', e.data));
    +          this.listeners.add(this._source, 'message', (e) => set(this, 'content', e.data));
               this._super(...arguments);
             },
    -        addEventListener: function(type, handler) {
    +        addEventListener: function (type, handler) {
               this.listeners.add(this._source, type, handler);
             },
    -        getCurrentEvent: function() {
    +        getCurrentEvent: function () {
               return this._source.getCurrentEvent(...arguments);
             },
    -        removeEventListener: function() {
    +        removeEventListener: function () {
               return this._source.removeEventListener(...arguments);
             },
    -        dispatchEvent: function() {
    +        dispatchEvent: function () {
               return this._source.dispatchEvent(...arguments);
             },
    -        close: function() {
    +        close: function () {
               return this._source.close(...arguments);
             },
    -        open: function() {
    +        open: function () {
               return this._source.open(...arguments);
             },
    -        willDestroy: function() {
    +        willDestroy: function () {
               this._super(...arguments);
               this.close();
               this.listeners.remove();
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/resolver.js b/ui/packages/consul-ui/app/utils/dom/event-source/resolver.js
    index fcab16350..3046f2da3 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/resolver.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/resolver.js
    @@ -1,26 +1,26 @@
    -export default function(P = Promise) {
    -  return function(source, listeners) {
    +export default function (P = Promise) {
    +  return function (source, listeners) {
         let current;
         if (typeof source.getCurrentEvent === 'function') {
           current = source.getCurrentEvent();
         }
         if (current != null) {
           // immediately resolve if we have previous cached data
    -      return P.resolve(current.data).then(function(cached) {
    +      return P.resolve(current.data).then(function (cached) {
             source.open();
             return cached;
           });
         }
         // if we have no previously cached data, listen for the first response
    -    return new P(function(resolve, reject) {
    +    return new P(function (resolve, reject) {
           // close, cleanup and reject if we get an error
    -      listeners.add(source, 'error', function(e) {
    +      listeners.add(source, 'error', function (e) {
             listeners.remove();
             e.target.close();
             reject(e.error);
           });
           // ...or cleanup and respond with the first lot of data
    -      listeners.add(source, 'message', function(e) {
    +      listeners.add(source, 'message', function (e) {
             listeners.remove();
             resolve(e.data);
           });
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-source/storage.js b/ui/packages/consul-ui/app/utils/dom/event-source/storage.js
    index 602c6af3d..183b4434c 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-source/storage.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-source/storage.js
    @@ -1,10 +1,10 @@
    -export default function(EventTarget, P = Promise) {
    -  const handler = function(e) {
    +export default function (EventTarget, P = Promise) {
    +  const handler = function (e) {
         // e is undefined on the opening call
         if (typeof e === 'undefined' || e.key === this.configuration.key) {
           if (this.readyState === 1) {
             const res = this.source(this.configuration);
    -        P.resolve(res).then(data => {
    +        P.resolve(res).then((data) => {
               this.configuration.cursor++;
               this._currentEvent = { type: 'message', data: data };
               this.dispatchEvent({ type: 'message', data: data });
    diff --git a/ui/packages/consul-ui/app/utils/dom/event-target/rsvp.js b/ui/packages/consul-ui/app/utils/dom/event-target/rsvp.js
    index d1a108663..ee54c8672 100644
    --- a/ui/packages/consul-ui/app/utils/dom/event-target/rsvp.js
    +++ b/ui/packages/consul-ui/app/utils/dom/event-target/rsvp.js
    @@ -4,7 +4,7 @@ import RSVP from 'rsvp';
     // The MIT License (MIT) - Copyright (c) 2015 Toru Nagashima
     import { setCurrentTarget, wrapEvent } from './event-target-shim/event';
     
    -const EventTarget = function() {};
    +const EventTarget = function () {};
     function callbacksFor(object) {
       let callbacks = object._promiseCallbacks;
     
    @@ -23,7 +23,7 @@ EventTarget.prototype = Object.assign(
         },
       }),
       {
    -    dispatchEvent: function(obj) {
    +    dispatchEvent: function (obj) {
           // borrow just what I need from event-target-shim
           // to make true events even ErrorEvents with targets
           const wrappedEvent = wrapEvent(this, obj);
    @@ -47,10 +47,10 @@ EventTarget.prototype = Object.assign(
             }
           }
         },
    -    addEventListener: function(event, cb) {
    +    addEventListener: function (event, cb) {
           this.on(event, cb);
         },
    -    removeEventListener: function(event, cb) {
    +    removeEventListener: function (event, cb) {
           try {
             this.off(event, cb);
           } catch (e) {
    diff --git a/ui/packages/consul-ui/app/utils/dom/get-component-factory.js b/ui/packages/consul-ui/app/utils/dom/get-component-factory.js
    index 5ebc174ca..9bd69c344 100644
    --- a/ui/packages/consul-ui/app/utils/dom/get-component-factory.js
    +++ b/ui/packages/consul-ui/app/utils/dom/get-component-factory.js
    @@ -1,6 +1,6 @@
    -export default function(owner, key = '-view-registry:main') {
    +export default function (owner, key = '-view-registry:main') {
       const components = owner.lookup(key);
    -  return function(el) {
    +  return function (el) {
         const id = el.getAttribute('id');
         if (id) {
           return components[id];
    diff --git a/ui/packages/consul-ui/app/utils/dom/is-outside.js b/ui/packages/consul-ui/app/utils/dom/is-outside.js
    index d069a361d..378140a38 100644
    --- a/ui/packages/consul-ui/app/utils/dom/is-outside.js
    +++ b/ui/packages/consul-ui/app/utils/dom/is-outside.js
    @@ -1,4 +1,4 @@
    -export default function(el, target, doc = document) {
    +export default function (el, target, doc = document) {
       if (el) {
         // TODO: Potentially type check el and target
         // look to see what .contains does when it gets an unexpected type
    diff --git a/ui/packages/consul-ui/app/utils/dom/normalize-event.js b/ui/packages/consul-ui/app/utils/dom/normalize-event.js
    index b61f11b79..4b0f41ffe 100644
    --- a/ui/packages/consul-ui/app/utils/dom/normalize-event.js
    +++ b/ui/packages/consul-ui/app/utils/dom/normalize-event.js
    @@ -1,4 +1,4 @@
    -export default function(e, value, target = {}) {
    +export default function (e, value, target = {}) {
       if (typeof e.target !== 'undefined') {
         return e;
       }
    diff --git a/ui/packages/consul-ui/app/utils/dom/qsa-factory.js b/ui/packages/consul-ui/app/utils/dom/qsa-factory.js
    index ff5770a34..592db4749 100644
    --- a/ui/packages/consul-ui/app/utils/dom/qsa-factory.js
    +++ b/ui/packages/consul-ui/app/utils/dom/qsa-factory.js
    @@ -1,5 +1,5 @@
    -export default function(doc = document) {
    -  return function(sel, context = doc) {
    +export default function (doc = document) {
    +  return function (sel, context = doc) {
         return context.querySelectorAll(sel);
       };
     }
    diff --git a/ui/packages/consul-ui/app/utils/dom/sibling.js b/ui/packages/consul-ui/app/utils/dom/sibling.js
    index 9c6cee85b..cd2708990 100644
    --- a/ui/packages/consul-ui/app/utils/dom/sibling.js
    +++ b/ui/packages/consul-ui/app/utils/dom/sibling.js
    @@ -1,4 +1,4 @@
    -export default function(el, name) {
    +export default function (el, name) {
       let sibling = el;
       while ((sibling = sibling.nextSibling)) {
         if (sibling.nodeType === 1) {
    diff --git a/ui/packages/consul-ui/app/utils/editor/lint.js b/ui/packages/consul-ui/app/utils/editor/lint.js
    index b77b65d21..ed38c4cfe 100644
    --- a/ui/packages/consul-ui/app/utils/editor/lint.js
    +++ b/ui/packages/consul-ui/app/utils/editor/lint.js
    @@ -5,16 +5,16 @@
     // follow more or less what CodeMirror does but doesn't expose
     // see codemirror/addon/mode/loadmode.js
     
    -export const createLoader = function(
    +export const createLoader = function (
       $$ = document.getElementsByTagName.bind(document),
       CM = CodeMirror
     ) {
    -  CM.registerHelper('lint', 'ruby', function(text) {
    +  CM.registerHelper('lint', 'ruby', function (text) {
         return [];
       });
    -  return function(editor, mode, cb) {
    +  return function (editor, mode, cb) {
         let scripts = [...$$('script')];
    -    const loaded = scripts.find(function(item) {
    +    const loaded = scripts.find(function (item) {
           return item.src.indexOf(`/codemirror/mode/${mode}/${mode}.js`) !== -1;
         });
         CM.autoLoadMode(editor, mode);
    @@ -22,15 +22,15 @@ export const createLoader = function(
           cb();
         } else {
           scripts = [...$$('script')];
    -      CM.on(scripts[0], 'load', function() {
    +      CM.on(scripts[0], 'load', function () {
             cb();
           });
         }
       };
     };
     const load = createLoader();
    -export default function(editor, mode) {
    -  load(editor, mode, function() {
    +export default function (editor, mode) {
    +  load(editor, mode, function () {
         if (editor.getValue().trim().length) {
           editor.performLint();
         }
    diff --git a/ui/packages/consul-ui/app/utils/filter/index.js b/ui/packages/consul-ui/app/utils/filter/index.js
    index 4b4d16cb2..1791634ab 100644
    --- a/ui/packages/consul-ui/app/utils/filter/index.js
    +++ b/ui/packages/consul-ui/app/utils/filter/index.js
    @@ -1,6 +1,6 @@
     import setHelpers from 'mnemonist/set';
     
    -const createPossibles = function(predicates) {
    +const createPossibles = function (predicates) {
       // create arrays of allowed values
       return Object.entries(predicates).reduce((prev, [key, value]) => {
         if (typeof value !== 'function') {
    @@ -11,7 +11,7 @@ const createPossibles = function(predicates) {
         return prev;
       }, {});
     };
    -const sanitize = function(values, possibles) {
    +const sanitize = function (values, possibles) {
       return Object.keys(possibles).reduce((prev, key) => {
         // only set the value if the value has a length of > 0
         const value = typeof values[key] === 'undefined' ? [] : values[key];
    @@ -27,7 +27,7 @@ const sanitize = function(values, possibles) {
         return prev;
       }, {});
     };
    -const execute = function(item, values, predicates) {
    +const execute = function (item, values, predicates) {
       // every/and the top level values
       return Object.entries(values).every(([key, values]) => {
         let predicate = predicates[key];
    @@ -35,21 +35,21 @@ const execute = function(item, values, predicates) {
           return predicate(item, values);
         } else {
           // if the top level values can have multiple values some/or them
    -      return values.some(val => predicate[val](item, val));
    +      return values.some((val) => predicate[val](item, val));
         }
       });
     };
     // exports a function that requires a hash of predicates passed in
    -export const andOr = predicates => {
    +export const andOr = (predicates) => {
       // figure out all possible values from the hash of predicates
       const possibles = createPossibles(predicates);
    -  return values => {
    +  return (values) => {
         // this is what is called post injection
         // the actual user values are passed in here so 'sanitize' them which is
         // basically checking against the possibles
         values = sanitize(values, possibles);
         // this is your actual filter predicate
    -    return item => {
    +    return (item) => {
           return execute(item, values, predicates);
         };
       };
    diff --git a/ui/packages/consul-ui/app/utils/form/builder.js b/ui/packages/consul-ui/app/utils/form/builder.js
    index 8d3276891..e0b140aa4 100644
    --- a/ui/packages/consul-ui/app/utils/form/builder.js
    +++ b/ui/packages/consul-ui/app/utils/form/builder.js
    @@ -6,7 +6,7 @@ import lookupValidator from 'ember-changeset-validations';
     // Keep these here for now so forms are easy to make
     // TODO: Probably move this to utils/form/parse-element-name
     import parseElementName from 'consul-ui/utils/get-form-name-property';
    -export const defaultChangeset = function(data, validators) {
    +export const defaultChangeset = function (data, validators) {
       return createChangeset(data, lookupValidator(validators), validators, { changeset: Changeset });
     };
     /**
    @@ -26,18 +26,18 @@ export const defaultChangeset = function(data, validators) {
      *   currently the only supported property of these configuration objects is `type` which currently allows you to
      *   set a property as 'array-like'
      */
    -export default function(changeset = defaultChangeset, getFormNameProperty = parseElementName) {
    -  return function(name = '', obj = {}) {
    +export default function (changeset = defaultChangeset, getFormNameProperty = parseElementName) {
    +  return function (name = '', obj = {}) {
         const _children = {};
         let _validators = null;
         // TODO make this into a class to reuse prototype
         const form = {
           data: null,
           name: name,
    -      getName: function() {
    +      getName: function () {
             return this.name;
           },
    -      setData: function(data) {
    +      setData: function (data) {
             // Array check temporarily for when we get an empty array from repo.status
             if (_validators && !Array.isArray(data)) {
               data = changeset(data, _validators);
    @@ -45,14 +45,14 @@ export default function(changeset = defaultChangeset, getFormNameProperty = pars
             set(this, 'data', data);
             return this;
           },
    -      getData: function() {
    +      getData: function () {
             return this.data;
           },
    -      add: function(child) {
    +      add: function (child) {
             _children[child.getName()] = child;
             return this;
           },
    -      handleEvent: function(e, targetName) {
    +      handleEvent: function (e, targetName) {
             const target = e.target;
             // currently we only use targetName in {{form-component}} for handling deeply
             // nested forms, once {{form-component}} handles deeply nested forms targetName can go
    @@ -118,32 +118,32 @@ export default function(changeset = defaultChangeset, getFormNameProperty = pars
             // validate everything
             return this.validate();
           },
    -      reset: function() {
    +      reset: function () {
             const data = this.getData();
             if (typeof data.rollbackAttributes === 'function') {
               this.getData().rollbackAttributes();
             }
             return this;
           },
    -      clear: function(cb = {}) {
    +      clear: function (cb = {}) {
             if (typeof cb === 'function') {
               return (this.clearer = cb);
             } else {
               return this.setData(this.clearer(cb)).getData();
             }
           },
    -      submit: function(cb = {}) {
    +      submit: function (cb = {}) {
             if (typeof cb === 'function') {
               return (this.submitter = cb);
             } else {
               this.submitter(this.getData());
             }
           },
    -      setValidators: function(validators) {
    +      setValidators: function (validators) {
             _validators = validators;
             return this;
           },
    -      validate: function() {
    +      validate: function () {
             const data = this.getData();
             // just pass along to the Changeset for now
             if (typeof data.validate === 'function') {
    @@ -151,19 +151,19 @@ export default function(changeset = defaultChangeset, getFormNameProperty = pars
             }
             return this;
           },
    -      addError: function(name, message) {
    +      addError: function (name, message) {
             const data = this.getData();
             if (typeof data.addError === 'function') {
               data.addError(...arguments);
             }
           },
    -      form: function(name) {
    +      form: function (name) {
             if (name == null) {
               return this;
             }
             return _children[name];
           },
    -      has: function(name) {
    +      has: function (name) {
             return typeof _children[name] !== 'undefined';
           },
         };
    diff --git a/ui/packages/consul-ui/app/utils/get-environment.js b/ui/packages/consul-ui/app/utils/get-environment.js
    index fd3757fbf..264e5c8e4 100644
    --- a/ui/packages/consul-ui/app/utils/get-environment.js
    +++ b/ui/packages/consul-ui/app/utils/get-environment.js
    @@ -7,25 +7,20 @@ import { runInDebug } from '@ember/debug';
     // 3. Those that can be set only during development by adding cookie values
     // via the browsers Web Inspector, or via the browsers hash (#COOKIE_NAME=1),
     // which is useful for showing the UI with various settings enabled/disabled
    -export default function(config = {}, win = window, doc = document) {
    +export default function (config = {}, win = window, doc = document) {
       // look at the hash in the URL and transfer anything after the hash into
       // cookies to enable linking of the UI with various settings enabled
       runInDebug(() => {
    -    const cookies = function(str) {
    +    const cookies = function (str) {
           return str
             .split(';')
    -        .map(item => item.trim())
    -        .filter(item => item !== '')
    -        .filter(item =>
    -          item
    -            .split('=')
    -            .shift()
    -            .startsWith('CONSUL_')
    -        );
    +        .map((item) => item.trim())
    +        .filter((item) => item !== '')
    +        .filter((item) => item.split('=').shift().startsWith('CONSUL_'));
         };
    -    win['Scenario'] = function(str = '') {
    +    win['Scenario'] = function (str = '') {
           if (str.length > 0) {
    -        cookies(str).forEach(item => {
    +        cookies(str).forEach((item) => {
               // this current outlier is the only one that
               // 1. Toggles
               // 2. Uses localStorage
    @@ -68,23 +63,23 @@ export default function(config = {}, win = window, doc = document) {
           win['Scenario'](win.location.hash.substr(1));
         }
       });
    -  const dev = function(str = doc.cookie) {
    +  const dev = function (str = doc.cookie) {
         return str
           .split(';')
    -      .filter(item => item !== '')
    -      .map(item => {
    +      .filter((item) => item !== '')
    +      .map((item) => {
             const [key, ...rest] = item.trim().split('=');
             return [key, rest.join('=')];
           });
       };
    -  const user = function(str) {
    +  const user = function (str) {
         const item = win.localStorage.getItem(str);
         return item === null ? undefined : item;
       };
    -  const getResourceFor = function(src) {
    +  const getResourceFor = function (src) {
         try {
           return (
    -        win.performance.getEntriesByType('resource').find(item => {
    +        win.performance.getEntriesByType('resource').find((item) => {
               return item.initiatorType === 'script' && src === item.name;
             }) || {}
           );
    @@ -107,7 +102,7 @@ export default function(config = {}, win = window, doc = document) {
       // turning off blocking queries if its a busy cluster
       // forcing/providing amount of possible HTTP connections
       // re-setting the base url for the API etc
    -  const operator = function(str, env) {
    +  const operator = function (str, env) {
         let protocol, dashboards, provider, proxy;
         switch (str) {
           case 'CONSUL_NSPACES_ENABLED':
    @@ -128,10 +123,24 @@ export default function(config = {}, win = window, doc = document) {
             return typeof operatorConfig.PeeringEnabled === 'undefined'
               ? false
               : operatorConfig.PeeringEnabled;
    +      case 'CONSUL_HCP_ENABLED':
    +        return typeof operatorConfig.HCPEnabled === 'undefined'
    +          ? false
    +          : operatorConfig.HCPEnabled;
           case 'CONSUL_DATACENTER_LOCAL':
             return operatorConfig.LocalDatacenter;
           case 'CONSUL_DATACENTER_PRIMARY':
             return operatorConfig.PrimaryDatacenter;
    +      case 'CONSUL_HCP_MANAGED_RUNTIME':
    +        return operatorConfig.HCPManagedRuntime;
    +      case 'CONSUL_API_PREFIX':
    +        // we want API prefix to look like an env var for if we ever change
    +        // operator config to be an API request, we need this variable before we
    +        // make and API request so this specific variable should never be be
    +        // retrived via an API request
    +        return operatorConfig.APIPrefix;
    +      case 'CONSUL_HCP_URL':
    +        return operatorConfig.HCPURL;
           case 'CONSUL_UI_CONFIG':
             dashboards = {
               service: undefined,
    @@ -150,10 +159,7 @@ export default function(config = {}, win = window, doc = document) {
             }
             return ui_config;
           case 'CONSUL_BASE_UI_URL':
    -        return currentSrc
    -          .split('/')
    -          .slice(0, -2)
    -          .join('/');
    +        return currentSrc.split('/').slice(0, -2).join('/');
           case 'CONSUL_HTTP_PROTOCOL':
             if (typeof resource === 'undefined') {
               // resource needs to be retrieved lazily as entries aren't guaranteed
    @@ -183,14 +189,14 @@ export default function(config = {}, win = window, doc = document) {
             }
         }
       };
    -  const ui = function(key) {
    -    let $;
    +  const ui = function (key) {
    +    let $ = {};
         switch (config.environment) {
           case 'development':
           case 'staging':
           case 'coverage':
           case 'test':
    -        $ = dev().reduce(function(prev, [key, value]) {
    +        $ = dev().reduce(function (prev, [key, value]) {
               switch (key) {
                 case 'CONSUL_INTL_LOCALE':
                   prev['CONSUL_INTL_LOCALE'] = String(value).toLowerCase();
    @@ -216,18 +222,34 @@ export default function(config = {}, win = window, doc = document) {
                 case 'CONSUL_PEERINGS_ENABLE':
                   prev['CONSUL_PEERINGS_ENABLED'] = !!JSON.parse(String(value).toLowerCase());
                   break;
    +            case 'CONSUL_HCP_ENABLE':
    +              prev['CONSUL_HCP_ENABLED'] = !!JSON.parse(String(value).toLowerCase());
    +              break;
                 case 'CONSUL_UI_CONFIG':
                   prev['CONSUL_UI_CONFIG'] = JSON.parse(value);
                   break;
    +            case 'TokenSecretID':
    +              prev['CONSUL_HTTP_TOKEN'] = value;
    +              break;
                 default:
                   prev[key] = value;
               }
               return prev;
             }, {});
    -        if (typeof $[key] !== 'undefined') {
    -          return $[key];
    -        }
             break;
    +      case 'production':
    +        $ = dev().reduce(function (prev, [key, value]) {
    +          switch (key) {
    +            case 'TokenSecretID':
    +              prev['CONSUL_HTTP_TOKEN'] = value;
    +              break;
    +          }
    +          return prev;
    +        }, {});
    +        break;
    +    }
    +    if (typeof $[key] !== 'undefined') {
    +      return $[key];
         }
         return config[key];
       };
    @@ -246,9 +268,13 @@ export default function(config = {}, win = window, doc = document) {
           case 'CONSUL_UI_CONFIG':
           case 'CONSUL_DATACENTER_LOCAL':
           case 'CONSUL_DATACENTER_PRIMARY':
    +      case 'CONSUL_HCP_MANAGED_RUNTIME':
    +      case 'CONSUL_API_PREFIX':
    +      case 'CONSUL_HCP_URL':
           case 'CONSUL_ACLS_ENABLED':
           case 'CONSUL_NSPACES_ENABLED':
           case 'CONSUL_PEERINGS_ENABLED':
    +      case 'CONSUL_HCP_ENABLED':
           case 'CONSUL_SSO_ENABLED':
           case 'CONSUL_PARTITIONS_ENABLED':
           case 'CONSUL_METRICS_PROVIDER':
    diff --git a/ui/packages/consul-ui/app/utils/get-form-name-property.js b/ui/packages/consul-ui/app/utils/get-form-name-property.js
    index 50379c6f3..4ce874621 100644
    --- a/ui/packages/consul-ui/app/utils/get-form-name-property.js
    +++ b/ui/packages/consul-ui/app/utils/get-form-name-property.js
    @@ -1,4 +1,4 @@
    -export default function(name) {
    +export default function (name) {
       if (name.indexOf('[') !== -1) {
         return name.match(/(.*)\[(.*)\]/).slice(1);
       }
    diff --git a/ui/packages/consul-ui/app/utils/helpers/call-if-type.js b/ui/packages/consul-ui/app/utils/helpers/call-if-type.js
    index fa751d1e0..73a58a5db 100644
    --- a/ui/packages/consul-ui/app/utils/helpers/call-if-type.js
    +++ b/ui/packages/consul-ui/app/utils/helpers/call-if-type.js
    @@ -1,6 +1,6 @@
    -export default function(type) {
    -  return function(cb) {
    -    return function(params, hash = {}) {
    +export default function (type) {
    +  return function (cb) {
    +    return function (params, hash = {}) {
           if (typeof params[0] !== type) {
             return params[0];
           }
    diff --git a/ui/packages/consul-ui/app/utils/http/create-headers.js b/ui/packages/consul-ui/app/utils/http/create-headers.js
    index 43f30094c..9ead9e7ce 100644
    --- a/ui/packages/consul-ui/app/utils/http/create-headers.js
    +++ b/ui/packages/consul-ui/app/utils/http/create-headers.js
    @@ -1,6 +1,6 @@
    -export default function() {
    -  return function(lines) {
    -    return lines.reduce(function(prev, item) {
    +export default function () {
    +  return function (lines) {
    +    return lines.reduce(function (prev, item) {
           const [key, ...value] = item.split(':');
           if (value.length > 0) {
             prev[key.trim()] = value.join(':').trim();
    diff --git a/ui/packages/consul-ui/app/utils/http/create-query-params.js b/ui/packages/consul-ui/app/utils/http/create-query-params.js
    index 5b4bcd139..aade6f0b5 100644
    --- a/ui/packages/consul-ui/app/utils/http/create-query-params.js
    +++ b/ui/packages/consul-ui/app/utils/http/create-query-params.js
    @@ -1,7 +1,7 @@
    -export default function(encode = encodeURIComponent) {
    +export default function (encode = encodeURIComponent) {
       return function stringify(obj, parent) {
         return Object.entries(obj)
    -      .reduce(function(prev, [key, value], i) {
    +      .reduce(function (prev, [key, value], i) {
             // if the value is undefined do nothing
             if (typeof value === 'undefined') {
               return prev;
    diff --git a/ui/packages/consul-ui/app/utils/http/create-url.js b/ui/packages/consul-ui/app/utils/http/create-url.js
    index 2405ecdd7..aa0f8096c 100644
    --- a/ui/packages/consul-ui/app/utils/http/create-url.js
    +++ b/ui/packages/consul-ui/app/utils/http/create-url.js
    @@ -3,8 +3,8 @@ const PATH_PARSING = 1;
     const QUERY_PARSING = 2;
     const HEADER_PARSING = 3;
     const BODY_PARSING = 4;
    -export default function(encode, queryParams) {
    -  return function(strs, ...values) {
    +export default function (encode, queryParams) {
    +  return function (strs, ...values) {
         // TODO: Potentially url should check if any of the params
         // passed to it are undefined (null is fine). We could then get rid of the
         // multitude of checks we do throughout the adapters
    @@ -12,7 +12,7 @@ export default function(encode, queryParams) {
         // anywhere
         let state = PATH_PARSING;
         return strs
    -      .map(function(item, i, arr) {
    +      .map(function (item, i, arr) {
             if (i === 0) {
               item = item.trimStart();
             }
    @@ -39,7 +39,7 @@ export default function(encode, queryParams) {
                   // split encode and join arrays by `/`
                   case Array.isArray(val):
                     val = val
    -                  .map(function(item) {
    +                  .map(function (item) {
                         return `${encode(item)}`;
                       }, '')
                       .join('/');
    diff --git a/ui/packages/consul-ui/app/utils/http/xhr.js b/ui/packages/consul-ui/app/utils/http/xhr.js
    index cbdea6411..29440f2a2 100644
    --- a/ui/packages/consul-ui/app/utils/http/xhr.js
    +++ b/ui/packages/consul-ui/app/utils/http/xhr.js
    @@ -1,7 +1,7 @@
    -export default function(parseHeaders, XHR) {
    -  return function(options) {
    +export default function (parseHeaders, XHR) {
    +  return function (options) {
         const xhr = new (XHR || XMLHttpRequest)();
    -    xhr.onreadystatechange = function() {
    +    xhr.onreadystatechange = function () {
           if (this.readyState === 4) {
             const headers = parseHeaders(this.getAllResponseHeaders().split('\n'));
             if (this.status >= 200 && this.status < 400) {
    @@ -27,6 +27,7 @@ export default function(parseHeaders, XHR) {
         };
         Object.entries(headers).forEach(([key, value]) => xhr.setRequestHeader(key, value));
         options.beforeSend(xhr);
    +    xhr.withCredentials = true;
         xhr.send(options.body);
         return xhr;
       };
    diff --git a/ui/packages/consul-ui/app/utils/intl/missing-message.js b/ui/packages/consul-ui/app/utils/intl/missing-message.js
    index a454243ca..3d1758659 100644
    --- a/ui/packages/consul-ui/app/utils/intl/missing-message.js
    +++ b/ui/packages/consul-ui/app/utils/intl/missing-message.js
    @@ -4,13 +4,7 @@ import { runInDebug } from '@ember/debug';
     // if we can't find the message, take the last part of the identifier and
     // ucfirst it so it looks human
     export default function missingMessage(key, locales) {
    -  runInDebug(
    -    _ => console.debug(`Translation key not found: ${key}`)
    -  );
    -  const last = key
    -    .split('.')
    -    .pop()
    -    .split('-')
    -    .join(' ');
    +  runInDebug((_) => console.debug(`Translation key not found: ${key}`));
    +  const last = key.split('.').pop().split('-').join(' ');
       return `${last.substr(0, 1).toUpperCase()}${last.substr(1)}`;
     }
    diff --git a/ui/packages/consul-ui/app/utils/isFolder.js b/ui/packages/consul-ui/app/utils/isFolder.js
    index 3a8d795da..51b8c146a 100644
    --- a/ui/packages/consul-ui/app/utils/isFolder.js
    +++ b/ui/packages/consul-ui/app/utils/isFolder.js
    @@ -1,5 +1,5 @@
     // Boolean if the key is a "folder" or not, i.e is a nested key
     // that feels like a folder.
    -export default function(path = '') {
    +export default function (path = '') {
       return path.slice(-1) === '/';
     }
    diff --git a/ui/packages/consul-ui/app/utils/keyToArray.js b/ui/packages/consul-ui/app/utils/keyToArray.js
    index 47ac07449..316125f75 100644
    --- a/ui/packages/consul-ui/app/utils/keyToArray.js
    +++ b/ui/packages/consul-ui/app/utils/keyToArray.js
    @@ -7,6 +7,6 @@
      * @param {String} separator - The separator
      * @returns {String[]}
      */
    -export default function(key, separator = '/') {
    +export default function (key, separator = '/') {
       return (key === separator ? '' : key).split(separator);
     }
    diff --git a/ui/packages/consul-ui/app/utils/maybe-call.js b/ui/packages/consul-ui/app/utils/maybe-call.js
    index de501b6d5..c76b8a61c 100644
    --- a/ui/packages/consul-ui/app/utils/maybe-call.js
    +++ b/ui/packages/consul-ui/app/utils/maybe-call.js
    @@ -5,9 +5,9 @@
      * @param {Promise} [what] - A boolean resolving promise
      * @returns {function} - function when called returns a Promise that resolves the argument it is called with
      */
    -export default function(cb, what) {
    -  return function(res) {
    -    return what.then(function(bool) {
    +export default function (cb, what) {
    +  return function (res) {
    +    return what.then(function (bool) {
           if (bool) {
             cb();
           }
    diff --git a/ui/packages/consul-ui/app/utils/merge-checks.js b/ui/packages/consul-ui/app/utils/merge-checks.js
    index 99b33feb4..9f444cdb6 100644
    --- a/ui/packages/consul-ui/app/utils/merge-checks.js
    +++ b/ui/packages/consul-ui/app/utils/merge-checks.js
    @@ -17,7 +17,7 @@ export default (checks = [], exposed = false, MMap = MultiMap) => {
       const ids = new MMap();
       const a = checks.shift();
       const result = a
    -    .map(item => {
    +    .map((item) => {
           // its a Node check (ServiceName === ""), record this one so we
           // don't end up with duplicates of it
           if (item.ServiceName === '') {
    @@ -53,8 +53,8 @@ export default (checks = [], exposed = false, MMap = MultiMap) => {
       // TODO: consider moving this out of here so we aren't doing too much in one util
       if (exposed) {
         result
    -      .filter(item => get(item, 'Exposable'))
    -      .forEach(item => {
    +      .filter((item) => get(item, 'Exposable'))
    +      .forEach((item) => {
             set(item, 'Exposed', exposed);
           });
       }
    diff --git a/ui/packages/consul-ui/app/utils/minimizeModel.js b/ui/packages/consul-ui/app/utils/minimizeModel.js
    index 6411ad8ce..c48819c7a 100644
    --- a/ui/packages/consul-ui/app/utils/minimizeModel.js
    +++ b/ui/packages/consul-ui/app/utils/minimizeModel.js
    @@ -1,13 +1,13 @@
     import { get } from '@ember/object';
     
    -export default function(arr) {
    +export default function (arr) {
       if (Array.isArray(arr)) {
         return arr
    -      .filter(function(item) {
    +      .filter(function (item) {
             // Just incase, don't save any models that aren't saved
             return !get(item, 'isNew');
           })
    -      .map(function(item) {
    +      .map(function (item) {
             return {
               ID: get(item, 'ID'),
               Name: get(item, 'Name'),
    diff --git a/ui/packages/consul-ui/app/utils/non-empty-set.js b/ui/packages/consul-ui/app/utils/non-empty-set.js
    index d9ba2df68..b2ca9de05 100644
    --- a/ui/packages/consul-ui/app/utils/non-empty-set.js
    +++ b/ui/packages/consul-ui/app/utils/non-empty-set.js
    @@ -1,5 +1,5 @@
    -export default function(prop) {
    -  return function(value) {
    +export default function (prop) {
    +  return function (value) {
         if (typeof value === 'undefined' || value === null || value === '') {
           return {};
         } else {
    diff --git a/ui/packages/consul-ui/app/utils/promisedTimeout.js b/ui/packages/consul-ui/app/utils/promisedTimeout.js
    index cd16f0987..f7984bde3 100644
    --- a/ui/packages/consul-ui/app/utils/promisedTimeout.js
    +++ b/ui/packages/consul-ui/app/utils/promisedTimeout.js
    @@ -1,11 +1,11 @@
    -export default function(P = Promise, timeout = setTimeout) {
    +export default function (P = Promise, timeout = setTimeout) {
       // var interval;
    -  return function(milliseconds, cb = function() {}) {
    +  return function (milliseconds, cb = function () {}) {
         // clearInterval(interval);
         // const cb = typeof _cb !== 'function' ? (i) => { clearInterval(interval);interval = i; } : _cb;
         return new P((resolve, reject) => {
           cb(
    -        timeout(function() {
    +        timeout(function () {
               resolve(milliseconds);
             }, milliseconds)
           );
    diff --git a/ui/packages/consul-ui/app/utils/routing/redirect-to.js b/ui/packages/consul-ui/app/utils/routing/redirect-to.js
    index 38fb3fb28..cfdd0dd5e 100644
    --- a/ui/packages/consul-ui/app/utils/routing/redirect-to.js
    +++ b/ui/packages/consul-ui/app/utils/routing/redirect-to.js
    @@ -1,9 +1,6 @@
    -export default function(to, route) {
    -  return function(model, transition) {
    -    const parent = this.routeName
    -      .split('.')
    -      .slice(0, -1)
    -      .join('.');
    +export default function (to, route) {
    +  return function (model, transition) {
    +    const parent = this.routeName.split('.').slice(0, -1).join('.');
         this.replaceWith(`${parent}.${to}`, model);
       };
     }
    diff --git a/ui/packages/consul-ui/app/utils/routing/transitionable.js b/ui/packages/consul-ui/app/utils/routing/transitionable.js
    index e1a2098da..81bf23dc5 100644
    --- a/ui/packages/consul-ui/app/utils/routing/transitionable.js
    +++ b/ui/packages/consul-ui/app/utils/routing/transitionable.js
    @@ -1,9 +1,9 @@
    -const filter = function(routeName, atts, params) {
    +const filter = function (routeName, atts, params) {
       return [routeName, ...atts];
     };
    -const replaceRouteParams = function(route, params = {}) {
    +const replaceRouteParams = function (route, params = {}) {
       return (route.paramNames || [])
    -    .map(function(item) {
    +    .map(function (item) {
           if (typeof params[item] !== 'undefined') {
             return params[item];
           }
    @@ -11,7 +11,7 @@ const replaceRouteParams = function(route, params = {}) {
         })
         .reverse();
     };
    -export default function(route, params = {}, container) {
    +export default function (route, params = {}, container) {
       if (route === null) {
         route = container.lookup('route:application');
       }
    diff --git a/ui/packages/consul-ui/app/utils/routing/walk.js b/ui/packages/consul-ui/app/utils/routing/walk.js
    index 388f4ce21..9e2556614 100644
    --- a/ui/packages/consul-ui/app/utils/routing/walk.js
    +++ b/ui/packages/consul-ui/app/utils/routing/walk.js
    @@ -1,18 +1,18 @@
     import { runInDebug } from '@ember/debug';
     
    -export const walk = function(routes) {
    +export const walk = function (routes) {
       const keys = Object.keys(routes);
       keys.forEach((item, i) => {
         if (item === '_options') {
           return;
         }
    -    if(routes[item] === null) {
    +    if (routes[item] === null) {
           return;
         }
         const options = routes[item]._options;
         let cb;
         if (Object.keys(routes[item]).length > 1) {
    -      cb = function() {
    +      cb = function () {
             walk.apply(this, [routes[item]]);
           };
         }
    @@ -34,8 +34,8 @@ export const walk = function(routes) {
      *
      * @param {object} routes - JSON representation of routes
      */
    -export default function(routes) {
    -  return function() {
    +export default function (routes) {
    +  return function () {
         walk.apply(this, [routes]);
       };
     }
    @@ -43,10 +43,8 @@ export default function(routes) {
     export let dump = (routes) => {};
     
     runInDebug(() => {
    -  const indent = function(num) {
    -    return Array(num)
    -      .fill('  ', 0, num)
    -      .join('');
    +  const indent = function (num) {
    +    return Array(num).fill('  ', 0, num).join('');
       };
       /**
        * String dumper to produce Router.map code
    @@ -56,11 +54,11 @@ runInDebug(() => {
        * @param {object} routes - JSON representation of routes
        * @example `console.log(dump(routes));`
        */
    -  dump = function(routes) {
    +  dump = function (routes) {
         let level = 2;
         const obj = {
           out: '',
    -      route: function(name, options, cb) {
    +      route: function (name, options, cb) {
             this.out += `${indent(level)}this.route('${name}', ${JSON.stringify(options)}`;
             if (cb) {
               level++;
    diff --git a/ui/packages/consul-ui/app/utils/routing/wildcard.js b/ui/packages/consul-ui/app/utils/routing/wildcard.js
    index 2fb82c446..f3339280b 100644
    --- a/ui/packages/consul-ui/app/utils/routing/wildcard.js
    +++ b/ui/packages/consul-ui/app/utils/routing/wildcard.js
    @@ -1,6 +1,6 @@
     import { get } from '@ember/object';
    -export default function(routes) {
    -  return function(name) {
    +export default function (routes) {
    +  return function (name) {
         let wildcard = false;
         try {
           wildcard = get(routes, name)._options.path.indexOf('*') !== -1;
    diff --git a/ui/packages/consul-ui/app/utils/search/exact.js b/ui/packages/consul-ui/app/utils/search/exact.js
    index b39b18b70..70aea39f0 100644
    --- a/ui/packages/consul-ui/app/utils/search/exact.js
    +++ b/ui/packages/consul-ui/app/utils/search/exact.js
    @@ -3,10 +3,6 @@ import PredicateSearch from './predicate';
     export default class ExactSearch extends PredicateSearch {
       predicate(s) {
         s = s.toLowerCase();
    -    return (item = '') =>
    -      item
    -        .toString()
    -        .toLowerCase()
    -        .indexOf(s) !== -1;
    +    return (item = '') => item.toString().toLowerCase().indexOf(s) !== -1;
       }
     }
    diff --git a/ui/packages/consul-ui/app/utils/search/fuzzy.js b/ui/packages/consul-ui/app/utils/search/fuzzy.js
    index 892b284c8..c8130b543 100644
    --- a/ui/packages/consul-ui/app/utils/search/fuzzy.js
    +++ b/ui/packages/consul-ui/app/utils/search/fuzzy.js
    @@ -15,6 +15,6 @@ export default class FuzzySearch {
       }
     
       search(s) {
    -    return this.fuse.search(s).map(item => item.item);
    +    return this.fuse.search(s).map((item) => item.item);
       }
     }
    diff --git a/ui/packages/consul-ui/app/utils/search/predicate.js b/ui/packages/consul-ui/app/utils/search/predicate.js
    index 398821755..a72cc69ad 100644
    --- a/ui/packages/consul-ui/app/utils/search/predicate.js
    +++ b/ui/packages/consul-ui/app/utils/search/predicate.js
    @@ -8,7 +8,7 @@ export default class PredicateSearch {
         const predicate = this.predicate(s);
         // Test the value of each key for each object against the regex
         // All that match are returned.
    -    return this.items.filter(item => {
    +    return this.items.filter((item) => {
           return Object.entries(this.options.finders).some(([key, finder]) => {
             const val = finder(item);
             if (Array.isArray(val)) {
    diff --git a/ui/packages/consul-ui/app/utils/search/regexp.js b/ui/packages/consul-ui/app/utils/search/regexp.js
    index f71e13799..583e17aa7 100644
    --- a/ui/packages/consul-ui/app/utils/search/regexp.js
    +++ b/ui/packages/consul-ui/app/utils/search/regexp.js
    @@ -10,6 +10,6 @@ export default class RegExpSearch extends PredicateSearch {
           // eager search of an incomplete regex
           return () => false;
         }
    -    return item => regex.test(item);
    +    return (item) => regex.test(item);
       }
     }
    diff --git a/ui/packages/consul-ui/app/utils/storage/local-storage.js b/ui/packages/consul-ui/app/utils/storage/local-storage.js
    index 9f8e1541d..f5e84f44c 100644
    --- a/ui/packages/consul-ui/app/utils/storage/local-storage.js
    +++ b/ui/packages/consul-ui/app/utils/storage/local-storage.js
    @@ -1,15 +1,15 @@
    -export default function(
    +export default function (
       scheme = '',
       storage = window.localStorage,
       encode = JSON.stringify,
       decode = JSON.parse,
    -  dispatch = function(key) {
    +  dispatch = function (key) {
         window.dispatchEvent(new StorageEvent('storage', { key: key }));
       }
     ) {
       const prefix = `${scheme}:`;
       return {
    -    getValue: function(path) {
    +    getValue: function (path) {
           let value = storage.getItem(`${prefix}${path}`);
           if (typeof value !== 'string') {
             value = '""';
    @@ -21,7 +21,7 @@ export default function(
           }
           return value;
         },
    -    setValue: function(path, value) {
    +    setValue: function (path, value) {
           if (value === null) {
             return this.removeValue(path);
           }
    @@ -34,12 +34,12 @@ export default function(
           dispatch(`${prefix}${path}`);
           return res;
         },
    -    removeValue: function(path) {
    +    removeValue: function (path) {
           const res = storage.removeItem(`${prefix}${path}`);
           dispatch(`${prefix}${path}`);
           return res;
         },
    -    all: function() {
    +    all: function () {
           return Object.keys(storage).reduce((prev, item, i, arr) => {
             if (item.indexOf(`${prefix}`) === 0) {
               const key = item.substr(prefix.length);
    diff --git a/ui/packages/consul-ui/app/utils/templatize.js b/ui/packages/consul-ui/app/utils/templatize.js
    index 4b76a11b9..0931c38b7 100644
    --- a/ui/packages/consul-ui/app/utils/templatize.js
    +++ b/ui/packages/consul-ui/app/utils/templatize.js
    @@ -1,3 +1,3 @@
    -export default function(arr = []) {
    -  return arr.map(item => `template-${item}`);
    +export default function (arr = []) {
    +  return arr.map((item) => `template-${item}`);
     }
    diff --git a/ui/packages/consul-ui/app/utils/ticker/index.js b/ui/packages/consul-ui/app/utils/ticker/index.js
    index 688ec2cae..ce6022a9a 100644
    --- a/ui/packages/consul-ui/app/utils/ticker/index.js
    +++ b/ui/packages/consul-ui/app/utils/ticker/index.js
    @@ -128,7 +128,7 @@ const TimelineAbstract = class {
       }
     };
     const Cubic = {
    -  easeOut: function(t, b, c, d) {
    +  easeOut: function (t, b, c, d) {
         t /= d;
         t--;
         return c * (t * t * t + 1) + b;
    @@ -140,7 +140,7 @@ export const Tween = class extends TimelineAbstract {
         Ticker.destroy();
       }
       static to(start, finish, frames, method) {
    -    Object.keys(finish).forEach(function(key) {
    +    Object.keys(finish).forEach(function (key) {
           finish[key] -= start[key];
         });
         return new Tween(start, finish, frames, method).play();
    @@ -154,7 +154,7 @@ export const Tween = class extends TimelineAbstract {
         this.tick = this.forwards;
       }
       _process() {
    -    Object.keys(this._props).forEach(key => {
    +    Object.keys(this._props).forEach((key) => {
           const num = this._method(
             this._currentframe,
             this._initialstate[key],
    @@ -187,7 +187,7 @@ export const Tween = class extends TimelineAbstract {
       gotoAndPlay() {
         if (typeof this._initialstate === 'undefined') {
           this._initialstate = {};
    -      Object.keys(this._props).forEach(key => {
    +      Object.keys(this._props).forEach((key) => {
             this._initialstate[key] = this._target[key];
           });
         }
    diff --git a/ui/packages/consul-ui/app/utils/tomography.js b/ui/packages/consul-ui/app/utils/tomography.js
    index 137bc1dcb..a636f5a2c 100644
    --- a/ui/packages/consul-ui/app/utils/tomography.js
    +++ b/ui/packages/consul-ui/app/utils/tomography.js
    @@ -1,15 +1,15 @@
     // TODO: Istanbul is ignored for the moment as it's not mine,
     // once I come here properly and 100% follow unignore
     /* istanbul ignore file */
    -export default function(distance) {
    -  return function(name, coordinates) {
    +export default function (distance) {
    +  return function (name, coordinates) {
         var min = 999999999;
         var max = -999999999;
         var distances = [];
    -    coordinates.forEach(function(node) {
    +    coordinates.forEach(function (node) {
           if (name == node.Node) {
             var segment = node.Segment;
    -        coordinates.forEach(function(other) {
    +        coordinates.forEach(function (other) {
               if (node.Node != other.Node && other.Segment == segment) {
                 var dist = distance(node, other);
                 distances.push({ node: other.Node, distance: dist, segment: segment });
    @@ -21,7 +21,7 @@ export default function(distance) {
                 }
               }
             });
    -        distances.sort(function(a, b) {
    +        distances.sort(function (a, b) {
               return a.distance - b.distance;
             });
           }
    diff --git a/ui/packages/consul-ui/app/utils/ucfirst.js b/ui/packages/consul-ui/app/utils/ucfirst.js
    index 346ae8c3e..fa2f2ee07 100644
    --- a/ui/packages/consul-ui/app/utils/ucfirst.js
    +++ b/ui/packages/consul-ui/app/utils/ucfirst.js
    @@ -1,3 +1,3 @@
    -export default function(str) {
    +export default function (str) {
       return `${str.substr(0, 1).toUpperCase()}${str.substr(1)}`;
     }
    diff --git a/ui/packages/consul-ui/app/utils/update-array-object.js b/ui/packages/consul-ui/app/utils/update-array-object.js
    index c8d264b87..716ea402f 100644
    --- a/ui/packages/consul-ui/app/utils/update-array-object.js
    +++ b/ui/packages/consul-ui/app/utils/update-array-object.js
    @@ -1,8 +1,8 @@
     import { get, set } from '@ember/object';
     import ObjectProxy from '@ember/object/proxy';
    -export default function(items, item, prop, value) {
    +export default function (items, item, prop, value) {
       value = typeof value === 'undefined' ? get(item, prop) : value;
    -  const pos = items.findIndex(function(item) {
    +  const pos = items.findIndex(function (item) {
         return get(item, prop) === value;
       });
       if (pos !== -1) {
    diff --git a/ui/packages/consul-ui/app/validations/intention-permission-http-header.js b/ui/packages/consul-ui/app/validations/intention-permission-http-header.js
    index de67f0619..67d5560b1 100644
    --- a/ui/packages/consul-ui/app/validations/intention-permission-http-header.js
    +++ b/ui/packages/consul-ui/app/validations/intention-permission-http-header.js
    @@ -1,8 +1,8 @@
     import { validatePresence } from 'ember-changeset-validations/validators';
     import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
    -export default schema => ({
    +export default (schema) => ({
       Name: [validatePresence(true)],
    -  Value: validateSometimes([validatePresence(true)], function() {
    +  Value: validateSometimes([validatePresence(true)], function () {
         return this.get('HeaderType') !== 'Present';
       }),
     });
    diff --git a/ui/packages/consul-ui/app/validations/intention-permission.js b/ui/packages/consul-ui/app/validations/intention-permission.js
    index ad855130e..8fd96a6ce 100644
    --- a/ui/packages/consul-ui/app/validations/intention-permission.js
    +++ b/ui/packages/consul-ui/app/validations/intention-permission.js
    @@ -6,8 +6,8 @@ import {
     import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
     
     const name = 'intention-permission';
    -export default schema => ({
    -  '*': validateSometimes([validatePresence(true)], function() {
    +export default (schema) => ({
    +  '*': validateSometimes([validatePresence(true)], function () {
         const methods = this.get('HTTP.Methods') || [];
         const headers = this.get('HTTP.Header') || [];
         const pathType = this.get('HTTP.PathType') || 'NoPath';
    @@ -21,7 +21,7 @@ export default schema => ({
       }),
       Action: [validateInclusion({ in: schema[name].Action.allowedValues })],
       HTTP: {
    -    Path: validateSometimes([validateFormat({ regex: /^\// })], function() {
    +    Path: validateSometimes([validateFormat({ regex: /^\// })], function () {
           const pathType = this.get('HTTP.PathType');
           return typeof pathType !== 'undefined' && pathType !== 'NoPath';
         }),
    diff --git a/ui/packages/consul-ui/app/validations/intention.js b/ui/packages/consul-ui/app/validations/intention.js
    index 12e3115b2..9f03e0dc3 100644
    --- a/ui/packages/consul-ui/app/validations/intention.js
    +++ b/ui/packages/consul-ui/app/validations/intention.js
    @@ -1,7 +1,7 @@
     import { validatePresence, validateLength } from 'ember-changeset-validations/validators';
     import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
     export default {
    -  '*': validateSometimes([validatePresence(true)], function() {
    +  '*': validateSometimes([validatePresence(true)], function () {
         const action = this.get('Action') || '';
         const permissions = this.get('Permissions') || [];
         if (action === '' && permissions.length === 0) {
    @@ -12,7 +12,7 @@ export default {
       SourceName: [validatePresence(true), validateLength({ min: 1 })],
       DestinationName: [validatePresence(true), validateLength({ min: 1 })],
       Permissions: [
    -    validateSometimes([validateLength({ min: 1 })], function(changes, content) {
    +    validateSometimes([validateLength({ min: 1 })], function (changes, content) {
           return !this.get('Action');
         }),
       ],
    diff --git a/ui/packages/consul-ui/blueprints/adapter-test/index.js b/ui/packages/consul-ui/blueprints/adapter-test/index.js
    index fcdb84f3c..76be5362f 100644
    --- a/ui/packages/consul-ui/blueprints/adapter-test/index.js
    +++ b/ui/packages/consul-ui/blueprints/adapter-test/index.js
    @@ -18,7 +18,6 @@ module.exports = useTestFrameworkDetector({
       },
     
       locals(options) {
    -    return {
    -    };
    +    return {};
       },
     });
    diff --git a/ui/packages/consul-ui/blueprints/adapter/index.js b/ui/packages/consul-ui/blueprints/adapter/index.js
    index d4fe91847..76d2fe873 100644
    --- a/ui/packages/consul-ui/blueprints/adapter/index.js
    +++ b/ui/packages/consul-ui/blueprints/adapter/index.js
    @@ -7,13 +7,11 @@ module.exports = {
     
       root: __dirname,
     
    -  fileMapTokens(options) {
    -  },
    +  fileMapTokens(options) {},
       locals(options) {
         // Return custom template variables here.
    -    return {
    -    };
    -  }
    +    return {};
    +  },
     
       // afterInstall(options) {
       //   // Perform extra work here.
    diff --git a/ui/packages/consul-ui/blueprints/component/index.js b/ui/packages/consul-ui/blueprints/component/index.js
    index bc164e848..011f1d2e1 100644
    --- a/ui/packages/consul-ui/blueprints/component/index.js
    +++ b/ui/packages/consul-ui/blueprints/component/index.js
    @@ -1,4 +1 @@
    -module.exports = Object.assign(
    -  require('ember-source/blueprints/component/index.js')
    -);
    -
    +module.exports = Object.assign(require('ember-source/blueprints/component/index.js'));
    diff --git a/ui/packages/consul-ui/blueprints/css-component/index.js b/ui/packages/consul-ui/blueprints/css-component/index.js
    index 48a3e7963..b8c23a837 100644
    --- a/ui/packages/consul-ui/blueprints/css-component/index.js
    +++ b/ui/packages/consul-ui/blueprints/css-component/index.js
    @@ -9,17 +9,16 @@ module.exports = {
       root: __dirname,
     
       fileMapTokens(options) {
    -      return {
    -        __path__() {
    -          return path.join('styles', 'components');
    -        }
    -      };
    +    return {
    +      __path__() {
    +        return path.join('styles', 'components');
    +      },
    +    };
       },
       locals(options) {
         // Return custom template variables here.
    -    return {
    -    };
    -  }
    +    return {};
    +  },
     
       // afterInstall(options) {
       //   // Perform extra work here.
    diff --git a/ui/packages/consul-ui/blueprints/model-test/index.js b/ui/packages/consul-ui/blueprints/model-test/index.js
    index 2a45c318c..6c8f155df 100644
    --- a/ui/packages/consul-ui/blueprints/model-test/index.js
    +++ b/ui/packages/consul-ui/blueprints/model-test/index.js
    @@ -18,7 +18,6 @@ module.exports = useTestFrameworkDetector({
       },
     
       locals(options) {
    -    return {
    -    };
    +    return {};
       },
     });
    diff --git a/ui/packages/consul-ui/blueprints/model/index.js b/ui/packages/consul-ui/blueprints/model/index.js
    index 4d0c6e185..3855bc4c1 100644
    --- a/ui/packages/consul-ui/blueprints/model/index.js
    +++ b/ui/packages/consul-ui/blueprints/model/index.js
    @@ -9,9 +9,8 @@ module.exports = {
     
       locals(options) {
         // Return custom template variables here.
    -    return {
    -    };
    -  }
    +    return {};
    +  },
     
       // afterInstall(options) {
       //   // Perform extra work here.
    diff --git a/ui/packages/consul-ui/blueprints/repository-test/index.js b/ui/packages/consul-ui/blueprints/repository-test/index.js
    index a35d3b9ab..650ec6ebb 100644
    --- a/ui/packages/consul-ui/blueprints/repository-test/index.js
    +++ b/ui/packages/consul-ui/blueprints/repository-test/index.js
    @@ -19,7 +19,7 @@ module.exports = useTestFrameworkDetector({
     
       locals(options) {
         return {
    -      screamingSnakeCaseModuleName: options.entity.name.replace('-', '_').toUpperCase()
    +      screamingSnakeCaseModuleName: options.entity.name.replace('-', '_').toUpperCase(),
         };
       },
     });
    diff --git a/ui/packages/consul-ui/blueprints/repository/index.js b/ui/packages/consul-ui/blueprints/repository/index.js
    index 29f9b1e1a..6dc50bdda 100644
    --- a/ui/packages/consul-ui/blueprints/repository/index.js
    +++ b/ui/packages/consul-ui/blueprints/repository/index.js
    @@ -9,17 +9,16 @@ module.exports = {
       root: __dirname,
     
       fileMapTokens(options) {
    -      return {
    -        __path__() {
    -          return path.join('services', 'repository');
    -        }
    -      };
    +    return {
    +      __path__() {
    +        return path.join('services', 'repository');
    +      },
    +    };
       },
       locals(options) {
         // Return custom template variables here.
    -    return {
    -    };
    -  }
    +    return {};
    +  },
     
       // afterInstall(options) {
       //   // Perform extra work here.
    diff --git a/ui/packages/consul-ui/blueprints/route/index.js b/ui/packages/consul-ui/blueprints/route/index.js
    index 0867ff7d1..e3b9ab3e2 100644
    --- a/ui/packages/consul-ui/blueprints/route/index.js
    +++ b/ui/packages/consul-ui/blueprints/route/index.js
    @@ -1,30 +1,28 @@
     /* eslint-env node */
     const chalk = require('chalk');
     
    -module.exports = Object.assign(
    -  require('ember-source/blueprints/route/index.js'),
    -  {
    -    afterInstall: function(options) {
    -      updateRouter.call(this, 'add', options);
    -    },
    +module.exports = Object.assign(require('ember-source/blueprints/route/index.js'), {
    +  afterInstall: function (options) {
    +    updateRouter.call(this, 'add', options);
    +  },
     
    -    afterUninstall: function(options) {
    -      updateRouter.call(this, 'remove', options);
    -    }
    -  }
    -);
    +  afterUninstall: function (options) {
    +    updateRouter.call(this, 'remove', options);
    +  },
    +});
     
     function updateRouter(action, options) {
       var entity = options.entity;
       var actionColorMap = {
         add: 'green',
    -    remove: 'red'
    +    remove: 'red',
       };
       var color = actionColorMap[action] || 'gray';
     
       if (this.shouldTouchRouter(entity.name, options)) {
    -
    -    this.ui.writeLine(`we don't currently update the router for you, please edit ${findRouter(options).join('/')}`);
    +    this.ui.writeLine(
    +      `we don't currently update the router for you, please edit ${findRouter(options).join('/')}`
    +    );
         this._writeStatusToUI(chalk[color], action + ' route', entity.name);
       }
     }
    @@ -40,4 +38,3 @@ function findRouter(options) {
     
       return routerPathParts;
     }
    -
    diff --git a/ui/packages/consul-ui/blueprints/serializer-test/index.js b/ui/packages/consul-ui/blueprints/serializer-test/index.js
    index 052ce5357..ca22ba7c2 100644
    --- a/ui/packages/consul-ui/blueprints/serializer-test/index.js
    +++ b/ui/packages/consul-ui/blueprints/serializer-test/index.js
    @@ -18,7 +18,6 @@ module.exports = useTestFrameworkDetector({
       },
     
       locals(options) {
    -    return {
    -    };
    +    return {};
       },
     });
    diff --git a/ui/packages/consul-ui/blueprints/serializer/index.js b/ui/packages/consul-ui/blueprints/serializer/index.js
    index c9df3ae22..72cfdc810 100644
    --- a/ui/packages/consul-ui/blueprints/serializer/index.js
    +++ b/ui/packages/consul-ui/blueprints/serializer/index.js
    @@ -7,13 +7,11 @@ module.exports = {
     
       root: __dirname,
     
    -  fileMapTokens(options) {
    -  },
    +  fileMapTokens(options) {},
       locals(options) {
         // Return custom template variables here.
    -    return {
    -    };
    -  }
    +    return {};
    +  },
     
       // afterInstall(options) {
       //   // Perform extra work here.
    diff --git a/ui/packages/consul-ui/config/ember-cli-update.json b/ui/packages/consul-ui/config/ember-cli-update.json
    new file mode 100644
    index 000000000..7ae868b44
    --- /dev/null
    +++ b/ui/packages/consul-ui/config/ember-cli-update.json
    @@ -0,0 +1,20 @@
    +{
    +  "schemaVersion": "1.0.0",
    +  "packages": [
    +    {
    +      "name": "ember-cli",
    +      "version": "3.24.0",
    +      "blueprints": [
    +        {
    +          "name": "app",
    +          "outputRepo": "https://github.com/ember-cli/ember-new-output",
    +          "codemodsSource": "ember-app-codemods-manifest@1",
    +          "isBaseBlueprint": true,
    +          "options": [
    +            "--no-welcome"
    +          ]
    +        }
    +      ]
    +    }
    +  ]
    +}
    diff --git a/ui/packages/consul-ui/config/ember-intl.js b/ui/packages/consul-ui/config/ember-intl.js
    index be1e0249e..4fc7158d7 100644
    --- a/ui/packages/consul-ui/config/ember-intl.js
    +++ b/ui/packages/consul-ui/config/ember-intl.js
    @@ -1,6 +1,6 @@
     /* jshint node:true */
     
    -module.exports = function(/* environment */) {
    +module.exports = function (/* environment */) {
       return {
         /**
          * Merges the fallback locale's translations into all other locales as a
    diff --git a/ui/packages/consul-ui/config/environment.js b/ui/packages/consul-ui/config/environment.js
    index a85b0093d..66d06aa60 100644
    --- a/ui/packages/consul-ui/config/environment.js
    +++ b/ui/packages/consul-ui/config/environment.js
    @@ -12,7 +12,7 @@ const repositoryYear = utils.repositoryYear;
     const repositorySHA = utils.repositorySHA;
     const binaryVersion = utils.binaryVersion(repositoryRoot);
     
    -module.exports = function(environment, $ = process.env) {
    +module.exports = function (environment, $ = process.env) {
       // available environments
       // ['production', 'development', 'staging', 'test'];
       const env = utils.env($);
    @@ -84,8 +84,10 @@ module.exports = function(environment, $ = process.env) {
           SSOEnabled: false,
           PeeringEnabled: false,
           PartitionsEnabled: false,
    +      HCPEnabled: false,
           LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
           PrimaryDatacenter: env('CONSUL_DATACENTER_PRIMARY', 'dc1'),
    +      APIPrefix: env('CONSUL_API_PREFIX', ''),
         },
     
         // Static variables used in multiple places throughout the UI
    @@ -109,8 +111,10 @@ module.exports = function(environment, $ = process.env) {
               // in testing peering feature is on by default
               PeeringEnabled: env('CONSUL_PEERINGS_ENABLED', true),
               PartitionsEnabled: env('CONSUL_PARTITIONS_ENABLED', false),
    +          HCPEnabled: env('CONSUL_HCP_ENABLED', false),
               LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
               PrimaryDatacenter: env('CONSUL_DATACENTER_PRIMARY', 'dc1'),
    +          APIPrefix: env('CONSUL_API_PREFIX', ''),
             },
     
             '@hashicorp/ember-cli-api-double': {
    @@ -118,6 +122,7 @@ module.exports = function(environment, $ = process.env) {
               enabled: true,
               endpoints: {
                 '/v1': '/mock-api/v1',
    +            '/prefixed-api': '/mock-api/prefixed-api',
               },
             },
             APP: Object.assign({}, ENV.APP, {
    @@ -160,8 +165,10 @@ module.exports = function(environment, $ = process.env) {
               SSOEnabled: env('CONSUL_SSO_ENABLED', true),
               PeeringEnabled: env('CONSUL_PEERINGS_ENABLED', true),
               PartitionsEnabled: env('CONSUL_PARTITIONS_ENABLED', true),
    +          HCPEnabled: env('CONSUL_HCP_ENABLED', false),
               LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
               PrimaryDatacenter: env('CONSUL_DATACENTER_PRIMARY', 'dc1'),
    +          APIPrefix: env('CONSUL_API_PREFIX', ''),
             },
     
             '@hashicorp/ember-cli-api-double': {
    @@ -176,7 +183,9 @@ module.exports = function(environment, $ = process.env) {
           ENV = Object.assign({}, ENV, {
             // in production operatorConfig is populated at consul runtime from
             // operator configuration
    -        operatorConfig: {},
    +        operatorConfig: {
    +          APIPrefix: '',
    +        },
           });
           break;
       }
    diff --git a/ui/packages/consul-ui/config/utils.js b/ui/packages/consul-ui/config/utils.js
    index b1b58a54e..c24172a03 100644
    --- a/ui/packages/consul-ui/config/utils.js
    +++ b/ui/packages/consul-ui/config/utils.js
    @@ -2,31 +2,27 @@ const read = require('fs').readFileSync;
     const exec = require('child_process').execSync;
     
     // See tests ../node-tests/config/utils.js
    -const repositoryYear = function(date = exec('git show -s --format=%ci HEAD')) {
    -  return date
    -    .toString()
    -    .trim()
    -    .split('-')
    -    .shift();
    +const repositoryYear = function (date = exec('git show -s --format=%ci HEAD')) {
    +  return date.toString().trim().split('-').shift();
     };
    -const repositorySHA = function(sha = exec('git rev-parse --short HEAD')) {
    +const repositorySHA = function (sha = exec('git rev-parse --short HEAD')) {
       return sha.toString().trim();
     };
    -const binaryVersion = function(repositoryRoot) {
    -  return function(versionFileContents = read(`${repositoryRoot}/version/version.go`)) {
    +const binaryVersion = function (repositoryRoot) {
    +  return function (versionFileContents = read(`${repositoryRoot}/version/version.go`)) {
         // see /scripts/dist.sh:8
         return versionFileContents
           .toString()
           .split('\n')
    -      .find(function(item, i, arr) {
    +      .find(function (item, i, arr) {
             return item.indexOf('Version =') !== -1;
           })
           .trim()
           .split('"')[1];
       };
     };
    -const env = function($) {
    -  return function(flag, fallback) {
    +const env = function ($) {
    +  return function (flag, fallback) {
         // a fallback value MUST be set
         if (typeof fallback === 'undefined') {
           throw new Error(`Please provide a fallback value for $${flag}`);
    diff --git a/ui/packages/consul-ui/ember-cli-build.js b/ui/packages/consul-ui/ember-cli-build.js
    index 576ea55ee..4fe791907 100644
    --- a/ui/packages/consul-ui/ember-cli-build.js
    +++ b/ui/packages/consul-ui/ember-cli-build.js
    @@ -11,7 +11,7 @@ const utils = require('./config/utils');
     // const BroccoliDebug = require('broccoli-debug');
     // const debug = BroccoliDebug.buildDebugCallback(`app:consul-ui`)
     
    -module.exports = function(defaults, $ = process.env) {
    +module.exports = function (defaults, $ = process.env) {
       // available environments
       // ['production', 'development', 'staging', 'test'];
     
    @@ -33,34 +33,31 @@ module.exports = function(defaults, $ = process.env) {
         'consul-peerings',
         'consul-partitions',
         'consul-nspaces',
    -    'consul-hcp'
    -  ].map(item => {
    +    'consul-hcp',
    +  ].map((item) => {
         return {
           name: item,
    -      path: path.dirname(require.resolve(`${item}/package.json`))
    +      path: path.dirname(require.resolve(`${item}/package.json`)),
         };
       });
     
       const babel = {
    -    plugins: [
    -      '@babel/plugin-proposal-object-rest-spread',
    -    ],
    +    plugins: ['@babel/plugin-proposal-object-rest-spread'],
         sourceMaps: sourcemaps ? 'inline' : false,
    -  }
    +  };
     
       // setup up different build configuration depending on environment
    -  if(!['test'].includes(env)) {
    +  if (!['test'].includes(env)) {
         // exclude any component/pageobject.js files from anything but test
         excludeFiles = excludeFiles.concat([
           'components/**/pageobject.js',
           'components/**/test-support.js',
           'components/**/*.test-support.js',
           'components/**/*.test.js',
    -    ])
    +    ]);
       }
     
    -
    -  if(['test', 'production'].includes(env)) {
    +  if (['test', 'production'].includes(env)) {
         // exclude our debug initializer, route and template
         excludeFiles = excludeFiles.concat([
           'instance-initializers/debug.js',
    @@ -69,69 +66,62 @@ module.exports = function(defaults, $ = process.env) {
           'modifiers/**/*-debug.js',
           'services/**/*-debug.js',
           'templates/debug.hbs',
    -      'components/debug/**/*.*'
    -    ])
    +      'components/debug/**/*.*',
    +    ]);
         // inspect *-debug configuration files for files to exclude
    -    excludeFiles = apps.reduce(
    -      (prev, item) => {
    -        return ['services', 'routes'].reduce(
    -          (prev, type) => {
    -            const path = `${item.path}/vendor/${item.name}/${type}-debug.js`;
    -            if(exists(path)) {
    -              return Object.entries(JSON.parse(require(path)[type])).reduce(
    -                (prev, [key, definition]) => {
    -                  if(typeof definition.class !== 'undefined') {
    -                    return prev.concat(`${definition.class.replace(`${item.name}/`, '')}.js`);
    -                  }
    -                  return prev;
    -                },
    -                prev
    -              );
    -            }
    -            return prev;
    -          },
    -          prev
    -        )
    -      },
    -      excludeFiles
    -    );
    +    excludeFiles = apps.reduce((prev, item) => {
    +      return ['services', 'routes'].reduce((prev, type) => {
    +        const path = `${item.path}/vendor/${item.name}/${type}-debug.js`;
    +        if (exists(path)) {
    +          return Object.entries(JSON.parse(require(path)[type])).reduce(
    +            (prev, [key, definition]) => {
    +              if (typeof definition.class !== 'undefined') {
    +                return prev.concat(`${definition.class.replace(`${item.name}/`, '')}.js`);
    +              }
    +              return prev;
    +            },
    +            prev
    +          );
    +        }
    +        return prev;
    +      }, prev);
    +    }, excludeFiles);
         // exclude any debug like addons from production or test environments
         addons.blacklist = [
           // exclude docfy
    -      '@docfy/ember'
    +      '@docfy/ember',
         ];
       }
     
    -  if(['production'].includes(env)) {
    +  if (['production'].includes(env)) {
         // everything apart from production is 'debug', including test
         // which means this and everything it affects is never tested
    -    babel.plugins.push(
    -      ['strip-function-call', {'strip': ['Ember.runInDebug']}]
    -    )
    +    babel.plugins.push(['strip-function-call', { strip: ['Ember.runInDebug'] }]);
       }
     
       //
    -  (
    -    function(apps) {
    -      trees.app = mergeTrees([
    -        new Funnel('app', { exclude: excludeFiles })
    -      ].concat(
    -        apps.filter(item => exists(`${item.path}/app`)).map(item => new Funnel(`${item.path}/app`, {exclude: excludeFiles}))
    -      ), {
    -        overwrite: true
    -      });
    -      trees.vendor = mergeTrees([
    -        new Funnel('vendor'),
    -      ].concat(
    -        apps.map(item => new Funnel(`${item.path}/vendor`))
    -      ));
    -    }
    -  // consul-ui will eventually be a separate app just like the others
    -  // at which point we can remove this filter/extra scope
    -  )(apps.filter(item => item.name !== 'consul-ui'));
    +  (function (apps) {
    +    trees.app = mergeTrees(
    +      [new Funnel('app', { exclude: excludeFiles })].concat(
    +        apps
    +          .filter((item) => exists(`${item.path}/app`))
    +          .map((item) => new Funnel(`${item.path}/app`, { exclude: excludeFiles }))
    +      ),
    +      {
    +        overwrite: true,
    +      }
    +    );
    +    trees.vendor = mergeTrees(
    +      [new Funnel('vendor')].concat(apps.map((item) => new Funnel(`${item.path}/vendor`)))
    +    );
    +  })(
    +    // consul-ui will eventually be a separate app just like the others
    +    // at which point we can remove this filter/extra scope
    +    apps.filter((item) => item.name !== 'consul-ui')
    +  );
       //
     
    -  const app = new EmberApp(
    +  let app = new EmberApp(
         Object.assign({}, defaults, {
           productionEnvironments: prodlike,
         }),
    @@ -143,7 +133,15 @@ module.exports = function(defaults, $ = process.env) {
             includePolyfill: true,
           },
           'ember-cli-string-helpers': {
    -        only: ['capitalize', 'lowercase', 'truncate', 'uppercase', 'humanize', 'titleize', 'classify'],
    +        only: [
    +          'capitalize',
    +          'lowercase',
    +          'truncate',
    +          'uppercase',
    +          'humanize',
    +          'titleize',
    +          'classify',
    +        ],
           },
           'ember-cli-math-helpers': {
             only: ['div'],
    @@ -163,26 +161,19 @@ module.exports = function(defaults, $ = process.env) {
               'mode/loadmode.js',
             ],
           },
    -      'ember-cli-uglify': {
    -        uglify: {
    -          compress: {
    -            keep_fargs: false,
    -          },
    -        },
    -      },
           sassOptions: {
             implementation: require('sass'),
             sourceMapEmbed: sourcemaps,
           },
         }
       );
    -  const build = function(path, options) {
    -    const {root, ...rest} = options;
    -    if(exists(`${root}/${path}`)) {
    +  const build = function (path, options) {
    +    const { root, ...rest } = options;
    +    if (exists(`${root}/${path}`)) {
           app.import(path, rest);
         }
       };
    -  apps.forEach(item => {
    +  apps.forEach((item) => {
         build(`vendor/${item.name}/routes.js`, {
           root: item.path,
           outputFile: `assets/${item.name}/routes.js`,
    @@ -191,7 +182,7 @@ module.exports = function(defaults, $ = process.env) {
           root: item.path,
           outputFile: `assets/${item.name}/services.js`,
         });
    -    if(devlike) {
    +    if (devlike) {
           build(`vendor/${item.name}/routes-debug.js`, {
             root: item.path,
             outputFile: `assets/${item.name}/routes-debug.js`,
    diff --git a/ui/packages/consul-ui/lib/block-slots/addon/components/block-slot.js b/ui/packages/consul-ui/lib/block-slots/addon/components/block-slot.js
    index 1b692a4a4..65513a3bc 100644
    --- a/ui/packages/consul-ui/lib/block-slots/addon/components/block-slot.js
    +++ b/ui/packages/consul-ui/lib/block-slots/addon/components/block-slot.js
    @@ -8,10 +8,10 @@ import YieldSlot from './yield-slot';
     const BlockSlot = Component.extend({
       layout,
       tagName: '',
    -  _name: computed('name', function() {
    +  _name: computed('name', function () {
         return this.name;
       }),
    -  didInsertElement: function() {
    +  didInsertElement: function () {
         const slottedComponent = this.nearestOfType(Slots);
         if (!slottedComponent._isRegistered(this._name)) {
           slottedComponent._activateSlot(this._name);
    @@ -42,7 +42,7 @@ const BlockSlot = Component.extend({
           }
         }
       },
    -  willDestroyElement: function() {
    +  willDestroyElement: function () {
         if (this.slottedComponent) {
           // Deactivate the yield slot using the slots interface when the block
           // is destroyed to allow the yield slot default {{else}} to take effect
    diff --git a/ui/packages/consul-ui/lib/block-slots/addon/components/yield-slot.js b/ui/packages/consul-ui/lib/block-slots/addon/components/yield-slot.js
    index 358d3b3cd..e394c739c 100644
    --- a/ui/packages/consul-ui/lib/block-slots/addon/components/yield-slot.js
    +++ b/ui/packages/consul-ui/lib/block-slots/addon/components/yield-slot.js
    @@ -5,16 +5,16 @@ import Slots from '../mixins/slots';
     const YieldSlotComponent = Component.extend({
       layout,
       tagName: '',
    -  _name: computed('name', function() {
    +  _name: computed('name', function () {
         return this.name;
       }),
    -  _blockParams: computed('params', function() {
    +  _blockParams: computed('params', function () {
         return this.params;
       }),
    -  _parentView: computed(function() {
    +  _parentView: computed(function () {
         return this.nearestOfType(Slots);
       }),
    -  isActive: computed('_parentView._slots.[]', '_name', function() {
    +  isActive: computed('_parentView._slots.[]', '_name', function () {
         return get(this, '_parentView._slots').includes(get(this, '_name'));
       }),
     });
    diff --git a/ui/packages/consul-ui/lib/block-slots/addon/mixins/slots.js b/ui/packages/consul-ui/lib/block-slots/addon/mixins/slots.js
    index 43f16ae43..3ed0273f1 100644
    --- a/ui/packages/consul-ui/lib/block-slots/addon/mixins/slots.js
    +++ b/ui/packages/consul-ui/lib/block-slots/addon/mixins/slots.js
    @@ -2,7 +2,7 @@ import { computed, get } from '@ember/object';
     import { A } from '@ember/array';
     import Mixin from '@ember/object/mixin';
     export default Mixin.create({
    -  _slots: computed(function() {
    +  _slots: computed(function () {
         return A();
       }),
       _activateSlot(name) {
    diff --git a/ui/packages/consul-ui/lib/colocated-components/index.js b/ui/packages/consul-ui/lib/colocated-components/index.js
    index ef3a1130a..8c8f75e54 100644
    --- a/ui/packages/consul-ui/lib/colocated-components/index.js
    +++ b/ui/packages/consul-ui/lib/colocated-components/index.js
    @@ -13,7 +13,7 @@ module.exports = {
        * Make any CSS available for import within app/components/component-name:
        * @import 'app-name/components/component-name/index.scss'
        */
    -  treeForStyles: function(tree) {
    +  treeForStyles: function (tree) {
         let debug = read(`${this.project.root}/app/styles/debug.scss`);
         if (['production', 'test'].includes(process.env.EMBER_ENV)) {
           debug = '';
    diff --git a/ui/packages/consul-ui/lib/commands/index.js b/ui/packages/consul-ui/lib/commands/index.js
    index c8c558db5..c5219bb84 100644
    --- a/ui/packages/consul-ui/lib/commands/index.js
    +++ b/ui/packages/consul-ui/lib/commands/index.js
    @@ -3,11 +3,11 @@
     'use strict';
     module.exports = {
       name: 'commands',
    -  includedCommands: function() {
    +  includedCommands: function () {
         return {
           'steps:list': {
             name: 'steps:list',
    -        run: function(config, args) {
    +        run: function (config, args) {
               require('./lib/list.js')(`${process.cwd()}/tests/steps.js`);
             },
           },
    diff --git a/ui/packages/consul-ui/lib/commands/lib/list.js b/ui/packages/consul-ui/lib/commands/lib/list.js
    index 82db58de2..f34863f5f 100644
    --- a/ui/packages/consul-ui/lib/commands/lib/list.js
    +++ b/ui/packages/consul-ui/lib/commands/lib/list.js
    @@ -7,41 +7,41 @@ const path = require('path');
     const vm = require('vm');
     const color = require('chalk');
     
    -const out = function(prefix, step, desc) {
    +const out = function (prefix, step, desc) {
       if (!Array.isArray(step)) {
         step = [step];
       }
    -  step.forEach(function(item) {
    +  step.forEach(function (item) {
         const str =
           prefix +
    -      item.replace('\n', ' | ').replace(/\$\w+/g, function(match) {
    +      item.replace('\n', ' | ').replace(/\$\w+/g, function (match) {
             return color.cyan(match);
           });
         console.log(color.green(str));
       });
     };
     const library = {
    -  given: function(step, cb, desc) {
    +  given: function (step, cb, desc) {
         out('Given ', step, desc);
         return this;
       },
    -  desc: function(desc) {
    +  desc: function (desc) {
         console.log(color.yellow(`- ${desc.trim()}`));
       },
    -  section: function() {
    +  section: function () {
         console.log(color.yellow(`##`));
       },
    -  then: function(step, cb, desc) {
    +  then: function (step, cb, desc) {
         out('Then ', step, desc);
         return this;
       },
    -  when: function(step, cb, desc) {
    +  when: function (step, cb, desc) {
         out('When ', step, desc);
         return this;
       },
     };
     const root = process.cwd();
    -const exec = function(filename) {
    +const exec = function (filename) {
       const js = read(filename);
       const code = babel.transform(js.toString(), {
         filename: filename,
    @@ -52,7 +52,7 @@ const exec = function(filename) {
         code,
         {
           exports: exports,
    -      require: function(str) {
    +      require: function (str) {
             return exec(path.resolve(`${root}/tests`, `${str}.js`)).default;
           },
         },
    @@ -63,7 +63,7 @@ const exec = function(filename) {
       return exports;
     };
     
    -module.exports = function(filename) {
    +module.exports = function (filename) {
       const assert = () => {};
       exec(filename).default({ assert, library });
     };
    diff --git a/ui/packages/consul-ui/lib/custom-element/index.js b/ui/packages/consul-ui/lib/custom-element/index.js
    index 6a52e24e0..f5ad21011 100644
    --- a/ui/packages/consul-ui/lib/custom-element/index.js
    +++ b/ui/packages/consul-ui/lib/custom-element/index.js
    @@ -2,24 +2,29 @@
     
     module.exports = {
       name: require('./package').name,
    -  getTransform: function() {
    +  getTransform: function () {
         return {
           name: 'custom-element',
           plugin: class {
             transform(ast) {
               this.syntax.traverse(ast, {
                 ElementNode: (node) => {
    -              if(node.tag === 'CustomElement') {
    +              if (node.tag === 'CustomElement') {
                     node.attributes = node.attributes
                       // completely remove these ones, they are not used runtime
                       // element is potentially only temporarily being removed
    -                  .filter(item => !['element', 'description', 'slots', 'cssparts'].includes(`${item.name.substr(1)}`))
    -                  .map(item => {
    -                    switch(true) {
    +                  .filter(
    +                    (item) =>
    +                      !['element', 'description', 'slots', 'cssparts'].includes(
    +                        `${item.name.substr(1)}`
    +                      )
    +                  )
    +                  .map((item) => {
    +                    switch (true) {
                           // these ones are ones where we need to remove the documentation only
                           // the attributes themselves are required at runtime
                           case ['attrs', 'cssprops'].includes(`${item.name.substr(1)}`):
    -                        item.value.params = item.value.params.map(item => {
    +                        item.value.params = item.value.params.map((item) => {
                               // we can't use arr.length here as we don't know
                               // whether someone has used the documentation entry
                               // in the array or not We use the hardcoded `3` for
    @@ -30,7 +35,6 @@ module.exports = {
                               return item;
                             });
                             break;
    -
                         }
                         return item;
                       });
    @@ -39,14 +43,13 @@ module.exports = {
               });
               return ast;
             }
    -
           },
    -      baseDir: function() {
    +      baseDir: function () {
             return __dirname;
           },
    -      cacheKey: function() {
    +      cacheKey: function () {
             return 'custom-element';
    -      }
    +      },
         };
       },
       setupPreprocessorRegistry(type, registry) {
    @@ -54,10 +57,8 @@ module.exports = {
         transform.parallelBabel = {
           requireFile: __filename,
           buildUsing: 'getTransform',
    -      params: {}
    +      params: {},
         };
         registry.add('htmlbars-ast-plugin', transform);
       },
    -
     };
    -
    diff --git a/ui/packages/consul-ui/lib/rehype-prism/index.js b/ui/packages/consul-ui/lib/rehype-prism/index.js
    index 5e7350c75..9e12d8b94 100644
    --- a/ui/packages/consul-ui/lib/rehype-prism/index.js
    +++ b/ui/packages/consul-ui/lib/rehype-prism/index.js
    @@ -37,14 +37,13 @@ module.exports = (options) => {
     
       return (tree) => {
         visit(tree, 'element', (node, index, parent) => {
    -      if (typeof parent === 'undefined' ||
    -          parent.tagName !== 'pre' ||
    -          node.tagName !== 'code'
    -      ) {
    +      if (typeof parent === 'undefined' || parent.tagName !== 'pre' || node.tagName !== 'code') {
             return;
           }
           const languagePrefix = 'language-';
    -      const langClass = ((node.properties.className || []).find(item => item.startsWith(languagePrefix)) || '').toLowerCase();
    +      const langClass = (
    +        (node.properties.className || []).find((item) => item.startsWith(languagePrefix)) || ''
    +      ).toLowerCase();
           if (langClass.length === 0) {
             return;
           }
    diff --git a/ui/packages/consul-ui/lib/startup/index.js b/ui/packages/consul-ui/lib/startup/index.js
    index 5a2f6b534..6f6e854e3 100644
    --- a/ui/packages/consul-ui/lib/startup/index.js
    +++ b/ui/packages/consul-ui/lib/startup/index.js
    @@ -19,7 +19,7 @@ const bodyParser = require('body-parser');
     //
     module.exports = {
       name: 'startup',
    -  serverMiddleware: function(server) {
    +  serverMiddleware: function (server) {
         // TODO: see if we can move these into the project specific `/server` directory
         // instead of inside an addon
     
    @@ -35,11 +35,11 @@ module.exports = {
           cookieParser(),
           bodyParser.text({ type: '*/*' }),
           controller().serve,
    -    ].reduce(function(app, item) {
    +    ].reduce(function (app, item) {
           return app.use(item);
         }, server.app);
       },
    -  treeFor: function(name) {
    +  treeFor: function (name) {
         const tree = this._super.treeFor.apply(this, arguments);
         if (name === 'app') {
           if (['production', 'test'].includes(process.env.EMBER_ENV)) {
    @@ -48,13 +48,13 @@ module.exports = {
         }
         return tree;
       },
    -  contentFor: function(type, config) {
    +  contentFor: function (type, config) {
         const vars = {
           appName: config.modulePrefix,
           environment: config.environment,
           rootURL: config.environment === 'production' ? '{{.ContentPath}}' : config.rootURL,
           config: config,
    -      env: function(key) {
    +      env: function (key) {
             if (process.env[key]) {
               return process.env[key];
             }
    diff --git a/ui/packages/consul-ui/lib/startup/templates/body.html.js b/ui/packages/consul-ui/lib/startup/templates/body.html.js
    index b4c46266e..4aeb0f9a2 100644
    --- a/ui/packages/consul-ui/lib/startup/templates/body.html.js
    +++ b/ui/packages/consul-ui/lib/startup/templates/body.html.js
    @@ -12,8 +12,8 @@ const hbs = (path, attrs = {}) =>
         .replace('{{yield}}', '')
         .replace(hbsRe, (match, prop) => attrs[prop.substr(1)]);
     
    -const BrandLoader = attrs => hbs('brand-loader/index.hbs', attrs);
    -const Enterprise = attrs => hbs('brand-loader/enterprise.hbs', attrs);
    +const BrandLoader = (attrs) => hbs('brand-loader/index.hbs', attrs);
    +const Enterprise = (attrs) => hbs('brand-loader/enterprise.hbs', attrs);
     
     module.exports = ({ appName, environment, rootURL, config, env }) => `