Merge remote-tracking branch 'hashicorp/main' into feature/health-checks_windows_service
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
This commit is contained in:
commit
a131741ada
|
@ -0,0 +1,3 @@
|
|||
```release-note:enhancement
|
||||
catalog: Add per-node indexes to reduce watchset firing for unrelated nodes and services.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
deps: Update go-grpc/grpc, resolving connection memory leak
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
```release-note:feature
|
||||
agent: Added information about build date alongside other version information for Consul. Extended /agent/self endpoint and `consul version` commands
|
||||
to report this. Agent also reports build date in log on startup.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: upgrade ember-composable-helpers to v5.x
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Fix incorrect text on certain page empty states
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
dns: Added support for specifying admin partition in node lookups.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
connect: Update Envoy support matrix to latest patch releases (1.22.2, 1.21.3, 1.20.4, 1.19.5)
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:enhancement
|
||||
api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way).
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
```release-note:improvement
|
||||
command: Add support for enabling TLS in the Envoy Prometheus endpoint via the `consul connect envoy` command.
|
||||
Adds the `-prometheus-ca-file`, `-prometheus-ca-path`, `-prometheus-cert-file` and `-prometheus-key-file` flags.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:breaking-change
|
||||
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.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
xds: Fix a bug that resulted in Lambda services not using the payload-passthrough option as expected.
|
||||
```
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
streaming: Added topics for `ingress-gateway`, `mesh`, `service-intentions` and `service-resolver` config entry events.
|
||||
```
|
|
@ -4,4 +4,7 @@ export GIT_COMMIT=$(git rev-parse --short HEAD)
|
|||
export GIT_COMMIT_YEAR=$(git show -s --format=%cd --date=format:%Y HEAD)
|
||||
export GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||
export GIT_IMPORT=github.com/hashicorp/consul/version
|
||||
export GOLDFLAGS="-X ${GIT_IMPORT}.GitCommit=${GIT_COMMIT}${GIT_DIRTY}"
|
||||
# we're using this for build date because it's stable across platform builds
|
||||
# the env -i and -noprofile are used to ensure we don't try to recursively call this profile when starting bash
|
||||
export GIT_DATE=$(env -i /bin/bash --noprofile -norc ${CIRCLE_WORKING_DIRECTORY}/build-support/scripts/build-date.sh)
|
||||
export GOLDFLAGS="-X ${GIT_IMPORT}.GitCommit=${GIT_COMMIT}${GIT_DIRTY} -X ${GIT_IMPORT}.BuildDate=${GIT_DATE}"
|
||||
|
|
|
@ -23,6 +23,10 @@ references:
|
|||
BASH_ENV: .circleci/bash_env.sh
|
||||
VAULT_BINARY_VERSION: 1.9.4
|
||||
GO_VERSION: 1.18.1
|
||||
envoy-versions: &supported_envoy_versions
|
||||
- &default_envoy_version "1.19.5"
|
||||
- "1.20.4"
|
||||
- "1.21.3"
|
||||
images:
|
||||
# When updating the Go version, remember to also update the versions in the
|
||||
# workflows section for go-test-lib jobs.
|
||||
|
@ -30,7 +34,7 @@ references:
|
|||
ember: &EMBER_IMAGE docker.mirror.hashicorp.services/circleci/node:14-browsers
|
||||
ubuntu: &UBUNTU_CI_IMAGE ubuntu-2004:202201-02
|
||||
cache:
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v7-{{ checksum "ui/yarn.lock" }}
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v8-{{ checksum "ui/yarn.lock" }}
|
||||
|
||||
steps:
|
||||
install-gotestsum: &install-gotestsum
|
||||
|
@ -852,13 +856,23 @@ jobs:
|
|||
path: *TEST_RESULTS_DIR
|
||||
- run: *notify-slack-failure
|
||||
|
||||
envoy-integration-test-1_19_3: &ENVOY_TESTS
|
||||
envoy-integration-test: &ENVOY_TESTS
|
||||
machine:
|
||||
image: *UBUNTU_CI_IMAGE
|
||||
parallelism: 4
|
||||
resource_class: medium
|
||||
parameters:
|
||||
envoy-version:
|
||||
type: enum
|
||||
enum: *supported_envoy_versions
|
||||
default: *default_envoy_version
|
||||
xds-target:
|
||||
type: enum
|
||||
enum: ["server", "client"]
|
||||
default: "server"
|
||||
environment:
|
||||
ENVOY_VERSION: "1.19.3"
|
||||
ENVOY_VERSION: << parameters.envoy-version >>
|
||||
XDS_TARGET: << parameters.xds-target >>
|
||||
steps: &ENVOY_INTEGRATION_TEST_STEPS
|
||||
- checkout
|
||||
# Get go binary from workspace
|
||||
|
@ -891,21 +905,6 @@ jobs:
|
|||
path: *TEST_RESULTS_DIR
|
||||
- run: *notify-slack-failure
|
||||
|
||||
envoy-integration-test-1_20_2:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.20.2"
|
||||
|
||||
envoy-integration-test-1_21_1:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.21.1"
|
||||
|
||||
envoy-integration-test-1_22_0:
|
||||
<<: *ENVOY_TESTS
|
||||
environment:
|
||||
ENVOY_VERSION: "1.22.0"
|
||||
|
||||
# run integration tests for the connect ca providers
|
||||
test-connect-ca-providers:
|
||||
docker:
|
||||
|
@ -930,21 +929,6 @@ jobs:
|
|||
path: *TEST_RESULTS_DIR
|
||||
- run: *notify-slack-failure
|
||||
|
||||
trigger-oss-merge:
|
||||
docker:
|
||||
- image: docker.mirror.hashicorp.services/alpine:3.12
|
||||
steps:
|
||||
- run: apk add --no-cache --no-progress curl jq
|
||||
- run:
|
||||
name: trigger oss merge
|
||||
command: |
|
||||
curl -s -X POST \
|
||||
--header "Circle-Token: ${CIRCLECI_API_TOKEN}" \
|
||||
--header "Content-Type: application/json" \
|
||||
-d '{"build_parameters": {"CIRCLE_JOB": "oss-merge"}}' \
|
||||
"https://circleci.com/api/v1.1/project/github/hashicorp/consul-enterprise/tree/${CIRCLE_BRANCH}" | jq -r '.build_url'
|
||||
- run: *notify-slack-failure
|
||||
|
||||
# Run load tests against a commit
|
||||
load-test:
|
||||
docker:
|
||||
|
@ -1131,18 +1115,13 @@ workflows:
|
|||
- nomad-integration-0_8:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1_19_3:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1_20_2:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1_21_1:
|
||||
requires:
|
||||
- dev-build
|
||||
- envoy-integration-test-1_22_0:
|
||||
- envoy-integration-test:
|
||||
requires:
|
||||
- dev-build
|
||||
matrix:
|
||||
parameters:
|
||||
envoy-version: *supported_envoy_versions
|
||||
xds-target: ["server", "client"]
|
||||
- compatibility-integration-test:
|
||||
requires:
|
||||
- dev-build
|
||||
|
@ -1180,16 +1159,6 @@ workflows:
|
|||
requires:
|
||||
- ember-build-ent
|
||||
- noop
|
||||
workflow-automation:
|
||||
unless: << pipeline.parameters.trigger-load-test >>
|
||||
jobs:
|
||||
- trigger-oss-merge:
|
||||
context: team-consul
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- /release\/\d+\.\d+\.x$/
|
||||
|
||||
load-test:
|
||||
when: << pipeline.parameters.trigger-load-test >>
|
||||
|
|
|
@ -7,12 +7,6 @@ provider "aws" {
|
|||
assume_role {
|
||||
role_arn = var.role_arn
|
||||
}
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Environment = "ConsulLoadTest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "load-test" {
|
||||
|
@ -21,6 +15,7 @@ module "load-test" {
|
|||
vpc_az = ["us-east-2a", "us-east-2b"]
|
||||
vpc_name = var.vpc_name
|
||||
vpc_cidr = "10.0.0.0/16"
|
||||
vpc_allwed_ssh_cidr = "0.0.0.0/0"
|
||||
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
|
||||
private_subnet_cidrs = ["10.0.3.0/24"]
|
||||
test_public_ip = true
|
||||
|
|
|
@ -6,7 +6,7 @@ set -uo pipefail
|
|||
### It is still up to the reviewer to make sure that any tests added are needed and meaningful.
|
||||
|
||||
# search for any "new" or modified metric emissions
|
||||
metrics_modified=$(git --no-pager diff HEAD origin/main | grep -i "SetGauge\|EmitKey\|IncrCounter\|AddSample\|MeasureSince\|UpdateFilter")
|
||||
metrics_modified=$(git --no-pager diff origin/main...HEAD | grep -i "SetGauge\|EmitKey\|IncrCounter\|AddSample\|MeasureSince\|UpdateFilter")
|
||||
# search for PR body or title metric references
|
||||
metrics_in_pr_body=$(echo "${PR_BODY-""}" | grep -i "metric")
|
||||
metrics_in_pr_title=$(echo "${PR_TITLE-""}" | grep -i "metric")
|
||||
|
|
|
@ -73,7 +73,7 @@ function verify_rpm {
|
|||
docker_platform="linux/amd64"
|
||||
docker_image="amd64/centos:7"
|
||||
;;
|
||||
*.arm.rpm)
|
||||
*.armv7hl.rpm)
|
||||
docker_platform="linux/arm/v7"
|
||||
docker_image="arm32v7/fedora:36"
|
||||
;;
|
||||
|
@ -120,7 +120,7 @@ function verify_deb {
|
|||
docker_platform="linux/amd64"
|
||||
docker_image="amd64/debian:bullseye"
|
||||
;;
|
||||
*_arm.deb)
|
||||
*_armhf.deb)
|
||||
docker_platform="linux/arm/v7"
|
||||
docker_image="arm32v7/debian:bullseye"
|
||||
;;
|
||||
|
|
|
@ -15,6 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
product-version: ${{ steps.get-product-version.outputs.product-version }}
|
||||
product-date: ${{ steps.get-product-version.outputs.product-date }}
|
||||
pre-version: ${{ steps.get-product-version.outputs.pre-version }}
|
||||
pkg-version: ${{ steps.get-product-version.outputs.pkg-version }}
|
||||
shared-ldflags: ${{ steps.shared-ldflags.outputs.shared-ldflags }}
|
||||
|
@ -24,6 +25,7 @@ jobs:
|
|||
id: get-product-version
|
||||
run: |
|
||||
CONSUL_VERSION=$(build-support/scripts/version.sh -r)
|
||||
CONSUL_DATE=$(build-support/scripts/build-date.sh)
|
||||
## TODO: This assumes `make version` outputs 1.1.1+ent-prerel
|
||||
IFS="+" read VERSION _other <<< "$CONSUL_VERSION"
|
||||
IFS="-" read _other PREREL_VERSION <<< "$CONSUL_VERSION"
|
||||
|
@ -32,12 +34,15 @@ jobs:
|
|||
## [version]{-prerelease}+ent before then, we'll need to add
|
||||
## logic to handle presense/absence of the prerelease
|
||||
echo "::set-output name=product-version::${CONSUL_VERSION}"
|
||||
echo "::set-output name=product-date::${CONSUL_DATE}"
|
||||
echo "::set-output name=pre-version::${PREREL_VERSION}"
|
||||
echo "::set-output name=pkg-version::${VERSION}"
|
||||
|
||||
- name: Set shared -ldflags
|
||||
id: shared-ldflags
|
||||
run: echo "::set-output name=shared-ldflags::-X github.com/hashicorp/consul/version.GitCommit=${GITHUB_SHA::8} -X github.com/hashicorp/consul/version.GitDescribe=${{ steps.get-product-version.outputs.product-version }}"
|
||||
run: |
|
||||
T="github.com/hashicorp/consul/version"
|
||||
echo "::set-output name=shared-ldflags::-X ${T}.GitCommit=${GITHUB_SHA::8} -X ${T}.GitDescribe=${{ steps.get-product-version.outputs.product-version }} -X ${T}.BuildDate=${{ steps.get-product-version.outputs.product-date }}"
|
||||
|
||||
generate-metadata-file:
|
||||
needs: get-product-version
|
||||
|
@ -95,9 +100,11 @@ jobs:
|
|||
- name: Build UI
|
||||
run: |
|
||||
CONSUL_VERSION=${{ needs.get-product-version.outputs.product-version }}
|
||||
CONSUL_DATE=${{ needs.get-product-version.outputs.product-date }}
|
||||
CONSUL_BINARY_TYPE=${CONSUL_BINARY_TYPE}
|
||||
CONSUL_COPYRIGHT_YEAR=$(git show -s --format=%cd --date=format:%Y HEAD)
|
||||
echo "consul_version is ${CONSUL_VERSION}"
|
||||
echo "consul_date is ${CONSUL_DATE}"
|
||||
echo "consul binary type is ${CONSUL_BINARY_TYPE}"
|
||||
echo "consul copyright year is ${CONSUL_COPYRIGHT_YEAR}"
|
||||
cd ui && make && cd ..
|
||||
|
@ -225,6 +232,14 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Strip everything but MAJOR.MINOR from the version string and add a `-dev` suffix
|
||||
# This naming convention will be used ONLY for per-commit dev images
|
||||
- name: Set docker dev tag
|
||||
run: |
|
||||
version="${{ env.version }}"
|
||||
echo "dev_tag=${version%.*}-dev" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker Build (Action)
|
||||
uses: hashicorp/actions-docker-build@v1
|
||||
with:
|
||||
|
@ -235,8 +250,8 @@ jobs:
|
|||
docker.io/hashicorp/${{env.repo}}:${{env.version}}
|
||||
public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}}
|
||||
dev_tags: |
|
||||
docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }}
|
||||
docker.io/hashicorppreview/${{ env.repo }}:${{ env.version }}-${{ github.sha }}
|
||||
docker.io/hashicorppreview/${{ env.repo }}:${{ env.dev_tag }}
|
||||
docker.io/hashicorppreview/${{ env.repo }}:${{ env.dev_tag }}-${{ github.sha }}
|
||||
smoke_test: .github/scripts/verify_docker.sh v${{ env.version }}
|
||||
|
||||
build-docker-redhat:
|
||||
|
@ -256,7 +271,7 @@ jobs:
|
|||
version: ${{env.version}}
|
||||
target: ubi
|
||||
arch: amd64
|
||||
redhat_tag: scan.connect.redhat.com/ospid-612d01d49f14588c41ebf67c/${{env.repo}}:${{env.version}}-ubi
|
||||
redhat_tag: scan.connect.redhat.com/ospid-60f9fdbec3a80eac643abedf/${{env.repo}}:${{env.version}}-ubi
|
||||
smoke_test: .github/scripts/verify_docker.sh v${{ env.version }}
|
||||
|
||||
verify-linux:
|
||||
|
@ -324,7 +339,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ["i386", "amd64", "arm", "arm64"]
|
||||
arch: ["i386", "amd64", "armhf", "arm64"]
|
||||
# fail-fast: true
|
||||
env:
|
||||
version: ${{ needs.get-product-version.outputs.product-version }}
|
||||
|
@ -332,14 +347,14 @@ jobs:
|
|||
name: Verify ${{ matrix.arch }} debian package
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Set package version
|
||||
run: |
|
||||
echo "pkg_version=$(echo ${{ needs.get-product-version.outputs.product-version }} | sed 's/\-/~/g')" >> $GITHUB_ENV
|
||||
echo "pkg_version=$(echo ${{ needs.get-product-version.outputs.product-version }} | sed 's/\-/~/g')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set package name
|
||||
run: |
|
||||
echo "pkg_name=consul_${{ env.pkg_version }}-1_${{ matrix.arch }}.deb" >> $GITHUB_ENV
|
||||
echo "pkg_name=consul_${{ env.pkg_version }}-1_${{ matrix.arch }}.deb" >> $GITHUB_ENV
|
||||
|
||||
- name: Download workflow artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
@ -361,7 +376,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ["i386", "x86_64", "arm", "aarch64"]
|
||||
arch: ["i386", "x86_64", "armv7hl", "aarch64"]
|
||||
# fail-fast: true
|
||||
env:
|
||||
version: ${{ needs.get-product-version.outputs.product-version }}
|
||||
|
@ -372,12 +387,12 @@ jobs:
|
|||
|
||||
- name: Set package version
|
||||
run: |
|
||||
echo "pkg_version=$(echo ${{ needs.get-product-version.outputs.product-version }} | sed 's/\-/~/g')" >> $GITHUB_ENV
|
||||
echo "pkg_version=$(echo ${{ needs.get-product-version.outputs.product-version }} | sed 's/\-/~/g')" >> $GITHUB_ENV
|
||||
|
||||
- name: Set package name
|
||||
run: |
|
||||
echo "pkg_name=consul-${{ env.pkg_version }}-1.${{ matrix.arch }}.rpm" >> $GITHUB_ENV
|
||||
|
||||
echo "pkg_name=consul-${{ env.pkg_version }}-1.${{ matrix.arch }}.rpm" >> $GITHUB_ENV
|
||||
|
||||
- name: Download workflow artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
@ -388,5 +403,5 @@ jobs:
|
|||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Verify ${{ matrix.arch }} rpm
|
||||
- name: Verify ${{ matrix.arch }} rpm
|
||||
run: ./.github/scripts/verify_artifact.sh ${{ env.pkg_name }} v${{ env.version }}
|
||||
|
|
|
@ -5,7 +5,7 @@ container {
|
|||
}
|
||||
|
||||
binary {
|
||||
secrets = true
|
||||
secrets = false
|
||||
go_modules = false
|
||||
osv = true
|
||||
oss_index = true
|
||||
|
|
55
CHANGELOG.md
55
CHANGELOG.md
|
@ -1,3 +1,58 @@
|
|||
## 1.13.0-alpha2 (June 21, 2022)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* api: `merge-central-config` query parameter support added to `/catalog/node-services/:node-name` API, to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13450](https://github.com/hashicorp/consul/issues/13450)]
|
||||
* connect: Update Envoy support matrix to latest patch releases (1.22.2, 1.21.3, 1.20.4, 1.19.5) [[GH-13431](https://github.com/hashicorp/consul/issues/13431)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* ui: Fix incorrect text on certain page empty states [[GH-13409](https://github.com/hashicorp/consul/issues/13409)]
|
||||
|
||||
## 1.13.0-alpha1 (June 15, 2022)
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
||||
* config-entry: Exporting a specific service name across all namespace is invalid.
|
||||
|
||||
FEATURES:
|
||||
|
||||
* acl: It is now possible to login and logout using the gRPC API [[GH-12935](https://github.com/hashicorp/consul/issues/12935)]
|
||||
* agent: Added information about build date alongside other version information for Consul. Extended /agent/self endpoint and `consul version` commands
|
||||
to report this. Agent also reports build date in log on startup. [[GH-13357](https://github.com/hashicorp/consul/issues/13357)]
|
||||
* ca: Leaf certificates can now be obtained via the gRPC API: `Sign` [[GH-12787](https://github.com/hashicorp/consul/issues/12787)]
|
||||
* checks: add UDP health checks.. [[GH-12722](https://github.com/hashicorp/consul/issues/12722)]
|
||||
* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-12825](https://github.com/hashicorp/consul/issues/12825)]
|
||||
* grpc: New gRPC endpoint to return envoy bootstrap parameters. [[GH-1717](https://github.com/hashicorp/consul/issues/1717)]
|
||||
* grpc: New gRPC service and endpoint to return the list of supported consul dataplane features [[GH-12695](https://github.com/hashicorp/consul/issues/12695)]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* api: `merge-central-config` query parameter support added to some catalog and health endpoints to view a fully resolved service definition (especially when not written into the catalog that way). [[GH-13001](https://github.com/hashicorp/consul/issues/13001)]
|
||||
* api: add the ability to specify a path prefix for when consul is behind a reverse proxy or API gateway [[GH-12914](https://github.com/hashicorp/consul/issues/12914)]
|
||||
* connect: add validation to ensure connect native services have a port or socketpath specified on catalog registration.
|
||||
This was the only missing piece to ensure all mesh services are validated for a port (or socketpath) specification on catalog registration. [[GH-12881](https://github.com/hashicorp/consul/issues/12881)]
|
||||
* Support Vault namespaces in Connect CA by adding RootPKINamespace and
|
||||
IntermediatePKINamespace fields to the config. [[GH-12904](https://github.com/hashicorp/consul/issues/12904)]
|
||||
* acl: Clarify node/service identities must be lowercase [[GH-12807](https://github.com/hashicorp/consul/issues/12807)]
|
||||
* connect: Added a `max_inbound_connections` setting to service-defaults for limiting the number of concurrent inbound connections to each service instance. [[GH-13143](https://github.com/hashicorp/consul/issues/13143)]
|
||||
* dns: Added support for specifying admin partition in node lookups. [[GH-13421](https://github.com/hashicorp/consul/issues/13421)]
|
||||
* grpc: Add a new ServerDiscovery.WatchServers gRPC endpoint for being notified when the set of ready servers has changed. [[GH-12819](https://github.com/hashicorp/consul/issues/12819)]
|
||||
* telemetry: Added `consul.raft.thread.main.saturation` and `consul.raft.thread.fsm.saturation` metrics to measure approximate saturation of the Raft goroutines [[GH-12865](https://github.com/hashicorp/consul/issues/12865)]
|
||||
* telemetry: Added a `consul.server.isLeader` metric to track if a server is a leader or not. [[GH-13304](https://github.com/hashicorp/consul/issues/13304)]
|
||||
* ui: removed external dependencies for serving UI assets in favor of Go's native embed capabilities [[GH-10996](https://github.com/hashicorp/consul/issues/10996)]
|
||||
* ui: upgrade ember-composable-helpers to v5.x [[GH-13394](https://github.com/hashicorp/consul/issues/13394)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* acl: Fixed a bug where the ACL down policy wasn't being applied on remote errors from the primary datacenter. [[GH-12885](https://github.com/hashicorp/consul/issues/12885)]
|
||||
* agent: Fixed a bug in HTTP handlers where URLs were being decoded twice [[GH-13256](https://github.com/hashicorp/consul/issues/13256)]
|
||||
* deps: Update go-grpc/grpc, resolving connection memory leak [[GH-13051](https://github.com/hashicorp/consul/issues/13051)]
|
||||
* fix a bug that caused an error when creating `grpc` or `http2` ingress gateway listeners with multiple services [[GH-13127](https://github.com/hashicorp/consul/issues/13127)]
|
||||
* proxycfg: Fixed a minor bug that would cause configuring a terminating gateway to watch too many service resolvers and waste resources doing filtering. [[GH-13012](https://github.com/hashicorp/consul/issues/13012)]
|
||||
* raft: upgrade to v1.3.8 which fixes a bug where non cluster member can still be able to participate in an election. [[GH-12844](https://github.com/hashicorp/consul/issues/12844)]
|
||||
* serf: upgrade serf to v0.9.8 which fixes a bug that crashes Consul when serf keyrings are listed [[GH-13062](https://github.com/hashicorp/consul/issues/13062)]
|
||||
|
||||
## 1.12.2 (June 3, 2022)
|
||||
|
||||
BUG FIXES:
|
||||
|
|
|
@ -25,7 +25,9 @@ GIT_COMMIT?=$(shell git rev-parse --short HEAD)
|
|||
GIT_COMMIT_YEAR?=$(shell git show -s --format=%cd --date=format:%Y HEAD)
|
||||
GIT_DIRTY?=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||
GIT_IMPORT=github.com/hashicorp/consul/version
|
||||
GOLDFLAGS=-X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT)$(GIT_DIRTY)
|
||||
DATE_FORMAT="%Y-%m-%dT%H:%M:%SZ" # it's tricky to do an RFC3339 format in a cross platform way, so we hardcode UTC
|
||||
GIT_DATE=$(shell $(CURDIR)/build-support/scripts/build-date.sh) # we're using this for build date because it's stable across platform builds
|
||||
GOLDFLAGS=-X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT)$(GIT_DIRTY) -X $(GIT_IMPORT).BuildDate=$(GIT_DATE)
|
||||
|
||||
ifeq ($(FORCE_REBUILD),1)
|
||||
NOCACHE=--no-cache
|
||||
|
@ -331,12 +333,12 @@ ifeq ("$(GOTAGS)","")
|
|||
@docker tag consul-dev:latest consul:local
|
||||
@docker run --rm -t consul:local consul version
|
||||
@cd ./test/integration/consul-container && \
|
||||
go test -v -timeout=30m ./upgrade --target-version local --latest-version latest
|
||||
go test -v -timeout=30m ./... --target-version local --latest-version latest
|
||||
else
|
||||
@docker tag consul-dev:latest hashicorp/consul-enterprise:local
|
||||
@docker run --rm -t hashicorp/consul-enterprise:local consul version
|
||||
@cd ./test/integration/consul-container && \
|
||||
go test -v -timeout=30m ./upgrade --tags $(GOTAGS) --target-version local --latest-version latest
|
||||
go test -v -timeout=30m ./... --tags $(GOTAGS) --target-version local --latest-version latest
|
||||
endif
|
||||
|
||||
.PHONY: test-metrics-integ
|
||||
|
|
|
@ -49,6 +49,7 @@ const (
|
|||
ResourceQuery Resource = "query"
|
||||
ResourceService Resource = "service"
|
||||
ResourceSession Resource = "session"
|
||||
ResourcePeering Resource = "peering"
|
||||
)
|
||||
|
||||
// Authorizer is the interface for policy enforcement.
|
||||
|
@ -540,6 +541,14 @@ func Enforce(authz Authorizer, rsc Resource, segment string, access string, ctx
|
|||
case "write":
|
||||
return authz.SessionWrite(segment, ctx), nil
|
||||
}
|
||||
case ResourcePeering:
|
||||
// TODO (peering) switch this over to using PeeringRead & PeeringWrite methods once implemented
|
||||
switch lowerAccess {
|
||||
case "read":
|
||||
return authz.OperatorRead(ctx), nil
|
||||
case "write":
|
||||
return authz.OperatorWrite(ctx), nil
|
||||
}
|
||||
default:
|
||||
if processed, decision, err := enforceEnterprise(authz, rsc, segment, lowerAccess, ctx); processed {
|
||||
return decision, err
|
||||
|
|
|
@ -462,6 +462,34 @@ func TestACL_Enforce(t *testing.T) {
|
|||
ret: Deny,
|
||||
err: "Invalid access level",
|
||||
},
|
||||
{
|
||||
// TODO (peering) Update to use PeeringRead
|
||||
method: "OperatorRead",
|
||||
resource: ResourcePeering,
|
||||
access: "read",
|
||||
ret: Allow,
|
||||
},
|
||||
{
|
||||
// TODO (peering) Update to use PeeringRead
|
||||
method: "OperatorRead",
|
||||
resource: ResourcePeering,
|
||||
access: "read",
|
||||
ret: Deny,
|
||||
},
|
||||
{
|
||||
// TODO (peering) Update to use PeeringWrite
|
||||
method: "OperatorWrite",
|
||||
resource: ResourcePeering,
|
||||
access: "write",
|
||||
ret: Allow,
|
||||
},
|
||||
{
|
||||
// TODO (peering) Update to use PeeringWrite
|
||||
method: "OperatorWrite",
|
||||
resource: ResourcePeering,
|
||||
access: "write",
|
||||
ret: Deny,
|
||||
},
|
||||
{
|
||||
method: "PreparedQueryRead",
|
||||
resource: ResourceQuery,
|
||||
|
|
|
@ -82,7 +82,9 @@ func (m *EnterpriseMeta) MergeNoWildcard(_ *EnterpriseMeta) {
|
|||
// do nothing
|
||||
}
|
||||
|
||||
func (_ *EnterpriseMeta) Normalize() {}
|
||||
func (_ *EnterpriseMeta) Normalize() {}
|
||||
func (_ *EnterpriseMeta) NormalizePartition() {}
|
||||
func (_ *EnterpriseMeta) NormalizeNamespace() {}
|
||||
|
||||
func (m *EnterpriseMeta) Matches(_ *EnterpriseMeta) bool {
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
acl.Authorizer
|
||||
// TODO: likely we can reduce this interface
|
||||
ACLIdentity structs.ACLIdentity
|
||||
}
|
||||
|
||||
func (a Result) AccessorID() string {
|
||||
if a.ACLIdentity == nil {
|
||||
return ""
|
||||
}
|
||||
return a.ACLIdentity.ID()
|
||||
}
|
||||
|
||||
func (a Result) Identity() structs.ACLIdentity {
|
||||
return a.ACLIdentity
|
||||
}
|
||||
|
||||
func (a Result) ToAllowAuthorizer() acl.AllowAuthorizer {
|
||||
return acl.AllowAuthorizer{Authorizer: a, AccessorID: a.AccessorID()}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/hashicorp/serf/serf"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/consul"
|
||||
"github.com/hashicorp/consul/agent/local"
|
||||
|
@ -94,15 +95,15 @@ func (a *TestACLAgent) ResolveToken(secretID string) (acl.Authorizer, error) {
|
|||
return authz, err
|
||||
}
|
||||
|
||||
func (a *TestACLAgent) ResolveTokenAndDefaultMeta(secretID string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (consul.ACLResolveResult, error) {
|
||||
func (a *TestACLAgent) ResolveTokenAndDefaultMeta(secretID string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error) {
|
||||
authz, err := a.ResolveToken(secretID)
|
||||
if err != nil {
|
||||
return consul.ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
}
|
||||
|
||||
identity, err := a.resolveIdentFn(secretID)
|
||||
if err != nil {
|
||||
return consul.ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
}
|
||||
|
||||
// Default the EnterpriseMeta based on the Tokens meta or actual defaults
|
||||
|
@ -116,7 +117,7 @@ func (a *TestACLAgent) ResolveTokenAndDefaultMeta(secretID string, entMeta *acl.
|
|||
// Use the meta to fill in the ACL authorization context
|
||||
entMeta.FillAuthzContext(authzContext)
|
||||
|
||||
return consul.ACLResolveResult{Authorizer: authz, ACLIdentity: identity}, err
|
||||
return resolver.Result{Authorizer: authz, ACLIdentity: identity}, err
|
||||
}
|
||||
|
||||
// All of these are stubs to satisfy the interface
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/ae"
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
||||
|
@ -177,7 +178,7 @@ type delegate interface {
|
|||
// actions based on the permissions granted to the token.
|
||||
// If either entMeta or authzContext are non-nil they will be populated with the
|
||||
// default partition and namespace from the token.
|
||||
ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (consul.ACLResolveResult, error)
|
||||
ResolveTokenAndDefaultMeta(token string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error)
|
||||
|
||||
RPC(method string, args interface{}, reply interface{}) error
|
||||
SnapshotRPC(args *structs.SnapshotRequest, in io.Reader, out io.Writer, replyFn structs.SnapshotReplyFn) error
|
||||
|
@ -638,30 +639,8 @@ func (a *Agent) Start(ctx context.Context) error {
|
|||
go a.baseDeps.ViewStore.Run(&lib.StopChannelContext{StopCh: a.shutdownCh})
|
||||
|
||||
// Start the proxy config manager.
|
||||
proxyDataSources := proxycfg.DataSources{
|
||||
CARoots: proxycfgglue.CacheCARoots(a.cache),
|
||||
CompiledDiscoveryChain: proxycfgglue.CacheCompiledDiscoveryChain(a.cache),
|
||||
ConfigEntry: proxycfgglue.CacheConfigEntry(a.cache),
|
||||
ConfigEntryList: proxycfgglue.CacheConfigEntryList(a.cache),
|
||||
Datacenters: proxycfgglue.CacheDatacenters(a.cache),
|
||||
FederationStateListMeshGateways: proxycfgglue.CacheFederationStateListMeshGateways(a.cache),
|
||||
GatewayServices: proxycfgglue.CacheGatewayServices(a.cache),
|
||||
Health: proxycfgglue.Health(a.rpcClientHealth),
|
||||
HTTPChecks: proxycfgglue.CacheHTTPChecks(a.cache),
|
||||
Intentions: proxycfgglue.CacheIntentions(a.cache),
|
||||
IntentionUpstreams: proxycfgglue.CacheIntentionUpstreams(a.cache),
|
||||
InternalServiceDump: proxycfgglue.CacheInternalServiceDump(a.cache),
|
||||
LeafCertificate: proxycfgglue.CacheLeafCertificate(a.cache),
|
||||
PreparedQuery: proxycfgglue.CachePrepraredQuery(a.cache),
|
||||
ResolvedServiceConfig: proxycfgglue.CacheResolvedServiceConfig(a.cache),
|
||||
ServiceList: proxycfgglue.CacheServiceList(a.cache),
|
||||
TrustBundle: proxycfgglue.CacheTrustBundle(a.cache),
|
||||
TrustBundleList: proxycfgglue.CacheTrustBundleList(a.cache),
|
||||
ExportedPeeredServices: proxycfgglue.CacheExportedPeeredServices(a.cache),
|
||||
}
|
||||
a.fillEnterpriseProxyDataSources(&proxyDataSources)
|
||||
a.proxyConfig, err = proxycfg.NewManager(proxycfg.ManagerConfig{
|
||||
DataSources: proxyDataSources,
|
||||
DataSources: a.proxyDataSources(),
|
||||
Logger: a.logger.Named(logging.ProxyConfig),
|
||||
Source: &structs.QuerySource{
|
||||
Datacenter: a.config.Datacenter,
|
||||
|
@ -745,12 +724,6 @@ func (a *Agent) Start(ctx context.Context) error {
|
|||
go a.retryJoinWAN()
|
||||
}
|
||||
|
||||
// DEPRECATED: Warn users if they're emitting deprecated metrics. Remove this warning and the flagged metrics in a
|
||||
// future release of Consul.
|
||||
if !a.config.Telemetry.DisableCompatOneNine {
|
||||
a.logger.Warn("DEPRECATED Backwards compatibility with pre-1.9 metrics enabled. These metrics will be removed in Consul 1.13. Consider not using this flag and rework instrumentation for 1.10 style http metrics.")
|
||||
}
|
||||
|
||||
if a.tlsConfigurator.Cert() != nil {
|
||||
m := tlsCertExpirationMonitor(a.tlsConfigurator, a.logger)
|
||||
go m.Monitor(&lib.StopChannelContext{StopCh: a.shutdownCh})
|
||||
|
@ -3935,12 +3908,6 @@ func (a *Agent) reloadConfig(autoReload bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
// DEPRECATED: Warn users on reload if they're emitting deprecated metrics. Remove this warning and the flagged
|
||||
// metrics in a future release of Consul.
|
||||
if !a.config.Telemetry.DisableCompatOneNine {
|
||||
a.logger.Warn("DEPRECATED Backwards compatibility with pre-1.9 metrics enabled. These metrics will be removed in Consul 1.13. Consider not using this flag and rework instrumentation for 1.10 style http metrics.")
|
||||
}
|
||||
|
||||
return a.reloadConfigInternal(newCfg)
|
||||
}
|
||||
|
||||
|
@ -4285,6 +4252,49 @@ func (a *Agent) listenerPortLocked(svcID structs.ServiceID, checkID structs.Chec
|
|||
return port, nil
|
||||
}
|
||||
|
||||
func (a *Agent) proxyDataSources() proxycfg.DataSources {
|
||||
sources := proxycfg.DataSources{
|
||||
CARoots: proxycfgglue.CacheCARoots(a.cache),
|
||||
CompiledDiscoveryChain: proxycfgglue.CacheCompiledDiscoveryChain(a.cache),
|
||||
ConfigEntry: proxycfgglue.CacheConfigEntry(a.cache),
|
||||
ConfigEntryList: proxycfgglue.CacheConfigEntryList(a.cache),
|
||||
Datacenters: proxycfgglue.CacheDatacenters(a.cache),
|
||||
FederationStateListMeshGateways: proxycfgglue.CacheFederationStateListMeshGateways(a.cache),
|
||||
GatewayServices: proxycfgglue.CacheGatewayServices(a.cache),
|
||||
Health: proxycfgglue.Health(a.rpcClientHealth),
|
||||
HTTPChecks: proxycfgglue.CacheHTTPChecks(a.cache),
|
||||
Intentions: proxycfgglue.CacheIntentions(a.cache),
|
||||
IntentionUpstreams: proxycfgglue.CacheIntentionUpstreams(a.cache),
|
||||
InternalServiceDump: proxycfgglue.CacheInternalServiceDump(a.cache),
|
||||
LeafCertificate: proxycfgglue.CacheLeafCertificate(a.cache),
|
||||
PeeredUpstreams: proxycfgglue.CachePeeredUpstreams(a.cache),
|
||||
PreparedQuery: proxycfgglue.CachePrepraredQuery(a.cache),
|
||||
ResolvedServiceConfig: proxycfgglue.CacheResolvedServiceConfig(a.cache),
|
||||
ServiceList: proxycfgglue.CacheServiceList(a.cache),
|
||||
TrustBundle: proxycfgglue.CacheTrustBundle(a.cache),
|
||||
TrustBundleList: proxycfgglue.CacheTrustBundleList(a.cache),
|
||||
ExportedPeeredServices: proxycfgglue.CacheExportedPeeredServices(a.cache),
|
||||
}
|
||||
|
||||
if server, ok := a.delegate.(*consul.Server); ok {
|
||||
deps := proxycfgglue.ServerDataSourceDeps{
|
||||
EventPublisher: a.baseDeps.EventPublisher,
|
||||
ViewStore: a.baseDeps.ViewStore,
|
||||
Logger: a.logger.Named("proxycfg.server-data-sources"),
|
||||
ACLResolver: a.delegate,
|
||||
GetStore: func() proxycfgglue.Store { return server.FSM().State() },
|
||||
}
|
||||
sources.ConfigEntry = proxycfgglue.ServerConfigEntry(deps)
|
||||
sources.ConfigEntryList = proxycfgglue.ServerConfigEntryList(deps)
|
||||
sources.Intentions = proxycfgglue.ServerIntentions(deps)
|
||||
sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps)
|
||||
}
|
||||
|
||||
a.fillEnterpriseProxyDataSources(&sources)
|
||||
return sources
|
||||
|
||||
}
|
||||
|
||||
func listenerPortKey(svcID structs.ServiceID, checkID structs.CheckID) string {
|
||||
return fmt.Sprintf("%s:%s", svcID, checkID)
|
||||
}
|
||||
|
|
|
@ -91,6 +91,7 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i
|
|||
Revision string
|
||||
Server bool
|
||||
Version string
|
||||
BuildDate string
|
||||
}{
|
||||
Datacenter: s.agent.config.Datacenter,
|
||||
PrimaryDatacenter: s.agent.config.PrimaryDatacenter,
|
||||
|
@ -100,8 +101,10 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i
|
|||
Revision: s.agent.config.Revision,
|
||||
Server: s.agent.config.ServerMode,
|
||||
// We expect the ent version to be part of the reported version string, and that's now part of the metadata, not the actual version.
|
||||
Version: s.agent.config.VersionWithMetadata(),
|
||||
Version: s.agent.config.VersionWithMetadata(),
|
||||
BuildDate: s.agent.config.BuildDate.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
return Self{
|
||||
Config: config,
|
||||
DebugConfig: s.agent.config.Sanitized(),
|
||||
|
@ -1524,6 +1527,8 @@ func (s *HTTPHandlers) AgentConnectCALeafCert(resp http.ResponseWriter, req *htt
|
|||
// not the ID of the service instance.
|
||||
serviceName := strings.TrimPrefix(req.URL.Path, "/v1/agent/connect/ca/leaf/")
|
||||
|
||||
// TODO(peering): expose way to get kind=mesh-gateway type cert with appropriate ACLs
|
||||
|
||||
args := cachetype.ConnectCALeafRequest{
|
||||
Service: serviceName, // Need name not ID
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/connect/ca"
|
||||
|
@ -1645,8 +1646,8 @@ type fakeResolveTokenDelegate struct {
|
|||
authorizer acl.Authorizer
|
||||
}
|
||||
|
||||
func (f fakeResolveTokenDelegate) ResolveTokenAndDefaultMeta(_ string, _ *acl.EnterpriseMeta, _ *acl.AuthorizerContext) (consul.ACLResolveResult, error) {
|
||||
return consul.ACLResolveResult{Authorizer: f.authorizer}, nil
|
||||
func (f fakeResolveTokenDelegate) ResolveTokenAndDefaultMeta(_ string, _ *acl.EnterpriseMeta, _ *acl.AuthorizerContext) (resolver.Result, error) {
|
||||
return resolver.Result{Authorizer: f.authorizer}, nil
|
||||
}
|
||||
|
||||
func TestAgent_Reload(t *testing.T) {
|
||||
|
|
|
@ -558,8 +558,19 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest,
|
|||
}
|
||||
dnsNames = append([]string{"localhost"}, req.DNSSAN...)
|
||||
ipAddresses = append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, req.IPSAN...)
|
||||
} else if req.Kind != "" {
|
||||
if req.Kind != structs.ServiceKindMeshGateway {
|
||||
return result, fmt.Errorf("unsupported kind: %s", req.Kind)
|
||||
}
|
||||
|
||||
id = &connect.SpiffeIDMeshGateway{
|
||||
Host: roots.TrustDomain,
|
||||
Datacenter: req.Datacenter,
|
||||
Partition: req.TargetPartition(),
|
||||
}
|
||||
dnsNames = append(dnsNames, req.DNSSAN...)
|
||||
} else {
|
||||
return result, errors.New("URI must be either service or agent")
|
||||
return result, errors.New("URI must be either service, agent, or kind")
|
||||
}
|
||||
|
||||
// Create a new private key
|
||||
|
@ -665,8 +676,9 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest,
|
|||
type ConnectCALeafRequest struct {
|
||||
Token string
|
||||
Datacenter string
|
||||
Service string // Service name, not ID
|
||||
Agent string // Agent name, not ID
|
||||
Service string // Service name, not ID
|
||||
Agent string // Agent name, not ID
|
||||
Kind structs.ServiceKind // only mesh-gateway for now
|
||||
DNSSAN []string
|
||||
IPSAN []net.IP
|
||||
MinQueryIndex uint64
|
||||
|
@ -677,20 +689,38 @@ type ConnectCALeafRequest struct {
|
|||
}
|
||||
|
||||
func (r *ConnectCALeafRequest) Key() string {
|
||||
if len(r.Agent) > 0 {
|
||||
return fmt.Sprintf("agent:%s", r.Agent)
|
||||
}
|
||||
|
||||
r.EnterpriseMeta.Normalize()
|
||||
|
||||
v, err := hashstructure.Hash([]interface{}{
|
||||
r.Service,
|
||||
r.EnterpriseMeta,
|
||||
r.DNSSAN,
|
||||
r.IPSAN,
|
||||
}, nil)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("service:%d", v)
|
||||
switch {
|
||||
case r.Agent != "":
|
||||
v, err := hashstructure.Hash([]interface{}{
|
||||
r.Agent,
|
||||
r.PartitionOrDefault(),
|
||||
}, nil)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("agent:%d", v)
|
||||
}
|
||||
case r.Kind == structs.ServiceKindMeshGateway:
|
||||
v, err := hashstructure.Hash([]interface{}{
|
||||
r.PartitionOrDefault(),
|
||||
r.DNSSAN,
|
||||
r.IPSAN,
|
||||
}, nil)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("kind:%d", v)
|
||||
}
|
||||
case r.Kind != "":
|
||||
// this is not valid
|
||||
default:
|
||||
v, err := hashstructure.Hash([]interface{}{
|
||||
r.Service,
|
||||
r.EnterpriseMeta,
|
||||
r.DNSSAN,
|
||||
r.IPSAN,
|
||||
}, nil)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("service:%d", v)
|
||||
}
|
||||
}
|
||||
|
||||
// If there is an error, we don't set the key. A blank key forces
|
||||
|
|
|
@ -1104,29 +1104,64 @@ func (r *testGatedRootsRPC) RPC(method string, args interface{}, reply interface
|
|||
}
|
||||
|
||||
func TestConnectCALeaf_Key(t *testing.T) {
|
||||
r1 := ConnectCALeafRequest{Service: "web"}
|
||||
r2 := ConnectCALeafRequest{Service: "api"}
|
||||
|
||||
r3 := ConnectCALeafRequest{DNSSAN: []string{"a.com"}}
|
||||
r4 := ConnectCALeafRequest{DNSSAN: []string{"b.com"}}
|
||||
|
||||
r5 := ConnectCALeafRequest{IPSAN: []net.IP{net.ParseIP("192.168.4.139")}}
|
||||
r6 := ConnectCALeafRequest{IPSAN: []net.IP{net.ParseIP("192.168.4.140")}}
|
||||
// hashstructure will hash the service name + ent meta to produce this key
|
||||
r1Key := r1.Key()
|
||||
r2Key := r2.Key()
|
||||
|
||||
r3Key := r3.Key()
|
||||
r4Key := r4.Key()
|
||||
|
||||
r5Key := r5.Key()
|
||||
r6Key := r6.Key()
|
||||
|
||||
require.True(t, strings.HasPrefix(r1Key, "service:"), "Key %s does not start with service:", r1Key)
|
||||
require.True(t, strings.HasPrefix(r2Key, "service:"), "Key %s does not start with service:", r2Key)
|
||||
require.NotEqual(t, r1Key, r2Key, "Cache keys for different services are not equal")
|
||||
require.NotEqual(t, r3Key, r4Key, "Cache keys for different DNSSAN are not equal")
|
||||
require.NotEqual(t, r5Key, r6Key, "Cache keys for different IPSAN are not equal")
|
||||
r := ConnectCALeafRequest{Agent: "abc"}
|
||||
require.Equal(t, "agent:abc", r.Key())
|
||||
key := func(r ConnectCALeafRequest) string {
|
||||
return r.Key()
|
||||
}
|
||||
t.Run("service", func(t *testing.T) {
|
||||
t.Run("name", func(t *testing.T) {
|
||||
r1 := key(ConnectCALeafRequest{Service: "web"})
|
||||
r2 := key(ConnectCALeafRequest{Service: "api"})
|
||||
require.True(t, strings.HasPrefix(r1, "service:"), "Key %s does not start with service:", r1)
|
||||
require.True(t, strings.HasPrefix(r2, "service:"), "Key %s does not start with service:", r2)
|
||||
require.NotEqual(t, r1, r2, "Cache keys for different services should not be equal")
|
||||
})
|
||||
t.Run("dns-san", func(t *testing.T) {
|
||||
r3 := key(ConnectCALeafRequest{Service: "foo", DNSSAN: []string{"a.com"}})
|
||||
r4 := key(ConnectCALeafRequest{Service: "foo", DNSSAN: []string{"b.com"}})
|
||||
require.NotEqual(t, r3, r4, "Cache keys for different DNSSAN should not be equal")
|
||||
})
|
||||
t.Run("ip-san", func(t *testing.T) {
|
||||
r5 := key(ConnectCALeafRequest{Service: "foo", IPSAN: []net.IP{net.ParseIP("192.168.4.139")}})
|
||||
r6 := key(ConnectCALeafRequest{Service: "foo", IPSAN: []net.IP{net.ParseIP("192.168.4.140")}})
|
||||
require.NotEqual(t, r5, r6, "Cache keys for different IPSAN should not be equal")
|
||||
})
|
||||
})
|
||||
t.Run("agent", func(t *testing.T) {
|
||||
t.Run("name", func(t *testing.T) {
|
||||
r1 := key(ConnectCALeafRequest{Agent: "abc"})
|
||||
require.True(t, strings.HasPrefix(r1, "agent:"), "Key %s does not start with agent:", r1)
|
||||
})
|
||||
t.Run("dns-san ignored", func(t *testing.T) {
|
||||
r3 := key(ConnectCALeafRequest{Agent: "foo", DNSSAN: []string{"a.com"}})
|
||||
r4 := key(ConnectCALeafRequest{Agent: "foo", DNSSAN: []string{"b.com"}})
|
||||
require.Equal(t, r3, r4, "DNSSAN is ignored for agent type")
|
||||
})
|
||||
t.Run("ip-san ignored", func(t *testing.T) {
|
||||
r5 := key(ConnectCALeafRequest{Agent: "foo", IPSAN: []net.IP{net.ParseIP("192.168.4.139")}})
|
||||
r6 := key(ConnectCALeafRequest{Agent: "foo", IPSAN: []net.IP{net.ParseIP("192.168.4.140")}})
|
||||
require.Equal(t, r5, r6, "IPSAN is ignored for agent type")
|
||||
})
|
||||
})
|
||||
t.Run("kind", func(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
r1 := key(ConnectCALeafRequest{Kind: "terminating-gateway"})
|
||||
require.Empty(t, r1)
|
||||
})
|
||||
t.Run("mesh-gateway", func(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
r1 := key(ConnectCALeafRequest{Kind: "mesh-gateway"})
|
||||
require.True(t, strings.HasPrefix(r1, "kind:"), "Key %s does not start with kind:", r1)
|
||||
})
|
||||
t.Run("dns-san", func(t *testing.T) {
|
||||
r3 := key(ConnectCALeafRequest{Kind: "mesh-gateway", DNSSAN: []string{"a.com"}})
|
||||
r4 := key(ConnectCALeafRequest{Kind: "mesh-gateway", DNSSAN: []string{"b.com"}})
|
||||
require.NotEqual(t, r3, r4, "Cache keys for different DNSSAN should not be equal")
|
||||
})
|
||||
t.Run("ip-san", func(t *testing.T) {
|
||||
r5 := key(ConnectCALeafRequest{Kind: "mesh-gateway", IPSAN: []net.IP{net.ParseIP("192.168.4.139")}})
|
||||
r6 := key(ConnectCALeafRequest{Kind: "mesh-gateway", IPSAN: []net.IP{net.ParseIP("192.168.4.140")}})
|
||||
require.NotEqual(t, r5, r6, "Cache keys for different IPSAN should not be equal")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
// Recommended name for registration.
|
||||
const IntentionUpstreamsName = "intention-upstreams"
|
||||
|
||||
// GatewayUpstreams supports fetching upstreams for a given gateway name.
|
||||
// IntentionUpstreams supports fetching upstreams for a given service name.
|
||||
type IntentionUpstreams struct {
|
||||
RegisterOptionsBlockingRefresh
|
||||
RPC RPC
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package cachetype
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
// IntentionUpstreamsDestinationName Recommended name for registration.
|
||||
const IntentionUpstreamsDestinationName = "intention-upstreams-destination"
|
||||
|
||||
// IntentionUpstreamsDestination supports fetching upstreams for a given gateway name.
|
||||
type IntentionUpstreamsDestination struct {
|
||||
RegisterOptionsBlockingRefresh
|
||||
RPC RPC
|
||||
}
|
||||
|
||||
func (i *IntentionUpstreamsDestination) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
||||
var result cache.FetchResult
|
||||
|
||||
// The request should be a ServiceSpecificRequest.
|
||||
reqReal, ok := req.(*structs.ServiceSpecificRequest)
|
||||
if !ok {
|
||||
return result, fmt.Errorf(
|
||||
"Internal cache failure: request wrong type: %T", req)
|
||||
}
|
||||
|
||||
// Lightweight copy this object so that manipulating QueryOptions doesn't race.
|
||||
dup := *reqReal
|
||||
reqReal = &dup
|
||||
|
||||
// Set the minimum query index to our current index so we block
|
||||
reqReal.QueryOptions.MinQueryIndex = opts.MinIndex
|
||||
reqReal.QueryOptions.MaxQueryTime = opts.Timeout
|
||||
|
||||
// Always allow stale - there's no point in hitting leader if the request is
|
||||
// going to be served from cache and end up arbitrarily stale anyway. This
|
||||
// allows cached service-discover to automatically read scale across all
|
||||
// servers too.
|
||||
reqReal.AllowStale = true
|
||||
|
||||
// Fetch
|
||||
var reply structs.IndexedServiceList
|
||||
if err := i.RPC.RPC("Internal.IntentionUpstreamsDestination", reqReal, &reply); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Value = &reply
|
||||
result.Index = reply.QueryMeta.Index
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package cachetype
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIntentionUpstreamsDestination(t *testing.T) {
|
||||
rpc := TestRPC(t)
|
||||
typ := &IntentionUpstreamsDestination{RPC: rpc}
|
||||
|
||||
// Expect the proper RPC call. This also sets the expected value
|
||||
// since that is return-by-pointer in the arguments.
|
||||
var resp *structs.IndexedServiceList
|
||||
rpc.On("RPC", "Internal.IntentionUpstreamsDestination", mock.Anything, mock.Anything).Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*structs.ServiceSpecificRequest)
|
||||
require.Equal(t, uint64(24), req.QueryOptions.MinQueryIndex)
|
||||
require.Equal(t, 1*time.Second, req.QueryOptions.MaxQueryTime)
|
||||
require.True(t, req.AllowStale)
|
||||
require.Equal(t, "foo", req.ServiceName)
|
||||
|
||||
services := structs.ServiceList{
|
||||
{Name: "foo"},
|
||||
}
|
||||
reply := args.Get(2).(*structs.IndexedServiceList)
|
||||
reply.Services = services
|
||||
reply.QueryMeta.Index = 48
|
||||
resp = reply
|
||||
})
|
||||
|
||||
// Fetch
|
||||
resultA, err := typ.Fetch(cache.FetchOptions{
|
||||
MinIndex: 24,
|
||||
Timeout: 1 * time.Second,
|
||||
}, &structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "foo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cache.FetchResult{
|
||||
Value: resp,
|
||||
Index: 48,
|
||||
}, resultA)
|
||||
|
||||
rpc.AssertExpectations(t)
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
|
||||
package cachetype
|
||||
|
||||
import (
|
||||
local "github.com/hashicorp/consul/agent/local"
|
||||
memdb "github.com/hashicorp/go-memdb"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
structs "github.com/hashicorp/consul/agent/structs"
|
||||
|
||||
testing "testing"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockAgent is an autogenerated mock type for the Agent type
|
||||
type MockAgent struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// LocalBlockingQuery provides a mock function with given fields: alwaysBlock, hash, wait, fn
|
||||
func (_m *MockAgent) LocalBlockingQuery(alwaysBlock bool, hash string, wait time.Duration, fn func(memdb.WatchSet) (string, interface{}, error)) (string, interface{}, error) {
|
||||
ret := _m.Called(alwaysBlock, hash, wait, fn)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(bool, string, time.Duration, func(memdb.WatchSet) (string, interface{}, error)) string); ok {
|
||||
r0 = rf(alwaysBlock, hash, wait, fn)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 interface{}
|
||||
if rf, ok := ret.Get(1).(func(bool, string, time.Duration, func(memdb.WatchSet) (string, interface{}, error)) interface{}); ok {
|
||||
r1 = rf(alwaysBlock, hash, wait, fn)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(interface{})
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(bool, string, time.Duration, func(memdb.WatchSet) (string, interface{}, error)) error); ok {
|
||||
r2 = rf(alwaysBlock, hash, wait, fn)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// LocalState provides a mock function with given fields:
|
||||
func (_m *MockAgent) LocalState() *local.State {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *local.State
|
||||
if rf, ok := ret.Get(0).(func() *local.State); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*local.State)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ServiceHTTPBasedChecks provides a mock function with given fields: id
|
||||
func (_m *MockAgent) ServiceHTTPBasedChecks(id structs.ServiceID) []structs.CheckType {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 []structs.CheckType
|
||||
if rf, ok := ret.Get(0).(func(structs.ServiceID) []structs.CheckType); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]structs.CheckType)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewMockAgent creates a new instance of MockAgent. It also registers a cleanup function to assert the mocks expectations.
|
||||
func NewMockAgent(t testing.TB) *MockAgent {
|
||||
mock := &MockAgent{}
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package cachetype
|
||||
|
||||
|
@ -27,9 +27,10 @@ func (_m *MockRPC) RPC(method string, args interface{}, reply interface{}) error
|
|||
return r0
|
||||
}
|
||||
|
||||
// NewMockRPC creates a new instance of MockRPC. It also registers a cleanup function to assert the mocks expectations.
|
||||
// NewMockRPC creates a new instance of MockRPC. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockRPC(t testing.TB) *MockRPC {
|
||||
mock := &MockRPC{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package cachetype
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
pbpeering "github.com/hashicorp/consul/proto/pbpeering"
|
||||
|
||||
testing "testing"
|
||||
)
|
||||
|
||||
// MockTrustBundleLister is an autogenerated mock type for the TrustBundleLister type
|
||||
type MockTrustBundleLister struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// TrustBundleListByService provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockTrustBundleLister) TrustBundleListByService(ctx context.Context, in *pbpeering.TrustBundleListByServiceRequest, opts ...grpc.CallOption) (*pbpeering.TrustBundleListByServiceResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 *pbpeering.TrustBundleListByServiceResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *pbpeering.TrustBundleListByServiceRequest, ...grpc.CallOption) *pbpeering.TrustBundleListByServiceResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*pbpeering.TrustBundleListByServiceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *pbpeering.TrustBundleListByServiceRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewMockTrustBundleLister creates a new instance of MockTrustBundleLister. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockTrustBundleLister(t testing.TB) *MockTrustBundleLister {
|
||||
mock := &MockTrustBundleLister{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package cachetype
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
// Recommended name for registration.
|
||||
const PeeredUpstreamsName = "peered-upstreams"
|
||||
|
||||
// PeeredUpstreams supports fetching imported upstream candidates of a given partition.
|
||||
type PeeredUpstreams struct {
|
||||
RegisterOptionsBlockingRefresh
|
||||
RPC RPC
|
||||
}
|
||||
|
||||
func (i *PeeredUpstreams) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
||||
var result cache.FetchResult
|
||||
|
||||
reqReal, ok := req.(*structs.PartitionSpecificRequest)
|
||||
if !ok {
|
||||
return result, fmt.Errorf(
|
||||
"Internal cache failure: request wrong type: %T", req)
|
||||
}
|
||||
|
||||
// Lightweight copy this object so that manipulating QueryOptions doesn't race.
|
||||
dup := *reqReal
|
||||
reqReal = &dup
|
||||
|
||||
// Set the minimum query index to our current index so we block
|
||||
reqReal.QueryOptions.MinQueryIndex = opts.MinIndex
|
||||
reqReal.QueryOptions.MaxQueryTime = opts.Timeout
|
||||
|
||||
// Always allow stale - there's no point in hitting leader if the request is
|
||||
// going to be served from cache and end up arbitrarily stale anyway. This
|
||||
// allows cached service-discover to automatically read scale across all
|
||||
// servers too.
|
||||
reqReal.AllowStale = true
|
||||
|
||||
// Fetch
|
||||
var reply structs.IndexedPeeredServiceList
|
||||
if err := i.RPC.RPC("Internal.PeeredUpstreams", reqReal, &reply); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Value = &reply
|
||||
result.Index = reply.QueryMeta.Index
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package cachetype
|
||||
|
||||
import (
|
||||
"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/structs"
|
||||
)
|
||||
|
||||
func TestPeeredUpstreams(t *testing.T) {
|
||||
rpc := TestRPC(t)
|
||||
defer rpc.AssertExpectations(t)
|
||||
typ := &PeeredUpstreams{RPC: rpc}
|
||||
|
||||
// Expect the proper RPC call. This also sets the expected value
|
||||
// since that is return-by-pointer in the arguments.
|
||||
var resp *structs.IndexedPeeredServiceList
|
||||
rpc.On("RPC", "Internal.PeeredUpstreams", mock.Anything, mock.Anything).Return(nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*structs.PartitionSpecificRequest)
|
||||
require.Equal(t, uint64(24), req.MinQueryIndex)
|
||||
require.Equal(t, 1*time.Second, req.QueryOptions.MaxQueryTime)
|
||||
require.True(t, req.AllowStale)
|
||||
|
||||
reply := args.Get(2).(*structs.IndexedPeeredServiceList)
|
||||
reply.Index = 48
|
||||
resp = reply
|
||||
})
|
||||
|
||||
// Fetch
|
||||
result, err := typ.Fetch(cache.FetchOptions{
|
||||
MinIndex: 24,
|
||||
Timeout: 1 * time.Second,
|
||||
}, &structs.PartitionSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cache.FetchResult{
|
||||
Value: resp,
|
||||
Index: 48,
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestPeeredUpstreams_badReqType(t *testing.T) {
|
||||
rpc := TestRPC(t)
|
||||
defer rpc.AssertExpectations(t)
|
||||
typ := &PeeredUpstreams{RPC: rpc}
|
||||
|
||||
// Fetch
|
||||
_, err := typ.Fetch(cache.FetchOptions{}, cache.TestRequest(
|
||||
t, cache.RequestInfo{Key: "foo", MinIndex: 64}))
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "wrong type")
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
package cachetype
|
||||
|
||||
//go:generate mockery --all --inpackage
|
||||
|
||||
// RPC is an interface that an RPC client must implement. This is a helper
|
||||
// interface that is implemented by the agent delegate so that Type
|
||||
// implementations can request RPC access.
|
||||
//go:generate mockery --name RPC --inpackage
|
||||
type RPC interface {
|
||||
RPC(method string, args interface{}, reply interface{}) error
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@ import (
|
|||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
testinf "github.com/mitchellh/go-testing-interface"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
)
|
||||
|
||||
// TestRPC returns a mock implementation of the RPC interface.
|
||||
func TestRPC(t testing.T) *MockRPC {
|
||||
func TestRPC(t testinf.T) *MockRPC {
|
||||
// This function is relatively useless but this allows us to perhaps
|
||||
// perform some initialization later.
|
||||
return &MockRPC{}
|
||||
|
@ -21,7 +21,7 @@ func TestRPC(t testing.T) *MockRPC {
|
|||
// Errors will show up as an error type on the resulting channel so a
|
||||
// type switch should be used.
|
||||
func TestFetchCh(
|
||||
t testing.T,
|
||||
t testinf.T,
|
||||
typ cache.Type,
|
||||
opts cache.FetchOptions,
|
||||
req cache.Request,
|
||||
|
@ -43,7 +43,7 @@ func TestFetchCh(
|
|||
// TestFetchChResult tests that the result from TestFetchCh matches
|
||||
// within a reasonable period of time (it expects it to be "immediate" but
|
||||
// waits some milliseconds).
|
||||
func TestFetchChResult(t testing.T, ch <-chan interface{}, expected interface{}) {
|
||||
func TestFetchChResult(t testinf.T, ch <-chan interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Recommended name for registration.
|
||||
|
@ -19,7 +20,7 @@ type TrustBundle struct {
|
|||
Client TrustBundleReader
|
||||
}
|
||||
|
||||
//go:generate mockery --name TrustBundleReader --inpackage --testonly
|
||||
//go:generate mockery --name TrustBundleReader --inpackage --filename mock_TrustBundleReader_test.go
|
||||
type TrustBundleReader interface {
|
||||
TrustBundleRead(
|
||||
ctx context.Context, in *pbpeering.TrustBundleReadRequest, opts ...grpc.CallOption,
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Recommended name for registration.
|
||||
|
@ -19,6 +20,7 @@ type TrustBundles struct {
|
|||
Client TrustBundleLister
|
||||
}
|
||||
|
||||
//go:generate mockery --name TrustBundleLister --inpackage --filename mock_TrustBundleLister_test.go
|
||||
type TrustBundleLister interface {
|
||||
TrustBundleListByService(
|
||||
ctx context.Context, in *pbpeering.TrustBundleListByServiceRequest, opts ...grpc.CallOption,
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/hashicorp/consul/agent/cache"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
)
|
||||
|
||||
func TestTrustBundles(t *testing.T) {
|
||||
|
@ -105,48 +105,3 @@ func TestTrustBundles_MultipleUpdates(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MockTrustBundleLister is an autogenerated mock type for the TrustBundleLister type
|
||||
type MockTrustBundleLister struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// TrustBundleListByService provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockTrustBundleLister) TrustBundleListByService(ctx context.Context, in *pbpeering.TrustBundleListByServiceRequest, opts ...grpc.CallOption) (*pbpeering.TrustBundleListByServiceResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 *pbpeering.TrustBundleListByServiceResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *pbpeering.TrustBundleListByServiceRequest, ...grpc.CallOption) *pbpeering.TrustBundleListByServiceResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*pbpeering.TrustBundleListByServiceResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *pbpeering.TrustBundleListByServiceRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewMockTrustBundleLister creates a new instance of MockTrustBundleLister. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockTrustBundleLister(t testing.TB) *MockTrustBundleLister {
|
||||
mock := &MockTrustBundleLister{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ import (
|
|||
"github.com/hashicorp/consul/lib/ttlcache"
|
||||
)
|
||||
|
||||
//go:generate mockery --all --inpackage
|
||||
|
||||
// TODO(kit): remove the namespace from these once the metrics themselves change
|
||||
var Gauges = []prometheus.GaugeDefinition{
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package cache
|
||||
|
||||
|
@ -27,9 +27,10 @@ func (_m *MockRequest) CacheInfo() RequestInfo {
|
|||
return r0
|
||||
}
|
||||
|
||||
// NewMockRequest creates a new instance of MockRequest. It also registers a cleanup function to assert the mocks expectations.
|
||||
// NewMockRequest creates a new instance of MockRequest. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockRequest(t testing.TB) *MockRequest {
|
||||
mock := &MockRequest{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package cache
|
||||
|
||||
|
@ -48,9 +48,10 @@ func (_m *MockType) RegisterOptions() RegisterOptions {
|
|||
return r0
|
||||
}
|
||||
|
||||
// NewMockType creates a new instance of MockType. It also registers a cleanup function to assert the mocks expectations.
|
||||
// NewMockType creates a new instance of MockType. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockType(t testing.TB) *MockType {
|
||||
mock := &MockType{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
//
|
||||
// This interface is typically implemented by request structures in
|
||||
// the agent/structs package.
|
||||
//go:generate mockery --name Request --inpackage
|
||||
type Request interface {
|
||||
// CacheInfo returns information used for caching this request.
|
||||
CacheInfo() RequestInfo
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
testinf "github.com/mitchellh/go-testing-interface"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
// TestCacheGetCh returns a channel that returns the result of the Get call.
|
||||
// This is useful for testing timing and concurrency with Get calls. Any
|
||||
// error will be logged, so the result value should always be asserted.
|
||||
func TestCacheGetCh(t testing.T, c *Cache, typ string, r Request) <-chan interface{} {
|
||||
func TestCacheGetCh(t testinf.T, c *Cache, typ string, r Request) <-chan interface{} {
|
||||
resultCh := make(chan interface{})
|
||||
go func() {
|
||||
result, _, err := c.Get(context.Background(), typ, r)
|
||||
|
@ -32,7 +32,7 @@ func TestCacheGetCh(t testing.T, c *Cache, typ string, r Request) <-chan interfa
|
|||
// TestCacheGetChResult tests that the result from TestCacheGetCh matches
|
||||
// within a reasonable period of time (it expects it to be "immediate" but
|
||||
// waits some milliseconds).
|
||||
func TestCacheGetChResult(t testing.T, ch <-chan interface{}, expected interface{}) {
|
||||
func TestCacheGetChResult(t testinf.T, ch <-chan interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
|
@ -51,7 +51,7 @@ func TestCacheGetChResult(t testing.T, ch <-chan interface{}, expected interface
|
|||
// "immediate" but waits some milliseconds). Expected may be given multiple
|
||||
// times and if so these are all waited for and asserted to match but IN ANY
|
||||
// ORDER to ensure we aren't timing dependent.
|
||||
func TestCacheNotifyChResult(t testing.T, ch <-chan UpdateEvent, expected ...UpdateEvent) {
|
||||
func TestCacheNotifyChResult(t testinf.T, ch <-chan UpdateEvent, expected ...UpdateEvent) {
|
||||
t.Helper()
|
||||
|
||||
expectLen := len(expected)
|
||||
|
@ -85,14 +85,14 @@ OUT:
|
|||
|
||||
// TestRequest returns a Request that returns the given cache key and index.
|
||||
// The Reset method can be called to reset it for custom usage.
|
||||
func TestRequest(t testing.T, info RequestInfo) *MockRequest {
|
||||
func TestRequest(t testinf.T, info RequestInfo) *MockRequest {
|
||||
req := &MockRequest{}
|
||||
req.On("CacheInfo").Return(info)
|
||||
return req
|
||||
}
|
||||
|
||||
// TestType returns a MockType that sets default RegisterOptions.
|
||||
func TestType(t testing.T) *MockType {
|
||||
func TestType(t testinf.T) *MockType {
|
||||
typ := &MockType{}
|
||||
typ.On("RegisterOptions").Return(RegisterOptions{
|
||||
SupportsBlocking: true,
|
||||
|
@ -101,7 +101,7 @@ func TestType(t testing.T) *MockType {
|
|||
}
|
||||
|
||||
// TestTypeNonBlocking returns a MockType that returns false to SupportsBlocking.
|
||||
func TestTypeNonBlocking(t testing.T) *MockType {
|
||||
func TestTypeNonBlocking(t testinf.T) *MockType {
|
||||
typ := &MockType{}
|
||||
typ.On("RegisterOptions").Return(RegisterOptions{
|
||||
SupportsBlocking: false,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
)
|
||||
|
||||
// Type implements the logic to fetch certain types of data.
|
||||
//go:generate mockery --name Type --inpackage
|
||||
type Type interface {
|
||||
// Fetch fetches a single unique item.
|
||||
//
|
||||
|
|
|
@ -499,6 +499,10 @@ func (s *HTTPHandlers) CatalogNodeServiceList(resp http.ResponseWriter, req *htt
|
|||
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing node name"}
|
||||
}
|
||||
|
||||
if _, ok := req.URL.Query()["merge-central-config"]; ok {
|
||||
args.MergeCentralConfig = true
|
||||
}
|
||||
|
||||
// Make the RPC request
|
||||
var out structs.IndexedNodeServiceList
|
||||
defer setMeta(resp, &out.QueryMeta)
|
||||
|
|
|
@ -1529,6 +1529,111 @@ func TestCatalogNodeServiceList(t *testing.T) {
|
|||
require.Equal(t, args.Service.Proxy, proxySvc.Proxy)
|
||||
}
|
||||
|
||||
func TestCatalogNodeServiceList_MergeCentralConfig(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||
|
||||
// Register the service
|
||||
registerServiceReq := registerService(t, a)
|
||||
// Register proxy-defaults
|
||||
proxyGlobalEntry := registerProxyDefaults(t, a)
|
||||
// Register service-defaults
|
||||
serviceDefaultsConfigEntry := registerServiceDefaults(t, a, registerServiceReq.Service.Proxy.DestinationServiceName)
|
||||
|
||||
url := fmt.Sprintf("/v1/catalog/node-services/%s?merge-central-config", registerServiceReq.Node)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := a.srv.CatalogNodeServiceList(resp, req)
|
||||
require.NoError(t, err)
|
||||
assertIndex(t, resp)
|
||||
|
||||
nodeServices := obj.(*structs.NodeServiceList)
|
||||
// validate response
|
||||
require.Len(t, nodeServices.Services, 1)
|
||||
validateMergeCentralConfigResponse(t, nodeServices.Services[0].ToServiceNode(nodeServices.Node.Node), registerServiceReq, proxyGlobalEntry, serviceDefaultsConfigEntry)
|
||||
}
|
||||
|
||||
func TestCatalogNodeServiceList_MergeCentralConfigBlocking(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||
|
||||
// Register the service
|
||||
registerServiceReq := registerService(t, a)
|
||||
// Register proxy-defaults
|
||||
proxyGlobalEntry := registerProxyDefaults(t, a)
|
||||
|
||||
// Run the query
|
||||
rpcReq := structs.NodeSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: registerServiceReq.Node,
|
||||
MergeCentralConfig: true,
|
||||
}
|
||||
var rpcResp structs.IndexedNodeServiceList
|
||||
require.NoError(t, a.RPC("Catalog.NodeServiceList", &rpcReq, &rpcResp))
|
||||
require.Len(t, rpcResp.NodeServices.Services, 1)
|
||||
nodeService := rpcResp.NodeServices.Services[0]
|
||||
require.Equal(t, registerServiceReq.Service.Service, nodeService.Service)
|
||||
// validate proxy global defaults are resolved in the merged service config
|
||||
require.Equal(t, proxyGlobalEntry.Config, nodeService.Proxy.Config)
|
||||
require.Equal(t, proxyGlobalEntry.Mode, nodeService.Proxy.Mode)
|
||||
|
||||
// Async cause a change - register service defaults
|
||||
waitIndex := rpcResp.Index
|
||||
start := time.Now()
|
||||
var serviceDefaultsConfigEntry structs.ServiceConfigEntry
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Register service-defaults
|
||||
serviceDefaultsConfigEntry = registerServiceDefaults(t, a, registerServiceReq.Service.Proxy.DestinationServiceName)
|
||||
}()
|
||||
|
||||
const waitDuration = 3 * time.Second
|
||||
RUN_BLOCKING_QUERY:
|
||||
|
||||
url := fmt.Sprintf("/v1/catalog/node-services/%s?merge-central-config&wait=%s&index=%d",
|
||||
registerServiceReq.Node, waitDuration.String(), waitIndex)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := a.srv.CatalogNodeServiceList(resp, req)
|
||||
require.NoError(t, err)
|
||||
assertIndex(t, resp)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
idx := getIndex(t, resp)
|
||||
if idx < waitIndex {
|
||||
t.Fatalf("bad index returned: %v", idx)
|
||||
} else if idx == waitIndex {
|
||||
if elapsed > waitDuration {
|
||||
// This should prevent the loop from running longer than the waitDuration
|
||||
t.Fatalf("too slow: %v", elapsed)
|
||||
}
|
||||
goto RUN_BLOCKING_QUERY
|
||||
}
|
||||
// Should block at least 100ms before getting the changed results
|
||||
if elapsed < 100*time.Millisecond {
|
||||
t.Fatalf("too fast: %v", elapsed)
|
||||
}
|
||||
|
||||
nodeServices := obj.(*structs.NodeServiceList)
|
||||
// validate response
|
||||
require.Len(t, nodeServices.Services, 1)
|
||||
validateMergeCentralConfigResponse(t, nodeServices.Services[0].ToServiceNode(nodeServices.Node.Node), registerServiceReq, proxyGlobalEntry, serviceDefaultsConfigEntry)
|
||||
}
|
||||
|
||||
func TestCatalogNodeServices_Filter(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
|
|
@ -804,6 +804,8 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
|
|||
Version: stringVal(c.Version),
|
||||
VersionPrerelease: stringVal(c.VersionPrerelease),
|
||||
VersionMetadata: stringVal(c.VersionMetadata),
|
||||
// What is a sensible default for BuildDate?
|
||||
BuildDate: timeValWithDefault(c.BuildDate, time.Date(1970, 1, 00, 00, 00, 01, 0, time.UTC)),
|
||||
|
||||
// consul configuration
|
||||
ConsulCoordinateUpdateBatchSize: intVal(c.Consul.Coordinate.UpdateBatchSize),
|
||||
|
@ -913,7 +915,6 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
|
|||
CirconusCheckTags: stringVal(c.Telemetry.CirconusCheckTags),
|
||||
CirconusSubmissionInterval: stringVal(c.Telemetry.CirconusSubmissionInterval),
|
||||
CirconusSubmissionURL: stringVal(c.Telemetry.CirconusSubmissionURL),
|
||||
DisableCompatOneNine: boolValWithDefault(c.Telemetry.DisableCompatOneNine, true),
|
||||
DisableHostname: boolVal(c.Telemetry.DisableHostname),
|
||||
DogstatsdAddr: stringVal(c.Telemetry.DogstatsdAddr),
|
||||
DogstatsdTags: c.Telemetry.DogstatsdTags,
|
||||
|
@ -1947,6 +1948,13 @@ func stringVal(v *string) string {
|
|||
return *v
|
||||
}
|
||||
|
||||
func timeValWithDefault(v *time.Time, defaultVal time.Time) time.Time {
|
||||
if v == nil {
|
||||
return defaultVal
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func float64ValWithDefault(v *float64, defaultVal float64) float64 {
|
||||
if v == nil {
|
||||
return defaultVal
|
||||
|
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul"
|
||||
|
||||
|
@ -261,18 +262,19 @@ type Config struct {
|
|||
SnapshotAgent map[string]interface{} `mapstructure:"snapshot_agent"`
|
||||
|
||||
// non-user configurable values
|
||||
AEInterval *string `mapstructure:"ae_interval"`
|
||||
CheckDeregisterIntervalMin *string `mapstructure:"check_deregister_interval_min"`
|
||||
CheckReapInterval *string `mapstructure:"check_reap_interval"`
|
||||
Consul Consul `mapstructure:"consul"`
|
||||
Revision *string `mapstructure:"revision"`
|
||||
SegmentLimit *int `mapstructure:"segment_limit"`
|
||||
SegmentNameLimit *int `mapstructure:"segment_name_limit"`
|
||||
SyncCoordinateIntervalMin *string `mapstructure:"sync_coordinate_interval_min"`
|
||||
SyncCoordinateRateTarget *float64 `mapstructure:"sync_coordinate_rate_target"`
|
||||
Version *string `mapstructure:"version"`
|
||||
VersionPrerelease *string `mapstructure:"version_prerelease"`
|
||||
VersionMetadata *string `mapstructure:"version_metadata"`
|
||||
AEInterval *string `mapstructure:"ae_interval"`
|
||||
CheckDeregisterIntervalMin *string `mapstructure:"check_deregister_interval_min"`
|
||||
CheckReapInterval *string `mapstructure:"check_reap_interval"`
|
||||
Consul Consul `mapstructure:"consul"`
|
||||
Revision *string `mapstructure:"revision"`
|
||||
SegmentLimit *int `mapstructure:"segment_limit"`
|
||||
SegmentNameLimit *int `mapstructure:"segment_name_limit"`
|
||||
SyncCoordinateIntervalMin *string `mapstructure:"sync_coordinate_interval_min"`
|
||||
SyncCoordinateRateTarget *float64 `mapstructure:"sync_coordinate_rate_target"`
|
||||
Version *string `mapstructure:"version"`
|
||||
VersionPrerelease *string `mapstructure:"version_prerelease"`
|
||||
VersionMetadata *string `mapstructure:"version_metadata"`
|
||||
BuildDate *time.Time `mapstructure:"build_date"`
|
||||
|
||||
// Enterprise Only
|
||||
Audit Audit `mapstructure:"audit"`
|
||||
|
@ -672,7 +674,6 @@ type Telemetry struct {
|
|||
CirconusCheckTags *string `mapstructure:"circonus_check_tags"`
|
||||
CirconusSubmissionInterval *string `mapstructure:"circonus_submission_interval"`
|
||||
CirconusSubmissionURL *string `mapstructure:"circonus_submission_url"`
|
||||
DisableCompatOneNine *bool `mapstructure:"disable_compat_1.9"`
|
||||
DisableHostname *bool `mapstructure:"disable_hostname"`
|
||||
DogstatsdAddr *string `mapstructure:"dogstatsd_addr"`
|
||||
DogstatsdTags []string `mapstructure:"dogstatsd_tags"`
|
||||
|
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/raft"
|
||||
|
||||
|
@ -197,8 +198,8 @@ func NonUserSource() Source {
|
|||
|
||||
# SegmentNameLimit is the maximum segment name length.
|
||||
segment_name_limit = 64
|
||||
|
||||
connect = {
|
||||
|
||||
connect = {
|
||||
# 0s causes the value to be ignored and operate without capping
|
||||
# the max time before leaf certs can be generated after a roots change.
|
||||
test_ca_leaf_root_change_spread = "0s"
|
||||
|
@ -210,7 +211,7 @@ func NonUserSource() Source {
|
|||
// versionSource creates a config source for the version parameters.
|
||||
// This should be merged in the tail since these values are not
|
||||
// user configurable.
|
||||
func versionSource(rev, ver, verPre, meta string) Source {
|
||||
func versionSource(rev, ver, verPre, meta string, buildDate time.Time) Source {
|
||||
return LiteralSource{
|
||||
Name: "version",
|
||||
Config: Config{
|
||||
|
@ -218,6 +219,7 @@ func versionSource(rev, ver, verPre, meta string) Source {
|
|||
Version: &ver,
|
||||
VersionPrerelease: &verPre,
|
||||
VersionMetadata: &meta,
|
||||
BuildDate: &buildDate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +227,8 @@ func versionSource(rev, ver, verPre, meta string) Source {
|
|||
// defaultVersionSource returns the version config source for the embedded
|
||||
// version numbers.
|
||||
func defaultVersionSource() Source {
|
||||
return versionSource(version.GitCommit, version.Version, version.VersionPrerelease, version.VersionMetadata)
|
||||
buildDate, _ := time.Parse(time.RFC3339, version.BuildDate) // This has been checked elsewhere
|
||||
return versionSource(version.GitCommit, version.Version, version.VersionPrerelease, version.VersionMetadata, buildDate)
|
||||
}
|
||||
|
||||
// DefaultConsulSource returns the default configuration for the consul agent.
|
||||
|
|
|
@ -62,6 +62,7 @@ type RuntimeConfig struct {
|
|||
Version string
|
||||
VersionPrerelease string
|
||||
VersionMetadata string
|
||||
BuildDate time.Time
|
||||
|
||||
// consul config
|
||||
ConsulCoordinateUpdateMaxBatches int
|
||||
|
@ -1701,6 +1702,10 @@ func sanitize(name string, v reflect.Value) reflect.Value {
|
|||
x := v.Interface().(time.Duration)
|
||||
return reflect.ValueOf(x.String())
|
||||
|
||||
case isTime(typ):
|
||||
x := v.Interface().(time.Time)
|
||||
return reflect.ValueOf(x.String())
|
||||
|
||||
case isString(typ):
|
||||
if strings.HasPrefix(name, "RetryJoinLAN[") || strings.HasPrefix(name, "RetryJoinWAN[") {
|
||||
x := v.Interface().(string)
|
||||
|
@ -1772,6 +1777,7 @@ func sanitize(name string, v reflect.Value) reflect.Value {
|
|||
}
|
||||
|
||||
func isDuration(t reflect.Type) bool { return t == reflect.TypeOf(time.Second) }
|
||||
func isTime(t reflect.Type) bool { return t == reflect.TypeOf(time.Time{}) }
|
||||
func isMap(t reflect.Type) bool { return t.Kind() == reflect.Map }
|
||||
func isNetAddr(t reflect.Type) bool { return t.Implements(reflect.TypeOf((*net.Addr)(nil)).Elem()) }
|
||||
func isPtr(t reflect.Type) bool { return t.Kind() == reflect.Ptr }
|
||||
|
|
|
@ -5695,6 +5695,7 @@ func TestLoad_FullConfig(t *testing.T) {
|
|||
Version: "R909Hblt",
|
||||
VersionPrerelease: "ZT1JOQLn",
|
||||
VersionMetadata: "GtTCa13",
|
||||
BuildDate: time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC),
|
||||
|
||||
// consul configuration
|
||||
ConsulCoordinateUpdateBatchSize: 128,
|
||||
|
@ -6344,7 +6345,6 @@ func TestLoad_FullConfig(t *testing.T) {
|
|||
CirconusCheckTags: "prvO4uBl",
|
||||
CirconusSubmissionInterval: "DolzaflP",
|
||||
CirconusSubmissionURL: "gTcbS93G",
|
||||
DisableCompatOneNine: true,
|
||||
DisableHostname: true,
|
||||
DogstatsdAddr: "0wSndumK",
|
||||
DogstatsdTags: []string{"3N81zSUB", "Xtj8AnXZ"},
|
||||
|
@ -6489,7 +6489,8 @@ func TestLoad_FullConfig(t *testing.T) {
|
|||
ConfigFiles: []string{"testdata/full-config." + format},
|
||||
HCL: []string{fmt.Sprintf(`data_dir = "%s"`, dataDir)},
|
||||
}
|
||||
opts.Overrides = append(opts.Overrides, versionSource("JNtPSav3", "R909Hblt", "ZT1JOQLn", "GtTCa13"))
|
||||
opts.Overrides = append(opts.Overrides, versionSource("JNtPSav3", "R909Hblt", "ZT1JOQLn", "GtTCa13",
|
||||
time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC)))
|
||||
r, err := Load(opts)
|
||||
require.NoError(t, err)
|
||||
prototest.AssertDeepEqual(t, expected, r.RuntimeConfig)
|
||||
|
@ -6683,6 +6684,7 @@ func parseCIDR(t *testing.T, cidr string) *net.IPNet {
|
|||
func TestRuntimeConfig_Sanitize(t *testing.T) {
|
||||
rt := RuntimeConfig{
|
||||
BindAddr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")},
|
||||
BuildDate: time.Date(2019, 11, 20, 5, 0, 0, 0, time.UTC),
|
||||
CheckOutputMaxSize: checks.DefaultBufSize,
|
||||
SerfAdvertiseAddrLAN: &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
|
||||
DNSAddrs: []net.Addr{
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
"BindAddr": "127.0.0.1",
|
||||
"Bootstrap": false,
|
||||
"BootstrapExpect": 0,
|
||||
"BuildDate": "2019-11-20 05:00:00 +0000 UTC",
|
||||
"Cache": {
|
||||
"EntryFetchMaxBurst": 42,
|
||||
"EntryFetchRate": 0.334,
|
||||
|
@ -418,7 +419,6 @@
|
|||
"CirconusSubmissionInterval": "",
|
||||
"CirconusSubmissionURL": "",
|
||||
"Disable": false,
|
||||
"DisableCompatOneNine": false,
|
||||
"DisableHostname": false,
|
||||
"DogstatsdAddr": "",
|
||||
"DogstatsdTags": [],
|
||||
|
|
|
@ -662,7 +662,6 @@ telemetry {
|
|||
prometheus_retention_time = "15s"
|
||||
statsd_address = "drce87cy"
|
||||
statsite_address = "HpFwKB8R"
|
||||
disable_compat_1.9 = true
|
||||
}
|
||||
tls {
|
||||
defaults {
|
||||
|
|
|
@ -658,8 +658,7 @@
|
|||
"metrics_prefix": "ftO6DySn",
|
||||
"prometheus_retention_time": "15s",
|
||||
"statsd_address": "drce87cy",
|
||||
"statsite_address": "HpFwKB8R",
|
||||
"disable_compat_1.9": true
|
||||
"statsite_address": "HpFwKB8R"
|
||||
},
|
||||
"tls": {
|
||||
"defaults": {
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/mitchellh/go-testing-interface"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
)
|
||||
|
||||
|
@ -296,6 +297,21 @@ func TestLeafWithNamespace(t testing.T, service, namespace string, root *structs
|
|||
return certPEM, keyPEM
|
||||
}
|
||||
|
||||
func TestMeshGatewayLeaf(t testing.T, partition string, root *structs.CARoot) (string, string) {
|
||||
// Build the SPIFFE ID
|
||||
spiffeId := &SpiffeIDMeshGateway{
|
||||
Host: fmt.Sprintf("%s.consul", TestClusterID),
|
||||
Partition: acl.PartitionOrDefault(partition),
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
|
||||
certPEM, keyPEM, err := testLeafWithID(t, spiffeId, root, DefaultPrivateKeyType, DefaultPrivateKeyBits, 0)
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
return certPEM, keyPEM
|
||||
}
|
||||
|
||||
// TestCSR returns a CSR to sign the given service along with the PEM-encoded
|
||||
// private key for this certificate.
|
||||
func TestCSR(t testing.T, uri CertURI) (string, string) {
|
||||
|
|
|
@ -24,6 +24,8 @@ var (
|
|||
`^(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`)
|
||||
spiffeIDAgentRegexp = regexp.MustCompile(
|
||||
`^(?:/ap/([^/]+))?/agent/client/dc/([^/]+)/id/([^/]+)$`)
|
||||
spiffeIDMeshGatewayRegexp = regexp.MustCompile(
|
||||
`^(?:/ap/([^/]+))?/gateway/mesh/dc/([^/]+)$`)
|
||||
)
|
||||
|
||||
// ParseCertURIFromString attempts to parse a string representation of a
|
||||
|
@ -117,6 +119,31 @@ func ParseCertURI(input *url.URL) (CertURI, error) {
|
|||
Datacenter: dc,
|
||||
Agent: agent,
|
||||
}, nil
|
||||
} else if v := spiffeIDMeshGatewayRegexp.FindStringSubmatch(path); v != nil {
|
||||
// Determine the values. We assume they're reasonable to save cycles,
|
||||
// but if the raw path is not empty that means that something is
|
||||
// URL encoded so we go to the slow path.
|
||||
ap := v[1]
|
||||
dc := v[2]
|
||||
if input.RawPath != "" {
|
||||
var err error
|
||||
if ap, err = url.PathUnescape(v[1]); err != nil {
|
||||
return nil, fmt.Errorf("Invalid admin partition: %s", err)
|
||||
}
|
||||
if dc, err = url.PathUnescape(v[2]); err != nil {
|
||||
return nil, fmt.Errorf("Invalid datacenter: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ap == "" {
|
||||
ap = "default"
|
||||
}
|
||||
|
||||
return &SpiffeIDMeshGateway{
|
||||
Host: input.Host,
|
||||
Partition: ap,
|
||||
Datacenter: dc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Test for signing ID
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package connect
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
)
|
||||
|
||||
type SpiffeIDMeshGateway struct {
|
||||
Host string
|
||||
Partition string
|
||||
Datacenter string
|
||||
}
|
||||
|
||||
func (id SpiffeIDMeshGateway) MatchesPartition(partition string) bool {
|
||||
return id.PartitionOrDefault() == acl.PartitionOrDefault(partition)
|
||||
}
|
||||
|
||||
func (id SpiffeIDMeshGateway) PartitionOrDefault() string {
|
||||
return acl.PartitionOrDefault(id.Partition)
|
||||
}
|
||||
|
||||
// URI returns the *url.URL for this SPIFFE ID.
|
||||
func (id SpiffeIDMeshGateway) URI() *url.URL {
|
||||
var result url.URL
|
||||
result.Scheme = "spiffe"
|
||||
result.Host = id.Host
|
||||
result.Path = id.uriPath()
|
||||
return &result
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//go:build !consulent
|
||||
// +build !consulent
|
||||
|
||||
package connect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
)
|
||||
|
||||
// GetEnterpriseMeta will synthesize an EnterpriseMeta struct from the SpiffeIDAgent.
|
||||
// in OSS this just returns an empty (but never nil) struct pointer
|
||||
func (id SpiffeIDMeshGateway) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
||||
return &acl.EnterpriseMeta{}
|
||||
}
|
||||
|
||||
func (id SpiffeIDMeshGateway) uriPath() string {
|
||||
return fmt.Sprintf("/gateway/mesh/dc/%s", id.Datacenter)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
//go:build !consulent
|
||||
// +build !consulent
|
||||
|
||||
package connect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSpiffeIDMeshGatewayURI(t *testing.T) {
|
||||
t.Run("default partition", func(t *testing.T) {
|
||||
mgw := &SpiffeIDMeshGateway{
|
||||
Host: "1234.consul",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
|
||||
require.Equal(t, "spiffe://1234.consul/gateway/mesh/dc/dc1", mgw.URI().String())
|
||||
})
|
||||
|
||||
t.Run("partitions are ignored", func(t *testing.T) {
|
||||
mgw := &SpiffeIDMeshGateway{
|
||||
Host: "1234.consul",
|
||||
Partition: "foobar",
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
|
||||
require.Equal(t, "spiffe://1234.consul/gateway/mesh/dc/dc1", mgw.URI().String())
|
||||
})
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package connect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
|
@ -23,10 +24,6 @@ func (id SpiffeIDService) MatchesPartition(partition string) bool {
|
|||
return id.PartitionOrDefault() == acl.PartitionOrDefault(partition)
|
||||
}
|
||||
|
||||
func (id SpiffeIDService) PartitionOrDefault() string {
|
||||
return acl.PartitionOrDefault(id.Partition)
|
||||
}
|
||||
|
||||
// URI returns the *url.URL for this SPIFFE ID.
|
||||
func (id SpiffeIDService) URI() *url.URL {
|
||||
var result url.URL
|
||||
|
@ -35,3 +32,20 @@ func (id SpiffeIDService) URI() *url.URL {
|
|||
result.Path = id.uriPath()
|
||||
return &result
|
||||
}
|
||||
|
||||
func (id SpiffeIDService) uriPath() string {
|
||||
path := fmt.Sprintf("/ns/%s/dc/%s/svc/%s",
|
||||
id.NamespaceOrDefault(),
|
||||
id.Datacenter,
|
||||
id.Service,
|
||||
)
|
||||
|
||||
// Although OSS has no support for partitions, it still needs to be able to
|
||||
// handle exportedPartition from peered Consul Enterprise clusters in order
|
||||
// to generate the correct SpiffeID.
|
||||
// We intentionally avoid using pbpartition.DefaultName here to be OSS friendly.
|
||||
if ap := id.PartitionOrDefault(); ap != "" && ap != "default" {
|
||||
return "/ap/" + ap + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
package connect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
)
|
||||
|
@ -15,10 +15,14 @@ func (id SpiffeIDService) GetEnterpriseMeta() *acl.EnterpriseMeta {
|
|||
return &acl.EnterpriseMeta{}
|
||||
}
|
||||
|
||||
func (id SpiffeIDService) uriPath() string {
|
||||
return fmt.Sprintf("/ns/%s/dc/%s/svc/%s",
|
||||
id.NamespaceOrDefault(),
|
||||
id.Datacenter,
|
||||
id.Service,
|
||||
)
|
||||
// PartitionOrDefault breaks from OSS's pattern of returning empty strings.
|
||||
// Although OSS has no support for partitions, it still needs to be able to
|
||||
// handle exportedPartition from peered Consul Enterprise clusters in order
|
||||
// to generate the correct SpiffeID.
|
||||
func (id SpiffeIDService) PartitionOrDefault() string {
|
||||
if id.Partition == "" {
|
||||
return "default"
|
||||
}
|
||||
|
||||
return strings.ToLower(id.Partition)
|
||||
}
|
||||
|
|
|
@ -19,16 +19,6 @@ func TestSpiffeIDServiceURI(t *testing.T) {
|
|||
require.Equal(t, "spiffe://1234.consul/ns/default/dc/dc1/svc/web", svc.URI().String())
|
||||
})
|
||||
|
||||
t.Run("partitions are ignored", func(t *testing.T) {
|
||||
svc := &SpiffeIDService{
|
||||
Host: "1234.consul",
|
||||
Partition: "other",
|
||||
Datacenter: "dc1",
|
||||
Service: "web",
|
||||
}
|
||||
require.Equal(t, "spiffe://1234.consul/ns/default/dc/dc1/svc/web", svc.URI().String())
|
||||
})
|
||||
|
||||
t.Run("namespaces are ignored", func(t *testing.T) {
|
||||
svc := &SpiffeIDService{
|
||||
Host: "1234.consul",
|
||||
|
|
|
@ -48,6 +48,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 *SpiffeIDMeshGateway:
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -95,6 +95,30 @@ func TestSpiffeIDSigning_CanSign(t *testing.T) {
|
|||
input: &SpiffeIDService{Host: TestClusterID + ".fake", Namespace: "default", Datacenter: "dc1", Service: "web"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "mesh gateway - good",
|
||||
id: testSigning,
|
||||
input: &SpiffeIDMeshGateway{Host: TestClusterID + ".consul", Datacenter: "dc1"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mesh gateway - good midex case",
|
||||
id: testSigning,
|
||||
input: &SpiffeIDMeshGateway{Host: strings.ToUpper(TestClusterID) + ".CONsuL", Datacenter: "dc1"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mesh gateway - different cluster",
|
||||
id: testSigning,
|
||||
input: &SpiffeIDMeshGateway{Host: "55555555-4444-3333-2222-111111111111.consul", Datacenter: "dc1"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "mesh gateway - different TLD",
|
||||
id: testSigning,
|
||||
input: &SpiffeIDMeshGateway{Host: TestClusterID + ".fake", Datacenter: "dc1"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -70,6 +70,26 @@ func TestParseCertURIFromString(t *testing.T) {
|
|||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"mesh-gateway with no partition",
|
||||
"spiffe://1234.consul/gateway/mesh/dc/dc1",
|
||||
&SpiffeIDMeshGateway{
|
||||
Host: "1234.consul",
|
||||
Partition: "default",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"mesh-gateway with partition",
|
||||
"spiffe://1234.consul/ap/bizdev/gateway/mesh/dc/dc1",
|
||||
&SpiffeIDMeshGateway{
|
||||
Host: "1234.consul",
|
||||
Partition: "bizdev",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"service with URL-encoded values",
|
||||
"spiffe://1234.consul/ns/foo%2Fbar/dc/bar%2Fbaz/svc/baz%2Fqux",
|
||||
|
|
|
@ -13,7 +13,9 @@ import (
|
|||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
)
|
||||
|
@ -42,10 +44,6 @@ const (
|
|||
// provided.
|
||||
anonymousToken = "anonymous"
|
||||
|
||||
// redactedToken is shown in structures with embedded tokens when they
|
||||
// are not allowed to be displayed.
|
||||
redactedToken = "<hidden>"
|
||||
|
||||
// aclTokenReapingRateLimit is the number of batch token reaping requests per second allowed.
|
||||
aclTokenReapingRateLimit rate.Limit = 1.0
|
||||
|
||||
|
@ -662,26 +660,6 @@ func (r *ACLResolver) synthesizePoliciesForNodeIdentities(nodeIdentities []*stru
|
|||
return syntheticPolicies
|
||||
}
|
||||
|
||||
// plainACLResolver wraps ACLResolver so that it can be used in other packages
|
||||
// that cannot import agent/consul wholesale (e.g. because of import cycles).
|
||||
//
|
||||
// TODO(agentless): this pattern was copied from subscribeBackend for expediency
|
||||
// but we should really refactor ACLResolver so it can be passed as a dependency
|
||||
// to other packages.
|
||||
type plainACLResolver struct {
|
||||
resolver *ACLResolver
|
||||
}
|
||||
|
||||
func (r plainACLResolver) ResolveTokenAndDefaultMeta(
|
||||
token string,
|
||||
entMeta *acl.EnterpriseMeta,
|
||||
authzContext *acl.AuthorizerContext,
|
||||
) (acl.Authorizer, error) {
|
||||
// ACLResolver.ResolveTokenAndDefaultMeta returns a ACLResolveResult which
|
||||
// can't be used in other packages, but it embeds acl.Authorizer which can.
|
||||
return r.resolver.ResolveTokenAndDefaultMeta(token, entMeta, authzContext)
|
||||
}
|
||||
|
||||
func mergeStringSlice(a, b []string) []string {
|
||||
out := make([]string, 0, len(a)+len(b))
|
||||
out = append(out, a...)
|
||||
|
@ -1008,13 +986,13 @@ func (r *ACLResolver) resolveLocallyManagedToken(token string) (structs.ACLIdent
|
|||
// ResolveToken to an acl.Authorizer and structs.ACLIdentity. The acl.Authorizer
|
||||
// can be used to check permissions granted to the token, and the ACLIdentity
|
||||
// describes the token and any defaults applied to it.
|
||||
func (r *ACLResolver) ResolveToken(token string) (ACLResolveResult, error) {
|
||||
func (r *ACLResolver) ResolveToken(token string) (resolver.Result, error) {
|
||||
if !r.ACLsEnabled() {
|
||||
return ACLResolveResult{Authorizer: acl.ManageAll()}, nil
|
||||
return resolver.Result{Authorizer: acl.ManageAll()}, nil
|
||||
}
|
||||
|
||||
if acl.RootAuthorizer(token) != nil {
|
||||
return ACLResolveResult{}, acl.ErrRootDenied
|
||||
return resolver.Result{}, acl.ErrRootDenied
|
||||
}
|
||||
|
||||
// handle the anonymous token
|
||||
|
@ -1023,7 +1001,7 @@ func (r *ACLResolver) ResolveToken(token string) (ACLResolveResult, error) {
|
|||
}
|
||||
|
||||
if ident, authz, ok := r.resolveLocallyManagedToken(token); ok {
|
||||
return ACLResolveResult{Authorizer: authz, ACLIdentity: ident}, nil
|
||||
return resolver.Result{Authorizer: authz, ACLIdentity: ident}, nil
|
||||
}
|
||||
|
||||
defer metrics.MeasureSince([]string{"acl", "ResolveToken"}, time.Now())
|
||||
|
@ -1034,10 +1012,10 @@ func (r *ACLResolver) ResolveToken(token string) (ACLResolveResult, error) {
|
|||
if IsACLRemoteError(err) {
|
||||
r.logger.Error("Error resolving token", "error", err)
|
||||
ident := &missingIdentity{reason: "primary-dc-down", token: token}
|
||||
return ACLResolveResult{Authorizer: r.down, ACLIdentity: ident}, nil
|
||||
return resolver.Result{Authorizer: r.down, ACLIdentity: ident}, nil
|
||||
}
|
||||
|
||||
return ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
}
|
||||
|
||||
// Build the Authorizer
|
||||
|
@ -1050,7 +1028,7 @@ func (r *ACLResolver) ResolveToken(token string) (ACLResolveResult, error) {
|
|||
|
||||
authz, err := policies.Compile(r.cache, &conf)
|
||||
if err != nil {
|
||||
return ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
}
|
||||
chain = append(chain, authz)
|
||||
|
||||
|
@ -1058,36 +1036,15 @@ func (r *ACLResolver) ResolveToken(token string) (ACLResolveResult, error) {
|
|||
if err != nil {
|
||||
if IsACLRemoteError(err) {
|
||||
r.logger.Error("Error resolving identity defaults", "error", err)
|
||||
return ACLResolveResult{Authorizer: r.down, ACLIdentity: identity}, nil
|
||||
return resolver.Result{Authorizer: r.down, ACLIdentity: identity}, nil
|
||||
}
|
||||
return ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
} else if authz != nil {
|
||||
chain = append(chain, authz)
|
||||
}
|
||||
|
||||
chain = append(chain, acl.RootAuthorizer(r.config.ACLDefaultPolicy))
|
||||
return ACLResolveResult{Authorizer: acl.NewChainedAuthorizer(chain), ACLIdentity: identity}, nil
|
||||
}
|
||||
|
||||
type ACLResolveResult struct {
|
||||
acl.Authorizer
|
||||
// TODO: likely we can reduce this interface
|
||||
ACLIdentity structs.ACLIdentity
|
||||
}
|
||||
|
||||
func (a ACLResolveResult) AccessorID() string {
|
||||
if a.ACLIdentity == nil {
|
||||
return ""
|
||||
}
|
||||
return a.ACLIdentity.ID()
|
||||
}
|
||||
|
||||
func (a ACLResolveResult) Identity() structs.ACLIdentity {
|
||||
return a.ACLIdentity
|
||||
}
|
||||
|
||||
func (a ACLResolveResult) ToAllowAuthorizer() acl.AllowAuthorizer {
|
||||
return acl.AllowAuthorizer{Authorizer: a, AccessorID: a.AccessorID()}
|
||||
return resolver.Result{Authorizer: acl.NewChainedAuthorizer(chain), ACLIdentity: identity}, nil
|
||||
}
|
||||
|
||||
func (r *ACLResolver) ACLsEnabled() bool {
|
||||
|
@ -1111,7 +1068,7 @@ func (r *ACLResolver) ResolveTokenAndDefaultMeta(
|
|||
token string,
|
||||
entMeta *acl.EnterpriseMeta,
|
||||
authzContext *acl.AuthorizerContext,
|
||||
) (ACLResolveResult, error) {
|
||||
) (resolver.Result, error) {
|
||||
return r.ResolveTokenAndDefaultMetaWithPeerName(token, entMeta, structs.DefaultPeerKeyword, authzContext)
|
||||
}
|
||||
|
||||
|
@ -1120,10 +1077,10 @@ func (r *ACLResolver) ResolveTokenAndDefaultMetaWithPeerName(
|
|||
entMeta *acl.EnterpriseMeta,
|
||||
peerName string,
|
||||
authzContext *acl.AuthorizerContext,
|
||||
) (ACLResolveResult, error) {
|
||||
) (resolver.Result, error) {
|
||||
result, err := r.ResolveToken(token)
|
||||
if err != nil {
|
||||
return ACLResolveResult{}, err
|
||||
return resolver.Result{}, err
|
||||
}
|
||||
|
||||
if entMeta == nil {
|
||||
|
@ -1154,816 +1111,8 @@ func (r *ACLResolver) ResolveTokenAndDefaultMetaWithPeerName(
|
|||
return result, err
|
||||
}
|
||||
|
||||
// aclFilter is used to filter results from our state store based on ACL rules
|
||||
// configured for the provided token.
|
||||
type aclFilter struct {
|
||||
authorizer acl.Authorizer
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
// newACLFilter constructs a new aclFilter.
|
||||
func newACLFilter(authorizer acl.Authorizer, logger hclog.Logger) *aclFilter {
|
||||
if logger == nil {
|
||||
logger = hclog.New(&hclog.LoggerOptions{})
|
||||
}
|
||||
return &aclFilter{
|
||||
authorizer: authorizer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// allowNode is used to determine if a node is accessible for an ACL.
|
||||
func (f *aclFilter) allowNode(node string, ent *acl.AuthorizerContext) bool {
|
||||
return f.authorizer.NodeRead(node, ent) == acl.Allow
|
||||
}
|
||||
|
||||
// allowNode is used to determine if the gateway and service are accessible for an ACL
|
||||
func (f *aclFilter) allowGateway(gs *structs.GatewayService) bool {
|
||||
var authzContext acl.AuthorizerContext
|
||||
|
||||
// Need read on service and gateway. Gateway may have different EnterpriseMeta so we fill authzContext twice
|
||||
gs.Gateway.FillAuthzContext(&authzContext)
|
||||
if !f.allowService(gs.Gateway.Name, &authzContext) {
|
||||
return false
|
||||
}
|
||||
|
||||
gs.Service.FillAuthzContext(&authzContext)
|
||||
if !f.allowService(gs.Service.Name, &authzContext) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// allowService is used to determine if a service is accessible for an ACL.
|
||||
func (f *aclFilter) allowService(service string, ent *acl.AuthorizerContext) bool {
|
||||
if service == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return f.authorizer.ServiceRead(service, ent) == acl.Allow
|
||||
}
|
||||
|
||||
// allowSession is used to determine if a session for a node is accessible for
|
||||
// an ACL.
|
||||
func (f *aclFilter) allowSession(node string, ent *acl.AuthorizerContext) bool {
|
||||
return f.authorizer.SessionRead(node, ent) == acl.Allow
|
||||
}
|
||||
|
||||
// filterHealthChecks is used to filter a set of health checks down based on
|
||||
// the configured ACL rules for a token. Returns true if any elements were
|
||||
// removed.
|
||||
func (f *aclFilter) filterHealthChecks(checks *structs.HealthChecks) bool {
|
||||
hc := *checks
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(hc); i++ {
|
||||
check := hc[i]
|
||||
check.FillAuthzContext(&authzContext)
|
||||
if f.allowNode(check.Node, &authzContext) && f.allowService(check.ServiceName, &authzContext) {
|
||||
continue
|
||||
}
|
||||
|
||||
f.logger.Debug("dropping check from result due to ACLs", "check", check.CheckID)
|
||||
removed = true
|
||||
hc = append(hc[:i], hc[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*checks = hc
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterServices is used to filter a set of services based on ACLs. Returns
|
||||
// true if any elements were removed.
|
||||
func (f *aclFilter) filterServices(services structs.Services, entMeta *acl.EnterpriseMeta) bool {
|
||||
var authzContext acl.AuthorizerContext
|
||||
entMeta.FillAuthzContext(&authzContext)
|
||||
|
||||
var removed bool
|
||||
|
||||
for svc := range services {
|
||||
if f.allowService(svc, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
||||
removed = true
|
||||
delete(services, svc)
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterServiceNodes is used to filter a set of nodes for a given service
|
||||
// based on the configured ACL rules. Returns true if any elements were removed.
|
||||
func (f *aclFilter) filterServiceNodes(nodes *structs.ServiceNodes) bool {
|
||||
sn := *nodes
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(sn); i++ {
|
||||
node := sn[i]
|
||||
|
||||
node.FillAuthzContext(&authzContext)
|
||||
if f.allowNode(node.Node, &authzContext) && f.allowService(node.ServiceName, &authzContext) {
|
||||
continue
|
||||
}
|
||||
removed = true
|
||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node, &node.EnterpriseMeta))
|
||||
sn = append(sn[:i], sn[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*nodes = sn
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterNodeServices is used to filter services on a given node base on ACLs.
|
||||
// Returns true if any elements were removed
|
||||
func (f *aclFilter) filterNodeServices(services **structs.NodeServices) bool {
|
||||
if *services == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var authzContext acl.AuthorizerContext
|
||||
(*services).Node.FillAuthzContext(&authzContext)
|
||||
if !f.allowNode((*services).Node.Node, &authzContext) {
|
||||
*services = nil
|
||||
return true
|
||||
}
|
||||
|
||||
var removed bool
|
||||
for svcName, svc := range (*services).Services {
|
||||
svc.FillAuthzContext(&authzContext)
|
||||
|
||||
if f.allowNode((*services).Node.Node, &authzContext) && f.allowService(svcName, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
||||
removed = true
|
||||
delete((*services).Services, svcName)
|
||||
}
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterNodeServices is used to filter services on a given node base on ACLs.
|
||||
// Returns true if any elements were removed.
|
||||
func (f *aclFilter) filterNodeServiceList(services *structs.NodeServiceList) bool {
|
||||
if services.Node == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var authzContext acl.AuthorizerContext
|
||||
services.Node.FillAuthzContext(&authzContext)
|
||||
if !f.allowNode(services.Node.Node, &authzContext) {
|
||||
*services = structs.NodeServiceList{}
|
||||
return true
|
||||
}
|
||||
|
||||
var removed bool
|
||||
svcs := services.Services
|
||||
for i := 0; i < len(svcs); i++ {
|
||||
svc := svcs[i]
|
||||
svc.FillAuthzContext(&authzContext)
|
||||
|
||||
if f.allowService(svc.Service, &authzContext) {
|
||||
continue
|
||||
}
|
||||
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc.CompoundServiceID())
|
||||
svcs = append(svcs[:i], svcs[i+1:]...)
|
||||
i--
|
||||
removed = true
|
||||
}
|
||||
services.Services = svcs
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterCheckServiceNodes is used to filter nodes based on ACL rules. Returns
|
||||
// true if any elements were removed.
|
||||
func (f *aclFilter) filterCheckServiceNodes(nodes *structs.CheckServiceNodes) bool {
|
||||
csn := *nodes
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(csn); i++ {
|
||||
node := csn[i]
|
||||
node.Service.FillAuthzContext(&authzContext)
|
||||
if f.allowNode(node.Node.Node, &authzContext) && f.allowService(node.Service.Service, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node.Node.Node, node.Node.GetEnterpriseMeta()))
|
||||
removed = true
|
||||
csn = append(csn[:i], csn[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*nodes = csn
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterServiceTopology is used to filter upstreams/downstreams based on ACL rules.
|
||||
// this filter is unlike others in that it also returns whether the result was filtered by ACLs
|
||||
func (f *aclFilter) filterServiceTopology(topology *structs.ServiceTopology) bool {
|
||||
filteredUpstreams := f.filterCheckServiceNodes(&topology.Upstreams)
|
||||
filteredDownstreams := f.filterCheckServiceNodes(&topology.Downstreams)
|
||||
return filteredUpstreams || filteredDownstreams
|
||||
}
|
||||
|
||||
// filterDatacenterCheckServiceNodes is used to filter nodes based on ACL rules.
|
||||
// Returns true if any elements are removed.
|
||||
func (f *aclFilter) filterDatacenterCheckServiceNodes(datacenterNodes *map[string]structs.CheckServiceNodes) bool {
|
||||
dn := *datacenterNodes
|
||||
out := make(map[string]structs.CheckServiceNodes)
|
||||
var removed bool
|
||||
for dc := range dn {
|
||||
nodes := dn[dc]
|
||||
if f.filterCheckServiceNodes(&nodes) {
|
||||
removed = true
|
||||
}
|
||||
if len(nodes) > 0 {
|
||||
out[dc] = nodes
|
||||
}
|
||||
}
|
||||
*datacenterNodes = out
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterSessions is used to filter a set of sessions based on ACLs. Returns
|
||||
// true if any elements were removed.
|
||||
func (f *aclFilter) filterSessions(sessions *structs.Sessions) bool {
|
||||
s := *sessions
|
||||
|
||||
var removed bool
|
||||
for i := 0; i < len(s); i++ {
|
||||
session := s[i]
|
||||
|
||||
var entCtx acl.AuthorizerContext
|
||||
session.FillAuthzContext(&entCtx)
|
||||
|
||||
if f.allowSession(session.Node, &entCtx) {
|
||||
continue
|
||||
}
|
||||
removed = true
|
||||
f.logger.Debug("dropping session from result due to ACLs", "session", session.ID)
|
||||
s = append(s[:i], s[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*sessions = s
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterCoordinates is used to filter nodes in a coordinate dump based on ACL
|
||||
// rules. Returns true if any elements were removed.
|
||||
func (f *aclFilter) filterCoordinates(coords *structs.Coordinates) bool {
|
||||
c := *coords
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(c); i++ {
|
||||
c[i].FillAuthzContext(&authzContext)
|
||||
node := c[i].Node
|
||||
if f.allowNode(node, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, c[i].GetEnterpriseMeta()))
|
||||
removed = true
|
||||
c = append(c[:i], c[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*coords = c
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterIntentions is used to filter intentions based on ACL rules.
|
||||
// We prune entries the user doesn't have access to, and we redact any tokens
|
||||
// if the user doesn't have a management token. Returns true if any elements
|
||||
// were removed.
|
||||
func (f *aclFilter) filterIntentions(ixns *structs.Intentions) bool {
|
||||
ret := make(structs.Intentions, 0, len(*ixns))
|
||||
var removed bool
|
||||
for _, ixn := range *ixns {
|
||||
if !ixn.CanRead(f.authorizer) {
|
||||
removed = true
|
||||
f.logger.Debug("dropping intention from result due to ACLs", "intention", ixn.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, ixn)
|
||||
}
|
||||
|
||||
*ixns = ret
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterNodeDump is used to filter through all parts of a node dump and
|
||||
// remove elements the provided ACL token cannot access. Returns true if
|
||||
// any elements were removed.
|
||||
func (f *aclFilter) filterNodeDump(dump *structs.NodeDump) bool {
|
||||
nd := *dump
|
||||
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
for i := 0; i < len(nd); i++ {
|
||||
info := nd[i]
|
||||
|
||||
// Filter nodes
|
||||
info.FillAuthzContext(&authzContext)
|
||||
if node := info.Node; !f.allowNode(node, &authzContext) {
|
||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, info.GetEnterpriseMeta()))
|
||||
removed = true
|
||||
nd = append(nd[:i], nd[i+1:]...)
|
||||
i--
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter services
|
||||
for j := 0; j < len(info.Services); j++ {
|
||||
svc := info.Services[j].Service
|
||||
info.Services[j].FillAuthzContext(&authzContext)
|
||||
if f.allowNode(info.Node, &authzContext) && f.allowService(svc, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", svc)
|
||||
removed = true
|
||||
info.Services = append(info.Services[:j], info.Services[j+1:]...)
|
||||
j--
|
||||
}
|
||||
|
||||
// Filter checks
|
||||
for j := 0; j < len(info.Checks); j++ {
|
||||
chk := info.Checks[j]
|
||||
chk.FillAuthzContext(&authzContext)
|
||||
if f.allowNode(info.Node, &authzContext) && f.allowService(chk.ServiceName, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping check from result due to ACLs", "check", chk.CheckID)
|
||||
removed = true
|
||||
info.Checks = append(info.Checks[:j], info.Checks[j+1:]...)
|
||||
j--
|
||||
}
|
||||
}
|
||||
*dump = nd
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterServiceDump is used to filter nodes based on ACL rules. Returns true
|
||||
// if any elements were removed.
|
||||
func (f *aclFilter) filterServiceDump(services *structs.ServiceDump) bool {
|
||||
svcs := *services
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(svcs); i++ {
|
||||
service := svcs[i]
|
||||
|
||||
if f.allowGateway(service.GatewayService) {
|
||||
// ServiceDump might only have gateway config and no node information
|
||||
if service.Node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
service.Service.FillAuthzContext(&authzContext)
|
||||
if f.allowNode(service.Node.Node, &authzContext) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", service.GatewayService.Service)
|
||||
removed = true
|
||||
svcs = append(svcs[:i], svcs[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*services = svcs
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterNodes is used to filter through all parts of a node list and remove
|
||||
// elements the provided ACL token cannot access. Returns true if any elements
|
||||
// were removed.
|
||||
func (f *aclFilter) filterNodes(nodes *structs.Nodes) bool {
|
||||
n := *nodes
|
||||
|
||||
var authzContext acl.AuthorizerContext
|
||||
var removed bool
|
||||
|
||||
for i := 0; i < len(n); i++ {
|
||||
n[i].FillAuthzContext(&authzContext)
|
||||
node := n[i].Node
|
||||
if f.allowNode(node, &authzContext) {
|
||||
continue
|
||||
}
|
||||
f.logger.Debug("dropping node from result due to ACLs", "node", structs.NodeNameString(node, n[i].GetEnterpriseMeta()))
|
||||
removed = true
|
||||
n = append(n[:i], n[i+1:]...)
|
||||
i--
|
||||
}
|
||||
*nodes = n
|
||||
return removed
|
||||
}
|
||||
|
||||
// redactPreparedQueryTokens will redact any tokens unless the client has a
|
||||
// management token. This eases the transition to delegated authority over
|
||||
// prepared queries, since it was easy to capture management tokens in Consul
|
||||
// 0.6.3 and earlier, and we don't want to willy-nilly show those. This does
|
||||
// have the limitation of preventing delegated non-management users from seeing
|
||||
// captured tokens, but they can at least see whether or not a token is set.
|
||||
func (f *aclFilter) redactPreparedQueryTokens(query **structs.PreparedQuery) {
|
||||
// Management tokens can see everything with no filtering.
|
||||
var authzContext acl.AuthorizerContext
|
||||
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
||||
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
||||
return
|
||||
}
|
||||
|
||||
// Let the user see if there's a blank token, otherwise we need
|
||||
// to redact it, since we know they don't have a management
|
||||
// token.
|
||||
if (*query).Token != "" {
|
||||
// Redact the token, using a copy of the query structure
|
||||
// since we could be pointed at a live instance from the
|
||||
// state store so it's not safe to modify it. Note that
|
||||
// this clone will still point to things like underlying
|
||||
// arrays in the original, but for modifying just the
|
||||
// token it will be safe to use.
|
||||
clone := *(*query)
|
||||
clone.Token = redactedToken
|
||||
*query = &clone
|
||||
}
|
||||
}
|
||||
|
||||
// filterPreparedQueries is used to filter prepared queries based on ACL rules.
|
||||
// We prune entries the user doesn't have access to, and we redact any tokens
|
||||
// if the user doesn't have a management token. Returns true if any (named)
|
||||
// queries were removed - un-named queries are meant to be ephemeral and can
|
||||
// only be enumerated by a management token
|
||||
func (f *aclFilter) filterPreparedQueries(queries *structs.PreparedQueries) bool {
|
||||
var authzContext acl.AuthorizerContext
|
||||
structs.DefaultEnterpriseMetaInDefaultPartition().FillAuthzContext(&authzContext)
|
||||
// Management tokens can see everything with no filtering.
|
||||
// TODO is this check even necessary - this looks like a search replace from
|
||||
// the 1.4 ACL rewrite. The global-management token will provide unrestricted query privileges
|
||||
// so asking for ACLWrite should be unnecessary.
|
||||
if f.authorizer.ACLWrite(&authzContext) == acl.Allow {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise, we need to see what the token has access to.
|
||||
var namedQueriesRemoved bool
|
||||
ret := make(structs.PreparedQueries, 0, len(*queries))
|
||||
for _, query := range *queries {
|
||||
// If no prefix ACL applies to this query then filter it, since
|
||||
// we know at this point the user doesn't have a management
|
||||
// token, otherwise see what the policy says.
|
||||
prefix, hasName := query.GetACLPrefix()
|
||||
switch {
|
||||
case hasName && f.authorizer.PreparedQueryRead(prefix, &authzContext) != acl.Allow:
|
||||
namedQueriesRemoved = true
|
||||
fallthrough
|
||||
case !hasName:
|
||||
f.logger.Debug("dropping prepared query from result due to ACLs", "query", query.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Redact any tokens if necessary. We make a copy of just the
|
||||
// pointer so we don't mess with the caller's slice.
|
||||
final := query
|
||||
f.redactPreparedQueryTokens(&final)
|
||||
ret = append(ret, final)
|
||||
}
|
||||
*queries = ret
|
||||
return namedQueriesRemoved
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterToken(token **structs.ACLToken) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if token == nil || *token == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*token).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
// no permissions to read
|
||||
*token = nil
|
||||
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
||||
// no write permissions - redact secret
|
||||
clone := *(*token)
|
||||
clone.SecretID = redactedToken
|
||||
*token = &clone
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterTokens(tokens *structs.ACLTokens) {
|
||||
ret := make(structs.ACLTokens, 0, len(*tokens))
|
||||
for _, token := range *tokens {
|
||||
final := token
|
||||
f.filterToken(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*tokens = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterTokenStub(token **structs.ACLTokenListStub) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if token == nil || *token == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*token).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
*token = nil
|
||||
} else if f.authorizer.ACLWrite(&entCtx) != acl.Allow {
|
||||
// no write permissions - redact secret
|
||||
clone := *(*token)
|
||||
clone.SecretID = redactedToken
|
||||
*token = &clone
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterTokenStubs(tokens *[]*structs.ACLTokenListStub) {
|
||||
ret := make(structs.ACLTokenListStubs, 0, len(*tokens))
|
||||
for _, token := range *tokens {
|
||||
final := token
|
||||
f.filterTokenStub(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*tokens = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterPolicy(policy **structs.ACLPolicy) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if policy == nil || *policy == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*policy).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
// no permissions to read
|
||||
*policy = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterPolicies(policies *structs.ACLPolicies) {
|
||||
ret := make(structs.ACLPolicies, 0, len(*policies))
|
||||
for _, policy := range *policies {
|
||||
final := policy
|
||||
f.filterPolicy(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*policies = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterRole(role **structs.ACLRole) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if role == nil || *role == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*role).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
// no permissions to read
|
||||
*role = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterRoles(roles *structs.ACLRoles) {
|
||||
ret := make(structs.ACLRoles, 0, len(*roles))
|
||||
for _, role := range *roles {
|
||||
final := role
|
||||
f.filterRole(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*roles = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterBindingRule(rule **structs.ACLBindingRule) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if rule == nil || *rule == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*rule).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
// no permissions to read
|
||||
*rule = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterBindingRules(rules *structs.ACLBindingRules) {
|
||||
ret := make(structs.ACLBindingRules, 0, len(*rules))
|
||||
for _, rule := range *rules {
|
||||
final := rule
|
||||
f.filterBindingRule(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*rules = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterAuthMethod(method **structs.ACLAuthMethod) {
|
||||
var entCtx acl.AuthorizerContext
|
||||
if method == nil || *method == nil || f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
(*method).FillAuthzContext(&entCtx)
|
||||
|
||||
if f.authorizer.ACLRead(&entCtx) != acl.Allow {
|
||||
// no permissions to read
|
||||
*method = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterAuthMethods(methods *structs.ACLAuthMethods) {
|
||||
ret := make(structs.ACLAuthMethods, 0, len(*methods))
|
||||
for _, method := range *methods {
|
||||
final := method
|
||||
f.filterAuthMethod(&final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
}
|
||||
}
|
||||
*methods = ret
|
||||
}
|
||||
|
||||
func (f *aclFilter) filterServiceList(services *structs.ServiceList) bool {
|
||||
ret := make(structs.ServiceList, 0, len(*services))
|
||||
var removed bool
|
||||
for _, svc := range *services {
|
||||
var authzContext acl.AuthorizerContext
|
||||
|
||||
svc.FillAuthzContext(&authzContext)
|
||||
|
||||
if f.authorizer.ServiceRead(svc.Name, &authzContext) != acl.Allow {
|
||||
removed = true
|
||||
sid := structs.NewServiceID(svc.Name, &svc.EnterpriseMeta)
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", sid.String())
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, svc)
|
||||
}
|
||||
|
||||
*services = ret
|
||||
return removed
|
||||
}
|
||||
|
||||
// filterGatewayServices is used to filter gateway to service mappings based on ACL rules.
|
||||
// Returns true if any elements were removed.
|
||||
func (f *aclFilter) filterGatewayServices(mappings *structs.GatewayServices) bool {
|
||||
ret := make(structs.GatewayServices, 0, len(*mappings))
|
||||
var removed bool
|
||||
for _, s := range *mappings {
|
||||
// This filter only checks ServiceRead on the linked service.
|
||||
// ServiceRead on the gateway is checked in the GatewayServices endpoint before filtering.
|
||||
var authzContext acl.AuthorizerContext
|
||||
s.Service.FillAuthzContext(&authzContext)
|
||||
|
||||
if f.authorizer.ServiceRead(s.Service.Name, &authzContext) != acl.Allow {
|
||||
f.logger.Debug("dropping service from result due to ACLs", "service", s.Service.String())
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
ret = append(ret, s)
|
||||
}
|
||||
*mappings = ret
|
||||
return removed
|
||||
}
|
||||
|
||||
func filterACLWithAuthorizer(logger hclog.Logger, authorizer acl.Authorizer, subj interface{}) {
|
||||
if authorizer == nil {
|
||||
return
|
||||
}
|
||||
filt := newACLFilter(authorizer, logger)
|
||||
|
||||
switch v := subj.(type) {
|
||||
case *structs.CheckServiceNodes:
|
||||
filt.filterCheckServiceNodes(v)
|
||||
|
||||
case *structs.IndexedCheckServiceNodes:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
|
||||
|
||||
case *structs.PreparedQueryExecuteResponse:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCheckServiceNodes(&v.Nodes)
|
||||
|
||||
case *structs.IndexedServiceTopology:
|
||||
filtered := filt.filterServiceTopology(v.ServiceTopology)
|
||||
if filtered {
|
||||
v.FilteredByACLs = true
|
||||
v.QueryMeta.ResultsFilteredByACLs = true
|
||||
}
|
||||
|
||||
case *structs.DatacenterIndexedCheckServiceNodes:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterDatacenterCheckServiceNodes(&v.DatacenterNodes)
|
||||
|
||||
case *structs.IndexedCoordinates:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterCoordinates(&v.Coordinates)
|
||||
|
||||
case *structs.IndexedHealthChecks:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterHealthChecks(&v.HealthChecks)
|
||||
|
||||
case *structs.IndexedIntentions:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterIntentions(&v.Intentions)
|
||||
|
||||
case *structs.IndexedNodeDump:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeDump(&v.Dump)
|
||||
|
||||
case *structs.IndexedServiceDump:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceDump(&v.Dump)
|
||||
|
||||
case *structs.IndexedNodes:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodes(&v.Nodes)
|
||||
|
||||
case *structs.IndexedNodeServices:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeServices(&v.NodeServices)
|
||||
|
||||
case *structs.IndexedNodeServiceList:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterNodeServiceList(&v.NodeServices)
|
||||
|
||||
case *structs.IndexedServiceNodes:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceNodes(&v.ServiceNodes)
|
||||
|
||||
case *structs.IndexedServices:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServices(v.Services, &v.EnterpriseMeta)
|
||||
|
||||
case *structs.IndexedSessions:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterSessions(&v.Sessions)
|
||||
|
||||
case *structs.IndexedPreparedQueries:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterPreparedQueries(&v.Queries)
|
||||
|
||||
case **structs.PreparedQuery:
|
||||
filt.redactPreparedQueryTokens(v)
|
||||
|
||||
case *structs.ACLTokens:
|
||||
filt.filterTokens(v)
|
||||
case **structs.ACLToken:
|
||||
filt.filterToken(v)
|
||||
case *[]*structs.ACLTokenListStub:
|
||||
filt.filterTokenStubs(v)
|
||||
case **structs.ACLTokenListStub:
|
||||
filt.filterTokenStub(v)
|
||||
|
||||
case *structs.ACLPolicies:
|
||||
filt.filterPolicies(v)
|
||||
case **structs.ACLPolicy:
|
||||
filt.filterPolicy(v)
|
||||
|
||||
case *structs.ACLRoles:
|
||||
filt.filterRoles(v)
|
||||
case **structs.ACLRole:
|
||||
filt.filterRole(v)
|
||||
|
||||
case *structs.ACLBindingRules:
|
||||
filt.filterBindingRules(v)
|
||||
case **structs.ACLBindingRule:
|
||||
filt.filterBindingRule(v)
|
||||
|
||||
case *structs.ACLAuthMethods:
|
||||
filt.filterAuthMethods(v)
|
||||
case **structs.ACLAuthMethod:
|
||||
filt.filterAuthMethod(v)
|
||||
|
||||
case *structs.IndexedServiceList:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceList(&v.Services)
|
||||
|
||||
case *structs.IndexedExportedServiceList:
|
||||
for peer, peerServices := range v.Services {
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterServiceList(&peerServices)
|
||||
if len(peerServices) == 0 {
|
||||
delete(v.Services, peer)
|
||||
} else {
|
||||
v.Services[peer] = peerServices
|
||||
}
|
||||
}
|
||||
|
||||
case *structs.IndexedGatewayServices:
|
||||
v.QueryMeta.ResultsFilteredByACLs = filt.filterGatewayServices(&v.Services)
|
||||
|
||||
case *structs.IndexedNodesWithGateways:
|
||||
if filt.filterCheckServiceNodes(&v.Nodes) {
|
||||
v.QueryMeta.ResultsFilteredByACLs = true
|
||||
}
|
||||
if filt.filterGatewayServices(&v.Gateways) {
|
||||
v.QueryMeta.ResultsFilteredByACLs = true
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("Unhandled type passed to ACL filter: %T %#v", subj, subj))
|
||||
}
|
||||
aclfilter.New(authorizer, logger).Filter(subj)
|
||||
}
|
||||
|
||||
// filterACL uses the ACLResolver to resolve the token in an acl.Authorizer,
|
||||
|
|
|
@ -17,10 +17,12 @@ import (
|
|||
uuid "github.com/hashicorp/go-uuid"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/consul/auth"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
)
|
||||
|
||||
|
@ -263,7 +265,7 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
|
|||
return err
|
||||
}
|
||||
|
||||
var authz ACLResolveResult
|
||||
var authz resolver.Result
|
||||
|
||||
if args.TokenIDType == structs.ACLTokenAccessor {
|
||||
var err error
|
||||
|
@ -290,7 +292,7 @@ func (a *ACL) TokenRead(args *structs.ACLTokenGetRequest, reply *structs.ACLToke
|
|||
a.srv.filterACLWithAuthorizer(authz, &token)
|
||||
|
||||
// token secret was redacted
|
||||
if token.SecretID == redactedToken {
|
||||
if token.SecretID == aclfilter.RedactedToken {
|
||||
reply.Redacted = true
|
||||
}
|
||||
}
|
||||
|
@ -718,7 +720,7 @@ func (a *ACL) TokenBatchRead(args *structs.ACLTokenBatchGetRequest, reply *struc
|
|||
a.srv.filterACLWithAuthorizer(authz, &final)
|
||||
if final != nil {
|
||||
ret = append(ret, final)
|
||||
if final.SecretID == redactedToken {
|
||||
if final.SecretID == aclfilter.RedactedToken {
|
||||
reply.Redacted = true
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
|
@ -1854,7 +1855,7 @@ func TestACLEndpoint_TokenList(t *testing.T) {
|
|||
}
|
||||
require.ElementsMatch(t, gatherIDs(t, resp.Tokens), tokens)
|
||||
for _, token := range resp.Tokens {
|
||||
require.Equal(t, redactedToken, token.SecretID)
|
||||
require.Equal(t, aclfilter.RedactedToken, token.SecretID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
tokenStore "github.com/hashicorp/consul/agent/token"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
|
@ -752,9 +753,9 @@ func TestACLReplication_TokensRedacted(t *testing.T) {
|
|||
var tokenResp structs.ACLTokenResponse
|
||||
req := structs.ACLTokenGetRequest{
|
||||
Datacenter: "dc2",
|
||||
TokenID: redactedToken,
|
||||
TokenID: aclfilter.RedactedToken,
|
||||
TokenIDType: structs.ACLTokenSecret,
|
||||
QueryOptions: structs.QueryOptions{Token: redactedToken},
|
||||
QueryOptions: structs.QueryOptions{Token: aclfilter.RedactedToken},
|
||||
}
|
||||
err := s2.RPC("ACL.TokenRead", &req, &tokenResp)
|
||||
// its not an error for the secret to not be found.
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
)
|
||||
|
||||
type aclTokenReplicator struct {
|
||||
|
@ -99,7 +100,7 @@ func (r *aclTokenReplicator) PendingUpdateEstimatedSize(i int) int {
|
|||
}
|
||||
|
||||
func (r *aclTokenReplicator) PendingUpdateIsRedacted(i int) bool {
|
||||
return r.updated[i].SecretID == redactedToken
|
||||
return r.updated[i].SecretID == aclfilter.RedactedToken
|
||||
}
|
||||
|
||||
func (r *aclTokenReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -86,7 +86,7 @@ func (s *Server) initAutopilot(config *Config) {
|
|||
)
|
||||
|
||||
// registers a snapshot handler for the event publisher to send as the first event for a new stream
|
||||
s.publisher.RegisterHandler(autopilotevents.EventTopicReadyServers, apDelegate.readyServersPublisher.HandleSnapshot)
|
||||
s.publisher.RegisterHandler(autopilotevents.EventTopicReadyServers, apDelegate.readyServersPublisher.HandleSnapshot, false)
|
||||
}
|
||||
|
||||
func (s *Server) autopilotServers() map[raft.ServerID]*autopilot.Server {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package autopilotevents
|
||||
|
||||
|
@ -19,9 +19,10 @@ func (_m *MockPublisher) Publish(_a0 []stream.Event) {
|
|||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// NewMockPublisher creates a new instance of MockPublisher. It also registers a cleanup function to assert the mocks expectations.
|
||||
// NewMockPublisher creates a new instance of MockPublisher. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewMockPublisher(t testing.TB) *MockPublisher {
|
||||
mock := &MockPublisher{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package autopilotevents
|
||||
|
||||
|
@ -48,9 +48,10 @@ func (_m *MockStateStore) GetNodeID(_a0 types.NodeID, _a1 *acl.EnterpriseMeta, _
|
|||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// NewMockStateStore creates a new instance of MockStateStore. It also registers a cleanup function to assert the mocks expectations.
|
||||
// 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{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by mockery v2.11.0. DO NOT EDIT.
|
||||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package autopilotevents
|
||||
|
||||
|
@ -29,9 +29,10 @@ func (_m *mockTimeProvider) Now() time.Time {
|
|||
return r0
|
||||
}
|
||||
|
||||
// newMockTimeProvider creates a new instance of mockTimeProvider. It also registers a cleanup function to assert the mocks expectations.
|
||||
// newMockTimeProvider creates a new instance of mockTimeProvider. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func newMockTimeProvider(t testing.TB) *mockTimeProvider {
|
||||
mock := &mockTimeProvider{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
|
|
|
@ -119,17 +119,17 @@ func NewReadyServersEventPublisher(config Config) *ReadyServersEventPublisher {
|
|||
}
|
||||
}
|
||||
|
||||
//go:generate mockery --name StateStore --inpackage --testonly
|
||||
//go:generate mockery --name StateStore --inpackage --filename mock_StateStore_test.go
|
||||
type StateStore interface {
|
||||
GetNodeID(types.NodeID, *acl.EnterpriseMeta, string) (uint64, *structs.Node, error)
|
||||
}
|
||||
|
||||
//go:generate mockery --name Publisher --inpackage --testonly
|
||||
//go:generate mockery --name Publisher --inpackage --filename mock_Publisher_test.go
|
||||
type Publisher interface {
|
||||
Publish([]stream.Event)
|
||||
}
|
||||
|
||||
//go:generate mockery --name timeProvider --inpackage --testonly
|
||||
//go:generate mockery --name timeProvider --inpackage --filename mock_timeProvider_test.go
|
||||
type timeProvider interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
|
|
@ -8,13 +8,14 @@ import (
|
|||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-metrics/prometheus"
|
||||
bexpr "github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/ipaddr"
|
||||
|
@ -160,7 +161,7 @@ func nodePreApply(nodeName, nodeID string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func servicePreApply(service *structs.NodeService, authz ACLResolveResult, authzCtxFill func(*acl.AuthorizerContext)) error {
|
||||
func servicePreApply(service *structs.NodeService, authz resolver.Result, authzCtxFill func(*acl.AuthorizerContext)) error {
|
||||
// Validate the service. This is in addition to the below since
|
||||
// the above just hasn't been moved over yet. We should move it over
|
||||
// in time.
|
||||
|
@ -230,7 +231,7 @@ func checkPreApply(check *structs.HealthCheck) {
|
|||
// worst let a service update revert a recent node update, so it doesn't open up
|
||||
// too much abuse).
|
||||
func vetRegisterWithACL(
|
||||
authz ACLResolveResult,
|
||||
authz resolver.Result,
|
||||
subj *structs.RegisterRequest,
|
||||
ns *structs.NodeServices,
|
||||
) error {
|
||||
|
@ -396,7 +397,7 @@ func (c *Catalog) Deregister(args *structs.DeregisterRequest, reply *struct{}) e
|
|||
// endpoint. The NodeService for the referenced service must be supplied, and can
|
||||
// be nil; similar for the HealthCheck for the referenced health check.
|
||||
func vetDeregisterWithACL(
|
||||
authz ACLResolveResult,
|
||||
authz resolver.Result,
|
||||
subj *structs.DeregisterRequest,
|
||||
ns *structs.NodeService,
|
||||
nc *structs.HealthCheck,
|
||||
|
@ -869,6 +870,11 @@ func (c *Catalog) NodeServiceList(args *structs.NodeSpecificRequest, reply *stru
|
|||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
priorMergeHash uint64
|
||||
ranMergeOnce bool
|
||||
)
|
||||
|
||||
return c.srv.blockingQuery(
|
||||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
|
@ -878,10 +884,55 @@ func (c *Catalog) NodeServiceList(args *structs.NodeSpecificRequest, reply *stru
|
|||
return err
|
||||
}
|
||||
|
||||
mergedServices := services
|
||||
var cfgIndex uint64
|
||||
if services != nil && args.MergeCentralConfig {
|
||||
var mergedNodeServices []*structs.NodeService
|
||||
for _, ns := range services.Services {
|
||||
mergedns := ns
|
||||
if ns.IsSidecarProxy() || ns.IsGateway() {
|
||||
serviceSpecificReq := structs.ServiceSpecificRequest{
|
||||
Datacenter: args.Datacenter,
|
||||
QueryOptions: args.QueryOptions,
|
||||
}
|
||||
cfgIndex, mergedns, err = mergeNodeServiceWithCentralConfig(ws, state, &serviceSpecificReq, ns, c.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfgIndex > index {
|
||||
index = cfgIndex
|
||||
}
|
||||
}
|
||||
mergedNodeServices = append(mergedNodeServices, mergedns)
|
||||
}
|
||||
if len(mergedNodeServices) > 0 {
|
||||
mergedServices.Services = mergedNodeServices
|
||||
}
|
||||
|
||||
// Generate a hash of the mergedServices driving this response.
|
||||
// Use it to determine if the response is identical to a prior wakeup.
|
||||
newMergeHash, err := hashstructure_v2.Hash(mergedServices, hashstructure_v2.FormatV2, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error hashing reply for spurious wakeup suppression: %w", err)
|
||||
}
|
||||
if ranMergeOnce && priorMergeHash == newMergeHash {
|
||||
// the below assignment is not required as the if condition already validates equality,
|
||||
// but makes it more clear that prior value is being reset to the new hash on each run.
|
||||
priorMergeHash = newMergeHash
|
||||
reply.Index = index
|
||||
// NOTE: the prior response is still alive inside of *reply, which is desirable
|
||||
return errNotChanged
|
||||
} else {
|
||||
priorMergeHash = newMergeHash
|
||||
ranMergeOnce = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reply.Index = index
|
||||
|
||||
if services != nil {
|
||||
reply.NodeServices = *services
|
||||
if mergedServices != nil {
|
||||
reply.NodeServices = *mergedServices
|
||||
|
||||
raw, err := filter.Execute(reply.NodeServices.Services)
|
||||
if err != nil {
|
||||
|
@ -985,6 +1036,7 @@ func (c *Catalog) VirtualIPForService(args *structs.ServiceSpecificRequest, repl
|
|||
}
|
||||
|
||||
state := c.srv.fsm.State()
|
||||
*reply, err = state.VirtualIPForService(structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta))
|
||||
psn := structs.PeeredServiceName{Peer: args.PeerName, ServiceName: structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta)}
|
||||
*reply, err = state.VirtualIPForService(psn)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/hashicorp/consul-net-rpc/net/rpc"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
|
@ -3467,11 +3468,11 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
}
|
||||
|
||||
// With an "allow all" authorizer the update should be allowed.
|
||||
require.NoError(t, vetRegisterWithACL(ACLResolveResult{Authorizer: acl.ManageAll()}, args, nil))
|
||||
require.NoError(t, vetRegisterWithACL(resolver.Result{Authorizer: acl.ManageAll()}, args, nil))
|
||||
})
|
||||
|
||||
var perms acl.Authorizer = acl.DenyAll()
|
||||
var resolvedPerms ACLResolveResult
|
||||
var resolvedPerms resolver.Result
|
||||
|
||||
args := &structs.RegisterRequest{
|
||||
Node: "nope",
|
||||
|
@ -3483,7 +3484,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
node "node" {
|
||||
policy = "write"
|
||||
} `)
|
||||
resolvedPerms = ACLResolveResult{Authorizer: perms}
|
||||
resolvedPerms = resolver.Result{Authorizer: perms}
|
||||
|
||||
// With that policy, the update should now be blocked for node reasons.
|
||||
err := vetRegisterWithACL(resolvedPerms, args, nil)
|
||||
|
@ -3514,7 +3515,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
ID: "my-id",
|
||||
},
|
||||
}
|
||||
err = vetRegisterWithACL(ACLResolveResult{Authorizer: perms}, args, ns)
|
||||
err = vetRegisterWithACL(resolver.Result{Authorizer: perms}, args, ns)
|
||||
require.True(t, acl.IsErrPermissionDenied(err))
|
||||
|
||||
// Chain on a basic service policy.
|
||||
|
@ -3522,7 +3523,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
service "service" {
|
||||
policy = "write"
|
||||
} `)
|
||||
resolvedPerms = ACLResolveResult{Authorizer: perms}
|
||||
resolvedPerms = resolver.Result{Authorizer: perms}
|
||||
|
||||
// With the service ACL, the update should go through.
|
||||
require.NoError(t, vetRegisterWithACL(resolvedPerms, args, ns))
|
||||
|
@ -3549,7 +3550,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
service "other" {
|
||||
policy = "write"
|
||||
} `)
|
||||
resolvedPerms = ACLResolveResult{Authorizer: perms}
|
||||
resolvedPerms = resolver.Result{Authorizer: perms}
|
||||
|
||||
// Now it should go through.
|
||||
require.NoError(t, vetRegisterWithACL(resolvedPerms, args, ns))
|
||||
|
@ -3655,7 +3656,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
service "other" {
|
||||
policy = "deny"
|
||||
} `)
|
||||
resolvedPerms = ACLResolveResult{Authorizer: perms}
|
||||
resolvedPerms = resolver.Result{Authorizer: perms}
|
||||
|
||||
// This should get rejected.
|
||||
err = vetRegisterWithACL(resolvedPerms, args, ns)
|
||||
|
@ -3682,7 +3683,7 @@ func TestVetRegisterWithACL(t *testing.T) {
|
|||
node "node" {
|
||||
policy = "deny"
|
||||
} `)
|
||||
resolvedPerms = ACLResolveResult{Authorizer: perms}
|
||||
resolvedPerms = resolver.Result{Authorizer: perms}
|
||||
|
||||
// This should get rejected because there's a node-level check in here.
|
||||
err = vetRegisterWithACL(resolvedPerms, args, ns)
|
||||
|
@ -3733,7 +3734,7 @@ func TestVetDeregisterWithACL(t *testing.T) {
|
|||
}
|
||||
|
||||
// With an "allow all" authorizer the update should be allowed.
|
||||
if err := vetDeregisterWithACL(ACLResolveResult{Authorizer: acl.ManageAll()}, args, nil, nil); err != nil {
|
||||
if err := vetDeregisterWithACL(resolver.Result{Authorizer: acl.ManageAll()}, args, nil, nil); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
|
@ -3966,7 +3967,7 @@ node "node" {
|
|||
},
|
||||
} {
|
||||
t.Run(args.Name, func(t *testing.T) {
|
||||
err = vetDeregisterWithACL(ACLResolveResult{Authorizer: args.Perms}, &args.DeregisterRequest, args.Service, args.Check)
|
||||
err = vetDeregisterWithACL(resolver.Result{Authorizer: args.Perms}, &args.DeregisterRequest, args.Service, args.Check)
|
||||
if !args.Expected {
|
||||
if err == nil {
|
||||
t.Errorf("expected error with %+v", args.DeregisterRequest)
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
|
||||
msgpackrpc "github.com/hashicorp/consul-net-rpc/net-rpc-msgpackrpc"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul/stream"
|
||||
grpc "github.com/hashicorp/consul/agent/grpc/private"
|
||||
"github.com/hashicorp/consul/agent/grpc/private/resolver"
|
||||
"github.com/hashicorp/consul/agent/pool"
|
||||
|
@ -510,7 +511,7 @@ func newDefaultDeps(t *testing.T, c *Config) Deps {
|
|||
|
||||
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
|
||||
Name: c.NodeName,
|
||||
Level: hclog.Trace,
|
||||
Level: testutil.TestLogLevel,
|
||||
Output: testutil.NewLogBuffer(t),
|
||||
})
|
||||
|
||||
|
@ -535,6 +536,7 @@ func newDefaultDeps(t *testing.T, c *Config) Deps {
|
|||
}
|
||||
|
||||
return Deps{
|
||||
EventPublisher: stream.NewEventPublisher(10 * time.Second),
|
||||
Logger: logger,
|
||||
TLSConfigurator: tls,
|
||||
Tokens: new(token.Store),
|
||||
|
|
|
@ -1133,6 +1133,31 @@ func TestConfigEntry_ResolveServiceConfig_TransparentProxy(t *testing.T) {
|
|||
TransparentProxy: structs.TransparentProxyConfig{OutboundListenerPort: 808},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "from service-defaults with endpoint",
|
||||
entries: []structs.ConfigEntry{
|
||||
&structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "foo",
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
Destination: &structs.DestinationConfig{
|
||||
Address: "hello.world.com",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
request: structs.ServiceConfigRequest{
|
||||
Name: "foo",
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
expect: structs.ServiceConfigResponse{
|
||||
Mode: structs.ProxyModeTransparent,
|
||||
Destination: structs.DestinationConfig{
|
||||
Address: "hello.world.com",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service-defaults overrides proxy-defaults",
|
||||
entries: []structs.ConfigEntry{
|
||||
|
@ -1207,11 +1232,10 @@ func TestConfigEntry_ResolveServiceConfig_Upstreams(t *testing.T) {
|
|||
wildcard := structs.NewServiceID(structs.WildcardSpecifier, structs.WildcardEnterpriseMetaInDefaultPartition())
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
entries []structs.ConfigEntry
|
||||
request structs.ServiceConfigRequest
|
||||
proxyCfg structs.ConnectProxyConfig
|
||||
expect structs.ServiceConfigResponse
|
||||
name string
|
||||
entries []structs.ConfigEntry
|
||||
request structs.ServiceConfigRequest
|
||||
expect structs.ServiceConfigResponse
|
||||
}{
|
||||
{
|
||||
name: "upstream config entries from Upstreams and service-defaults",
|
||||
|
|
|
@ -57,6 +57,12 @@ func (s *Server) revokeEnterpriseLeadership() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) startTenancyDeferredDeletion(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) stopTenancyDeferredDeletion() {
|
||||
}
|
||||
|
||||
func (s *Server) validateEnterpriseRequest(entMeta *acl.EnterpriseMeta, write bool) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,11 +6,12 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-raftchunking"
|
||||
"github.com/hashicorp/raft"
|
||||
|
||||
"github.com/hashicorp/consul-net-rpc/go-msgpack/codec"
|
||||
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/consul/stream"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -277,21 +278,49 @@ func (c *FSM) registerStreamSnapshotHandlers() {
|
|||
|
||||
err := c.deps.Publisher.RegisterHandler(state.EventTopicServiceHealth, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().ServiceHealthSnapshot(req, buf)
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicServiceHealthConnect, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().ServiceHealthSnapshot(req, buf)
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicCARoots, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().CARootsSnapshot(req, buf)
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicMeshConfig, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().MeshConfigSnapshot(req, buf)
|
||||
}, true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicServiceResolver, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().ServiceResolverSnapshot(req, buf)
|
||||
}, true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicIngressGateway, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().IngressGatewaySnapshot(req, buf)
|
||||
}, true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
||||
err = c.deps.Publisher.RegisterHandler(state.EventTopicServiceIntentions, func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (uint64, error) {
|
||||
return c.State().ServiceIntentionsSnapshot(req, buf)
|
||||
}, true)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error encountered registering streaming snapshot handlers: %w", err))
|
||||
}
|
||||
|
|
|
@ -451,7 +451,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
|
|||
Port: 8000,
|
||||
Connect: connectConf,
|
||||
})
|
||||
vip, err := fsm.state.VirtualIPForService(structs.NewServiceName("frontend", nil))
|
||||
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)}
|
||||
vip, err := fsm.state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vip, "240.0.0.1")
|
||||
|
||||
|
@ -462,7 +463,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
|
|||
Port: 9000,
|
||||
Connect: connectConf,
|
||||
})
|
||||
vip, err = fsm.state.VirtualIPForService(structs.NewServiceName("backend", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("backend", nil)}
|
||||
vip, err = fsm.state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vip, "240.0.0.2")
|
||||
|
||||
|
@ -476,6 +478,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
|
|||
|
||||
// Peerings
|
||||
require.NoError(t, fsm.state.PeeringWrite(31, &pbpeering.Peering{
|
||||
ID: "1fabcd52-1d46-49b0-b1d8-71559aee47f5",
|
||||
Name: "baz",
|
||||
}))
|
||||
|
||||
|
@ -591,10 +594,12 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
|
|||
require.Equal(t, uint64(25), checks[0].ModifyIndex)
|
||||
|
||||
// Verify virtual IPs are consistent.
|
||||
vip, err = fsm2.state.VirtualIPForService(structs.NewServiceName("frontend", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)}
|
||||
vip, err = fsm2.state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vip, "240.0.0.1")
|
||||
vip, err = fsm2.state.VirtualIPForService(structs.NewServiceName("backend", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("backend", nil)}
|
||||
vip, err = fsm2.state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vip, "240.0.0.2")
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/hashicorp/consul-net-rpc/net/rpc"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
|
@ -1209,6 +1210,102 @@ func registerTestRoutingConfigTopologyEntries(t *testing.T, codec rpc.ClientCode
|
|||
}
|
||||
}
|
||||
|
||||
func registerLocalAndRemoteServicesVIPEnabled(t *testing.T, state *state.Store) {
|
||||
t.Helper()
|
||||
|
||||
_, entry, err := state.SystemMetadataGet(nil, structs.SystemMetadataVirtualIPsEnabled)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, entry)
|
||||
require.Equal(t, "true", entry.Value)
|
||||
|
||||
// Register a local connect-native service
|
||||
require.NoError(t, state.EnsureRegistration(10, &structs.RegisterRequest{
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
Service: "api",
|
||||
Connect: structs.ServiceConnect{
|
||||
Native: true,
|
||||
},
|
||||
},
|
||||
}))
|
||||
// Should be assigned VIP
|
||||
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("api", nil)}
|
||||
vip, err := state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "240.0.0.1", vip)
|
||||
|
||||
// Register an imported service and its proxy
|
||||
require.NoError(t, state.EnsureRegistration(11, &structs.RegisterRequest{
|
||||
Node: "bar",
|
||||
SkipNodeUpdate: true,
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Service: "web",
|
||||
ID: "web-1",
|
||||
},
|
||||
PeerName: "peer-a",
|
||||
}))
|
||||
require.NoError(t, state.EnsureRegistration(12, &structs.RegisterRequest{
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindConnectProxy,
|
||||
ID: "web-proxy",
|
||||
Service: "web-proxy",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
DestinationServiceName: "web",
|
||||
},
|
||||
},
|
||||
PeerName: "peer-a",
|
||||
}))
|
||||
// Should be assigned one VIP for the real service name
|
||||
psn = structs.PeeredServiceName{Peer: "peer-a", ServiceName: structs.NewServiceName("web", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "240.0.0.2", vip)
|
||||
// web-proxy should not have a VIP
|
||||
psn = structs.PeeredServiceName{Peer: "peer-a", ServiceName: structs.NewServiceName("web-proxy", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, vip)
|
||||
|
||||
// Register an imported service and its proxy from another peer
|
||||
require.NoError(t, state.EnsureRegistration(11, &structs.RegisterRequest{
|
||||
Node: "gir",
|
||||
SkipNodeUpdate: true,
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
Service: "web",
|
||||
ID: "web-1",
|
||||
},
|
||||
PeerName: "peer-b",
|
||||
}))
|
||||
require.NoError(t, state.EnsureRegistration(12, &structs.RegisterRequest{
|
||||
Node: "gir",
|
||||
Address: "127.0.0.3",
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindConnectProxy,
|
||||
ID: "web-proxy",
|
||||
Service: "web-proxy",
|
||||
Proxy: structs.ConnectProxyConfig{
|
||||
DestinationServiceName: "web",
|
||||
},
|
||||
},
|
||||
PeerName: "peer-b",
|
||||
}))
|
||||
// Should be assigned one VIP for the real service name
|
||||
psn = structs.PeeredServiceName{Peer: "peer-b", ServiceName: structs.NewServiceName("web", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "240.0.0.3", vip)
|
||||
// web-proxy should not have a VIP
|
||||
psn = structs.PeeredServiceName{Peer: "peer-b", ServiceName: structs.NewServiceName("web-proxy", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, vip)
|
||||
}
|
||||
|
||||
func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token string) {
|
||||
t.Helper()
|
||||
|
||||
|
@ -1307,7 +1404,7 @@ func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token
|
|||
}
|
||||
registerTestCatalogEntriesMap(t, codec, registrations)
|
||||
|
||||
// Add intentions: deny all and web -> api
|
||||
// Add intentions: deny all and web -> api and web -> api.example.com
|
||||
entries := []structs.ConfigEntryRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
|
@ -1323,6 +1420,20 @@ func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token
|
|||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
Kind: structs.ServiceIntentions,
|
||||
Name: "api.example.com",
|
||||
Sources: []*structs.SourceIntention{
|
||||
{
|
||||
Name: "web",
|
||||
Action: structs.IntentionActionAllow,
|
||||
},
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceIntentionsConfigEntry{
|
||||
|
@ -1342,4 +1453,36 @@ func registerIntentionUpstreamEntries(t *testing.T, codec rpc.ClientCodec, token
|
|||
var out bool
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||
}
|
||||
|
||||
// Add destinations
|
||||
dests := []structs.ConfigEntryRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "api.example.com",
|
||||
Destination: &structs.DestinationConfig{
|
||||
Address: "api.example.com",
|
||||
Port: 443,
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Entry: &structs.ServiceConfigEntry{
|
||||
Kind: structs.ServiceDefaults,
|
||||
Name: "kafka.store.com",
|
||||
Destination: &structs.DestinationConfig{
|
||||
Address: "172.168.2.1",
|
||||
Port: 9003,
|
||||
},
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: token},
|
||||
},
|
||||
}
|
||||
for _, req := range dests {
|
||||
var out bool
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &out))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,10 @@ func (s *Intention) Apply(args *structs.IntentionRequest, reply *string) error {
|
|||
return ErrConnectNotEnabled
|
||||
}
|
||||
|
||||
if args.Intention != nil && args.Intention.SourcePeer != "" {
|
||||
return fmt.Errorf("SourcePeer field is not supported on this endpoint. Use config entries instead")
|
||||
}
|
||||
|
||||
// Ensure that all service-intentions config entry writes go to the primary
|
||||
// datacenter. These will then be replicated to all the other datacenters.
|
||||
args.Datacenter = s.srv.config.PrimaryDatacenter
|
||||
|
@ -432,7 +436,7 @@ func (s *Intention) Get(args *structs.IntentionQueryRequest, reply *structs.Inde
|
|||
}
|
||||
|
||||
if args.Exact != nil {
|
||||
// // Finish defaulting the namespace fields.
|
||||
// Finish defaulting the namespace fields.
|
||||
if args.Exact.SourceNS == "" {
|
||||
args.Exact.SourceNS = entMeta.NamespaceOrDefault()
|
||||
}
|
||||
|
@ -764,7 +768,7 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
|
|||
Partition: query.SourcePartition,
|
||||
Name: query.SourceName,
|
||||
}
|
||||
_, intentions, err := store.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
|
||||
_, intentions, err := store.IntentionMatchOne(nil, entry, structs.IntentionMatchSource, structs.IntentionTargetService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query intentions for %s/%s", query.SourceNS, query.SourceName)
|
||||
}
|
||||
|
|
|
@ -273,6 +273,41 @@ func TestIntentionApply_updateGood(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestIntentionApply_NoSourcePeer makes sure that no intention is created with a SourcePeer since this is not supported
|
||||
func TestIntentionApply_NoSourcePeer(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
_, s1 := testServer(t)
|
||||
codec := rpcClient(t, s1)
|
||||
|
||||
waitForLeaderEstablishment(t, s1)
|
||||
|
||||
// Setup a basic record to create
|
||||
ixn := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: &structs.Intention{
|
||||
SourceNS: structs.IntentionDefaultNamespace,
|
||||
SourceName: "test",
|
||||
SourcePeer: "peer1",
|
||||
DestinationNS: structs.IntentionDefaultNamespace,
|
||||
DestinationName: "test",
|
||||
Action: structs.IntentionActionAllow,
|
||||
SourceType: structs.IntentionSourceConsul,
|
||||
Meta: map[string]string{},
|
||||
},
|
||||
}
|
||||
var reply string
|
||||
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err, "SourcePeer field is not supported on this endpoint. Use config entries instead")
|
||||
require.Empty(t, reply)
|
||||
}
|
||||
|
||||
// Shouldn't be able to update a non-existent intention
|
||||
func TestIntentionApply_updateNonExist(t *testing.T) {
|
||||
if testing.Short() {
|
||||
|
|
|
@ -69,18 +69,60 @@ func (m *Internal) NodeDump(args *structs.DCSpecificRequest,
|
|||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
func(ws memdb.WatchSet, state *state.Store) error {
|
||||
index, dump, err := state.NodeDump(ws, &args.EnterpriseMeta, args.PeerName)
|
||||
if err != nil {
|
||||
return err
|
||||
// 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)
|
||||
}
|
||||
reply.Index, reply.Dump = index, dump
|
||||
|
||||
// this maxIndex will be the max of the NodeDump calls and the PeeringList call
|
||||
var maxIndex uint64
|
||||
// Get data for local nodes
|
||||
index, dump, err := state.NodeDump(ws, &args.EnterpriseMeta, structs.DefaultPeerKeyword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get a node dump for local nodes: %w", err)
|
||||
}
|
||||
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
reply.Dump = dump
|
||||
|
||||
// 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 node dump %w", err)
|
||||
}
|
||||
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
|
||||
// get node dumps for all peerings
|
||||
for _, p := range listedPeerings {
|
||||
index, importedDump, err := state.NodeDump(ws, &args.EnterpriseMeta, p.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get a node dump for peer %q: %w", p.Name, err)
|
||||
}
|
||||
reply.ImportedDump = append(reply.ImportedDump, importedDump...)
|
||||
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
reply.Index = maxIndex
|
||||
|
||||
raw, err := filter.Execute(reply.Dump)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("could not filter local node dump: %w", err)
|
||||
}
|
||||
reply.Dump = raw.(structs.NodeDump)
|
||||
|
||||
importedRaw, err := filter.Execute(reply.ImportedDump)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not filter peer node dump: %w", err)
|
||||
}
|
||||
reply.ImportedDump = importedRaw.(structs.NodeDump)
|
||||
|
||||
// Note: we filter the results with ACLs *after* applying the user-supplied
|
||||
// bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include
|
||||
// results that would be filtered out even if the user did have permission.
|
||||
|
@ -111,13 +153,47 @@ func (m *Internal) ServiceDump(args *structs.ServiceDumpRequest, reply *structs.
|
|||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
func(ws memdb.WatchSet, state *state.Store) error {
|
||||
// Get, store, and filter nodes
|
||||
maxIdx, nodes, err := state.ServiceDump(ws, args.ServiceKind, args.UseServiceKind, &args.EnterpriseMeta, args.PeerName)
|
||||
// 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 err
|
||||
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 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 {
|
||||
|
@ -125,17 +201,23 @@ func (m *Internal) ServiceDump(args *structs.ServiceDumpRequest, reply *structs.
|
|||
}
|
||||
reply.Gateways = gatewayServices
|
||||
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
if idx > maxIndex {
|
||||
maxIndex = idx
|
||||
}
|
||||
reply.Index = maxIdx
|
||||
reply.Index = maxIndex
|
||||
|
||||
raw, err := filter.Execute(reply.Nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("could not filter local service dump: %w", err)
|
||||
}
|
||||
reply.Nodes = raw.(structs.CheckServiceNodes)
|
||||
|
||||
importedRaw, err := filter.Execute(reply.ImportedNodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not filter peer service dump: %w", err)
|
||||
}
|
||||
reply.ImportedNodes = importedRaw.(structs.CheckServiceNodes)
|
||||
|
||||
// Note: we filter the results with ACLs *after* applying the user-supplied
|
||||
// bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include
|
||||
// results that would be filtered out even if the user did have permission.
|
||||
|
@ -210,7 +292,7 @@ func (m *Internal) ServiceTopology(args *structs.ServiceSpecificRequest, reply *
|
|||
})
|
||||
}
|
||||
|
||||
// IntentionUpstreams returns the upstreams of a service. Upstreams are inferred from intentions.
|
||||
// IntentionUpstreams returns a service's upstreams which are inferred from intentions.
|
||||
// If intentions allow a connection from the target to some candidate service, the candidate service is considered
|
||||
// an upstream of the target.
|
||||
func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, reply *structs.IndexedServiceList) error {
|
||||
|
@ -224,6 +306,27 @@ func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, repl
|
|||
if done, err := m.srv.ForwardRPC("Internal.IntentionUpstreams", args, reply); done {
|
||||
return err
|
||||
}
|
||||
return m.internalUpstreams(args, reply, structs.IntentionTargetService)
|
||||
}
|
||||
|
||||
// IntentionUpstreamsDestination returns a service's upstreams which are inferred from intentions.
|
||||
// If intentions allow a connection from the target to some candidate destination, the candidate destination is considered
|
||||
// an upstream of the target. This performs the same logic as IntentionUpstreams endpoint but for destination upstreams only.
|
||||
func (m *Internal) IntentionUpstreamsDestination(args *structs.ServiceSpecificRequest, reply *structs.IndexedServiceList) error {
|
||||
// Exit early if Connect hasn't been enabled.
|
||||
if !m.srv.config.ConnectEnabled {
|
||||
return ErrConnectNotEnabled
|
||||
}
|
||||
if args.ServiceName == "" {
|
||||
return fmt.Errorf("Must provide a service name")
|
||||
}
|
||||
if done, err := m.srv.ForwardRPC("Internal.IntentionUpstreamsDestination", args, reply); done {
|
||||
return err
|
||||
}
|
||||
return m.internalUpstreams(args, reply, structs.IntentionTargetDestination)
|
||||
}
|
||||
|
||||
func (m *Internal) internalUpstreams(args *structs.ServiceSpecificRequest, reply *structs.IndexedServiceList, intentionTarget structs.IntentionTargetType) error {
|
||||
|
||||
authz, err := m.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
|
||||
if err != nil {
|
||||
|
@ -244,7 +347,7 @@ func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, repl
|
|||
defaultDecision := authz.IntentionDefaultAllow(nil)
|
||||
|
||||
sn := structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta)
|
||||
index, services, err := state.IntentionTopology(ws, sn, false, defaultDecision)
|
||||
index, services, err := state.IntentionTopology(ws, sn, false, defaultDecision, intentionTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -272,7 +375,7 @@ func (m *Internal) IntentionUpstreams(args *structs.ServiceSpecificRequest, repl
|
|||
})
|
||||
}
|
||||
|
||||
// GatewayServiceNodes returns all the nodes for services associated with a gateway along with their gateway config
|
||||
// GatewayServiceDump returns all the nodes for services associated with a gateway along with their gateway config
|
||||
func (m *Internal) GatewayServiceDump(args *structs.ServiceSpecificRequest, reply *structs.IndexedServiceDump) error {
|
||||
if done, err := m.srv.ForwardRPC("Internal.GatewayServiceDump", args, reply); done {
|
||||
return err
|
||||
|
@ -350,7 +453,7 @@ func (m *Internal) GatewayServiceDump(args *structs.ServiceSpecificRequest, repl
|
|||
return err
|
||||
}
|
||||
|
||||
// Match returns the set of intentions that match the given source/destination.
|
||||
// GatewayIntentions Match returns the set of intentions that match the given source/destination.
|
||||
func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply *structs.IndexedIntentions) error {
|
||||
// Forward if necessary
|
||||
if done, err := m.srv.ForwardRPC("Internal.GatewayIntentions", args, reply); done {
|
||||
|
@ -405,7 +508,7 @@ func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply
|
|||
Partition: gs.Service.PartitionOrDefault(),
|
||||
Name: gs.Service.Name,
|
||||
}
|
||||
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination)
|
||||
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination, structs.IntentionTargetService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -468,6 +571,49 @@ func (m *Internal) ExportedPeeredServices(args *structs.DCSpecificRequest, reply
|
|||
})
|
||||
}
|
||||
|
||||
// PeeredUpstreams returns all imported services as upstreams for any service in a given partition.
|
||||
// Cluster peering does not replicate intentions so all imported services are considered potential upstreams.
|
||||
func (m *Internal) PeeredUpstreams(args *structs.PartitionSpecificRequest, reply *structs.IndexedPeeredServiceList) error {
|
||||
// Exit early if Connect hasn't been enabled.
|
||||
if !m.srv.config.ConnectEnabled {
|
||||
return ErrConnectNotEnabled
|
||||
}
|
||||
if done, err := m.srv.ForwardRPC("Internal.PeeredUpstreams", args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(peering): ACL for filtering
|
||||
// authz, err := m.srv.ResolveTokenAndDefaultMeta(args.Token, &args.EnterpriseMeta, nil)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
if err := m.srv.validateEnterpriseRequest(&args.EnterpriseMeta, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.srv.blockingQuery(
|
||||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
func(ws memdb.WatchSet, state *state.Store) error {
|
||||
index, vips, err := state.VirtualIPsForAllImportedServices(ws, args.EnterpriseMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := make([]structs.PeeredServiceName, 0, len(vips))
|
||||
for _, vip := range vips {
|
||||
result = append(result, vip.Service)
|
||||
}
|
||||
|
||||
reply.Index, reply.Services = index, result
|
||||
|
||||
// TODO(peering): low priority: consider ACL filtering
|
||||
// m.srv.filterACLWithAuthorizer(authz, reply)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EventFire is a bit of an odd endpoint, but it allows for a cross-DC RPC
|
||||
// call to fire an event. The primary use case is to enable user events being
|
||||
// triggered in a remote DC.
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul-net-rpc/net/rpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib/stringslice"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
|
@ -29,56 +31,79 @@ func TestInternal_NodeInfo(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Parallel()
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
_, s1 := testServer(t)
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
args := []*structs.RegisterRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.3",
|
||||
PeerName: "peer1",
|
||||
},
|
||||
}
|
||||
var out struct{}
|
||||
if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
||||
for _, reg := range args {
|
||||
err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.NodeSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
}
|
||||
if err := msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
t.Run("get local node", func(t *testing.T) {
|
||||
var out structs.IndexedNodeDump
|
||||
req := structs.NodeSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
}
|
||||
if err := msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
nodes := out2.Dump
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("Bad: %v", nodes)
|
||||
}
|
||||
if nodes[0].Node != "foo" {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if !stringslice.Contains(nodes[0].Services[0].Tags, "primary") {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if nodes[0].Checks[0].Status != api.HealthPassing {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
nodes := out.Dump
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("Bad: %v", nodes)
|
||||
}
|
||||
if nodes[0].Node != "foo" {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if !stringslice.Contains(nodes[0].Services[0].Tags, "primary") {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
if nodes[0].Checks[0].Status != api.HealthPassing {
|
||||
t.Fatalf("Bad: %v", nodes[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get peered node", func(t *testing.T) {
|
||||
var out structs.IndexedNodeDump
|
||||
req := structs.NodeSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
PeerName: "peer1",
|
||||
}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeInfo", &req, &out))
|
||||
|
||||
nodes := out.Dump
|
||||
require.Equal(t, 1, len(nodes))
|
||||
require.Equal(t, "foo", nodes[0].Node)
|
||||
require.Equal(t, "peer1", nodes[0].PeerName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternal_NodeDump(t *testing.T) {
|
||||
|
@ -87,53 +112,61 @@ func TestInternal_NodeDump(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Parallel()
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
_, s1 := testServer(t)
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
args := []*structs.RegisterRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"replica"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthWarning,
|
||||
ServiceID: "db",
|
||||
},
|
||||
},
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo-peer",
|
||||
Address: "127.0.0.3",
|
||||
PeerName: "peer1",
|
||||
},
|
||||
}
|
||||
var out struct{}
|
||||
if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
arg = structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"replica"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthWarning,
|
||||
ServiceID: "db",
|
||||
},
|
||||
}
|
||||
if err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
for _, reg := range args {
|
||||
err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{
|
||||
ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9",
|
||||
Name: "peer1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
|
@ -175,6 +208,10 @@ func TestInternal_NodeDump(t *testing.T) {
|
|||
if !foundFoo || !foundBar {
|
||||
t.Fatalf("missing foo or bar")
|
||||
}
|
||||
|
||||
require.Len(t, out2.ImportedDump, 1)
|
||||
require.Equal(t, "peer1", out2.ImportedDump[0].PeerName)
|
||||
require.Equal(t, "foo-peer", out2.ImportedDump[0].Node)
|
||||
}
|
||||
|
||||
func TestInternal_NodeDump_Filter(t *testing.T) {
|
||||
|
@ -183,60 +220,107 @@ func TestInternal_NodeDump_Filter(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Parallel()
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
_, s1 := testServer(t)
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
args := []*structs.RegisterRequest{
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"primary"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "db",
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"replica"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthWarning,
|
||||
ServiceID: "db",
|
||||
},
|
||||
},
|
||||
}
|
||||
var out struct{}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||
|
||||
arg = structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.2",
|
||||
Service: &structs.NodeService{
|
||||
ID: "db",
|
||||
Service: "db",
|
||||
Tags: []string{"replica"},
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "db connect",
|
||||
Status: api.HealthWarning,
|
||||
ServiceID: "db",
|
||||
{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo-peer",
|
||||
Address: "127.0.0.3",
|
||||
PeerName: "peer1",
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, &out))
|
||||
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Filter: "primary in Services.Tags"},
|
||||
for _, reg := range args {
|
||||
err := msgpackrpc.CallWithCodec(codec, "Catalog.Register", reg, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req, &out2))
|
||||
|
||||
nodes := out2.Dump
|
||||
require.Len(t, nodes, 1)
|
||||
require.Equal(t, "foo", nodes[0].Node)
|
||||
err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{
|
||||
ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9",
|
||||
Name: "peer1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("filter on the local node", func(t *testing.T) {
|
||||
var out2 structs.IndexedNodeDump
|
||||
req := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Filter: "primary in Services.Tags"},
|
||||
}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req, &out2))
|
||||
|
||||
nodes := out2.Dump
|
||||
require.Len(t, nodes, 1)
|
||||
require.Equal(t, "foo", nodes[0].Node)
|
||||
})
|
||||
|
||||
t.Run("filter on imported dump", func(t *testing.T) {
|
||||
var out3 structs.IndexedNodeDump
|
||||
req2 := structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
QueryOptions: structs.QueryOptions{Filter: "friend in PeerName"},
|
||||
}
|
||||
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3))
|
||||
require.Len(t, out3.Dump, 0)
|
||||
require.Len(t, out3.ImportedDump, 0)
|
||||
})
|
||||
|
||||
t.Run("filter look for peer nodes (non local nodes)", func(t *testing.T) {
|
||||
var out3 structs.IndexedNodeDump
|
||||
req2 := structs.DCSpecificRequest{
|
||||
QueryOptions: structs.QueryOptions{Filter: "PeerName != \"\""},
|
||||
}
|
||||
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3))
|
||||
require.Len(t, out3.Dump, 0)
|
||||
require.Len(t, out3.ImportedDump, 1)
|
||||
})
|
||||
|
||||
t.Run("filter look for a specific peer", func(t *testing.T) {
|
||||
var out3 structs.IndexedNodeDump
|
||||
req2 := structs.DCSpecificRequest{
|
||||
QueryOptions: structs.QueryOptions{Filter: "PeerName == peer1"},
|
||||
}
|
||||
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.NodeDump", &req2, &out3))
|
||||
require.Len(t, out3.Dump, 0)
|
||||
require.Len(t, out3.ImportedDump, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternal_KeyringOperation(t *testing.T) {
|
||||
|
@ -1665,6 +1749,89 @@ func TestInternal_GatewayServiceDump_Ingress_ACL(t *testing.T) {
|
|||
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
||||
}
|
||||
|
||||
func TestInternal_ServiceDump_Peering(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
_, s1 := testServer(t)
|
||||
codec := rpcClient(t, s1)
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
// prep the cluster with some data we can use in our filters
|
||||
registerTestCatalogEntries(t, codec)
|
||||
|
||||
doRequest := func(t *testing.T, filter string) structs.IndexedNodesWithGateways {
|
||||
t.Helper()
|
||||
args := structs.DCSpecificRequest{
|
||||
QueryOptions: structs.QueryOptions{Filter: filter},
|
||||
}
|
||||
|
||||
var out structs.IndexedNodesWithGateways
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceDump", &args, &out))
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
t.Run("No peerings", func(t *testing.T) {
|
||||
nodes := doRequest(t, "")
|
||||
// redis (3), web (3), critical (1), warning (1) and consul (1)
|
||||
require.Len(t, nodes.Nodes, 9)
|
||||
require.Len(t, nodes.ImportedNodes, 0)
|
||||
})
|
||||
|
||||
addPeerService(t, codec)
|
||||
|
||||
err := s1.fsm.State().PeeringWrite(1, &pbpeering.Peering{
|
||||
ID: "9e650110-ac74-4c5a-a6a8-9348b2bed4e9",
|
||||
Name: "peer1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("peerings", func(t *testing.T) {
|
||||
nodes := doRequest(t, "")
|
||||
// redis (3), web (3), critical (1), warning (1) and consul (1)
|
||||
require.Len(t, nodes.Nodes, 9)
|
||||
// service (1)
|
||||
require.Len(t, nodes.ImportedNodes, 1)
|
||||
})
|
||||
|
||||
t.Run("peerings w filter", func(t *testing.T) {
|
||||
nodes := doRequest(t, "Node.PeerName == foo")
|
||||
require.Len(t, nodes.Nodes, 0)
|
||||
require.Len(t, nodes.ImportedNodes, 0)
|
||||
|
||||
nodes2 := doRequest(t, "Node.PeerName == peer1")
|
||||
require.Len(t, nodes2.Nodes, 0)
|
||||
require.Len(t, nodes2.ImportedNodes, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func addPeerService(t *testing.T, codec rpc.ClientCodec) {
|
||||
// prep the cluster with some data we can use in our filters
|
||||
registrations := map[string]*structs.RegisterRequest{
|
||||
"Peer node foo with peer service": {
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
ID: types.NodeID("e0155642-135d-4739-9853-a1ee6c9f945b"),
|
||||
Address: "127.0.0.2",
|
||||
PeerName: "peer1",
|
||||
Service: &structs.NodeService{
|
||||
Kind: structs.ServiceKindTypical,
|
||||
ID: "serviceID",
|
||||
Service: "service",
|
||||
Port: 1235,
|
||||
Address: "198.18.1.2",
|
||||
PeerName: "peer1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
registerTestCatalogEntriesMap(t, codec, registrations)
|
||||
}
|
||||
|
||||
func TestInternal_GatewayIntentions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
@ -2323,6 +2490,50 @@ func TestInternal_IntentionUpstreams(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestInternal_IntentionUpstreamsDestination(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
// Services:
|
||||
// api and api-proxy on node foo
|
||||
// web and web-proxy on node foo
|
||||
//
|
||||
// Intentions
|
||||
// * -> * (deny) intention
|
||||
// web -> api (allow)
|
||||
registerIntentionUpstreamEntries(t, codec, "")
|
||||
|
||||
t.Run("api.example.com", func(t *testing.T) {
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
args := structs.ServiceSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
ServiceName: "web",
|
||||
}
|
||||
var out structs.IndexedServiceList
|
||||
require.NoError(r, msgpackrpc.CallWithCodec(codec, "Internal.IntentionUpstreamsDestination", &args, &out))
|
||||
|
||||
// foo/api
|
||||
require.Len(r, out.Services, 1)
|
||||
|
||||
expectUp := structs.ServiceList{
|
||||
structs.NewServiceName("api.example.com", structs.DefaultEnterpriseMetaInDefaultPartition()),
|
||||
}
|
||||
require.Equal(r, expectUp, out.Services)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternal_IntentionUpstreams_BlockOnNoChange(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
|
@ -2565,3 +2776,38 @@ func TestInternal_CatalogOverview_ACLDeny(t *testing.T) {
|
|||
arg.Token = opReadToken.SecretID
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.CatalogOverview", &arg, &out))
|
||||
}
|
||||
|
||||
func TestInternal_PeeredUpstreams(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
_, s1 := testServerWithConfig(t)
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
// Services
|
||||
// api local
|
||||
// web peer: peer-a
|
||||
// web-proxy peer: peer-a
|
||||
// web peer: peer-b
|
||||
// web-proxy peer: peer-b
|
||||
registerLocalAndRemoteServicesVIPEnabled(t, s1.fsm.State())
|
||||
|
||||
codec := rpcClient(t, s1)
|
||||
|
||||
args := structs.PartitionSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
|
||||
}
|
||||
var out structs.IndexedPeeredServiceList
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.PeeredUpstreams", &args, &out))
|
||||
|
||||
require.Len(t, out.Services, 2)
|
||||
expect := []structs.PeeredServiceName{
|
||||
{Peer: "peer-a", ServiceName: structs.NewServiceName("web", structs.DefaultEnterpriseMetaInDefaultPartition())},
|
||||
{Peer: "peer-b", ServiceName: structs.NewServiceName("web", structs.DefaultEnterpriseMetaInDefaultPartition())},
|
||||
}
|
||||
require.Equal(t, expect, out.Services)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/hashicorp/go-memdb"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/acl/resolver"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
|
@ -32,7 +33,7 @@ type KVS struct {
|
|||
// preApply does all the verification of a KVS update that is performed BEFORE
|
||||
// we submit as a Raft log entry. This includes enforcing the lock delay which
|
||||
// must only be done on the leader.
|
||||
func kvsPreApply(logger hclog.Logger, srv *Server, authz ACLResolveResult, op api.KVOp, dirEnt *structs.DirEntry) (bool, error) {
|
||||
func kvsPreApply(logger hclog.Logger, srv *Server, authz resolver.Result, op api.KVOp, dirEnt *structs.DirEntry) (bool, error) {
|
||||
// Verify the entry.
|
||||
if dirEnt.Key == "" && op != api.KVDeleteTree {
|
||||
return false, fmt.Errorf("Must provide key")
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/metadata"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/agent/structs/aclfilter"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
|
@ -47,6 +48,9 @@ var LeaderSummaries = []prometheus.SummaryDefinition{
|
|||
const (
|
||||
newLeaderEvent = "consul:new-leader"
|
||||
barrierWriteTimeout = 2 * time.Minute
|
||||
|
||||
defaultDeletionRoundBurst int = 5 // number replication round bursts
|
||||
defaultDeletionApplyRate rate.Limit = 10 // raft applies per second
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -313,6 +317,8 @@ func (s *Server) establishLeadership(ctx context.Context) error {
|
|||
|
||||
s.startPeeringStreamSync(ctx)
|
||||
|
||||
s.startDeferredDeletion(ctx)
|
||||
|
||||
if err := s.startConnectLeader(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -380,7 +386,7 @@ func (s *Server) initializeACLs(ctx context.Context) error {
|
|||
|
||||
// Remove any token affected by CVE-2019-8336
|
||||
if !s.InPrimaryDatacenter() {
|
||||
_, token, err := s.fsm.State().ACLTokenGetBySecret(nil, redactedToken, nil)
|
||||
_, token, err := s.fsm.State().ACLTokenGetBySecret(nil, aclfilter.RedactedToken, nil)
|
||||
if err == nil && token != nil {
|
||||
req := structs.ACLTokenBatchDeleteRequest{
|
||||
TokenIDs: []string{token.AccessorID},
|
||||
|
@ -751,6 +757,16 @@ func (s *Server) stopACLReplication() {
|
|||
s.leaderRoutineManager.Stop(aclTokenReplicationRoutineName)
|
||||
}
|
||||
|
||||
func (s *Server) startDeferredDeletion(ctx context.Context) {
|
||||
s.startPeeringDeferredDeletion(ctx)
|
||||
s.startTenancyDeferredDeletion(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) stopDeferredDeletion() {
|
||||
s.leaderRoutineManager.Stop(peeringDeletionRoutineName)
|
||||
s.stopTenancyDeferredDeletion()
|
||||
}
|
||||
|
||||
func (s *Server) startConfigReplication(ctx context.Context) {
|
||||
if s.config.PrimaryDatacenter == "" || s.config.PrimaryDatacenter == s.config.Datacenter {
|
||||
// replication shouldn't run in the primary DC
|
||||
|
|
|
@ -1412,6 +1412,20 @@ func (c *CAManager) AuthorizeAndSignCertificate(csr *x509.CertificateRequest, au
|
|||
if err := allow.NodeWriteAllowed(v.Agent, &authzContext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case *connect.SpiffeIDMeshGateway:
|
||||
// TODO(peering): figure out what is appropriate here for ACLs
|
||||
v.GetEnterpriseMeta().FillAuthzContext(&authzContext)
|
||||
if err := allow.MeshWriteAllowed(&authzContext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify that the DC in the gateway URI matches us. We might relax this
|
||||
// requirement later but being restrictive for now is safer.
|
||||
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")
|
||||
}
|
||||
|
@ -1436,18 +1450,25 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne
|
|||
signingID := connect.SpiffeIDSigningForCluster(config.ClusterID)
|
||||
serviceID, isService := spiffeID.(*connect.SpiffeIDService)
|
||||
agentID, isAgent := spiffeID.(*connect.SpiffeIDAgent)
|
||||
if !isService && !isAgent {
|
||||
return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service or agent ID")
|
||||
}
|
||||
mgwID, isMeshGateway := spiffeID.(*connect.SpiffeIDMeshGateway)
|
||||
|
||||
var entMeta acl.EnterpriseMeta
|
||||
if isService {
|
||||
switch {
|
||||
case isService:
|
||||
if !signingID.CanSign(spiffeID) {
|
||||
return nil, connect.InvalidCSRError("SPIFFE ID in CSR from a different trust domain: %s, "+
|
||||
"we are %s", serviceID.Host, signingID.Host())
|
||||
}
|
||||
entMeta.Merge(serviceID.GetEnterpriseMeta())
|
||||
} else {
|
||||
|
||||
case isMeshGateway:
|
||||
if !signingID.CanSign(spiffeID) {
|
||||
return nil, connect.InvalidCSRError("SPIFFE ID in CSR from a different trust domain: %s, "+
|
||||
"we are %s", mgwID.Host, signingID.Host())
|
||||
}
|
||||
entMeta.Merge(mgwID.GetEnterpriseMeta())
|
||||
|
||||
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
|
||||
// auto-config they make certificate requests before learning about the roots
|
||||
|
@ -1471,6 +1492,9 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne
|
|||
csr.URIs = uris
|
||||
}
|
||||
entMeta.Merge(agentID.GetEnterpriseMeta())
|
||||
|
||||
default:
|
||||
return nil, connect.InvalidCSRError("SPIFFE ID in CSR must be a service, agent, or mesh gateway ID")
|
||||
}
|
||||
|
||||
commonCfg, err := config.GetCommonConfig()
|
||||
|
@ -1548,12 +1572,19 @@ func (c *CAManager) SignCertificate(csr *x509.CertificateRequest, spiffeID conne
|
|||
CreateIndex: modIdx,
|
||||
},
|
||||
}
|
||||
if isService {
|
||||
|
||||
switch {
|
||||
case isService:
|
||||
reply.Service = serviceID.Service
|
||||
reply.ServiceURI = cert.URIs[0].String()
|
||||
} else if isAgent {
|
||||
case isMeshGateway:
|
||||
reply.Kind = structs.ServiceKindMeshGateway
|
||||
reply.KindURI = cert.URIs[0].String()
|
||||
case isAgent:
|
||||
reply.Agent = agentID.Agent
|
||||
reply.AgentURI = cert.URIs[0].String()
|
||||
default:
|
||||
return nil, errors.New("not possible")
|
||||
}
|
||||
|
||||
return &reply, nil
|
||||
|
|
|
@ -8,16 +8,21 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/hashicorp/consul/agent/rpc/peering"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"golang.org/x/time/rate"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/pool"
|
||||
"github.com/hashicorp/consul/agent/rpc/peering"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/logging"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
)
|
||||
|
||||
|
@ -50,6 +55,39 @@ func (s *Server) stopPeeringStreamSync() {
|
|||
// syncPeeringsAndBlock is a long-running goroutine that is responsible for watching
|
||||
// changes to peerings in the state store and managing streams to those peers.
|
||||
func (s *Server) syncPeeringsAndBlock(ctx context.Context, logger hclog.Logger, cancelFns map[string]context.CancelFunc) error {
|
||||
// We have to be careful not to introduce a data race here. We want to
|
||||
// compare the current known peerings in the state store with known
|
||||
// connected streams to know when we should TERMINATE stray peerings.
|
||||
//
|
||||
// If you read the current peerings from the state store, then read the
|
||||
// current established streams you could lose the data race and have the
|
||||
// sequence of events be:
|
||||
//
|
||||
// 1. list peerings [A,B,C]
|
||||
// 2. persist new peering [D]
|
||||
// 3. accept new stream for [D]
|
||||
// 4. list streams [A,B,C,D]
|
||||
// 5. terminate [D]
|
||||
//
|
||||
// Which is wrong. If we instead ensure that (4) happens before (1), given
|
||||
// that you can't get an established stream without first passing a "does
|
||||
// this peering exist in the state store?" inquiry then this happens:
|
||||
//
|
||||
// 1. list streams [A,B,C]
|
||||
// 2. list peerings [A,B,C]
|
||||
// 3. persist new peering [D]
|
||||
// 4. accept new stream for [D]
|
||||
// 5. terminate []
|
||||
//
|
||||
// Or even this is fine:
|
||||
//
|
||||
// 1. list streams [A,B,C]
|
||||
// 2. persist new peering [D]
|
||||
// 3. accept new stream for [D]
|
||||
// 4. list peerings [A,B,C,D]
|
||||
// 5. terminate []
|
||||
connectedStreams := s.peeringService.ConnectedStreams()
|
||||
|
||||
state := s.fsm.State()
|
||||
|
||||
// Pull the state store contents and set up to block for changes.
|
||||
|
@ -81,18 +119,24 @@ func (s *Server) syncPeeringsAndBlock(ctx context.Context, logger hclog.Logger,
|
|||
for _, peer := range peers {
|
||||
logger.Trace("evaluating stored peer", "peer", peer.Name, "should_dial", peer.ShouldDial(), "sequence_id", seq)
|
||||
|
||||
if !peer.ShouldDial() {
|
||||
if !peer.IsActive() {
|
||||
// The peering was marked for deletion by ourselves or our peer, no need to dial or track them.
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(peering) Account for deleted peers that are still in the state store
|
||||
// Track all active peerings,since the reconciliation loop below applies to the token generator as well.
|
||||
stored[peer.ID] = struct{}{}
|
||||
|
||||
if !peer.ShouldDial() {
|
||||
// We do not need to dial peerings where we generated the peering token.
|
||||
continue
|
||||
}
|
||||
|
||||
status, found := s.peeringService.StreamStatus(peer.ID)
|
||||
|
||||
// TODO(peering): If there is new peering data and a connected stream, should we tear down the stream?
|
||||
// If the data in the updated token is bad, the user wouldn't know until the old servers/certs become invalid.
|
||||
// Alternatively we could do a basic Ping from the initiate peering endpoint to avoid dealing with that here.
|
||||
// Alternatively we could do a basic Ping from the establish peering endpoint to avoid dealing with that here.
|
||||
if found && status.Connected {
|
||||
// Nothing to do when we already have an active stream to the peer.
|
||||
continue
|
||||
|
@ -121,7 +165,7 @@ func (s *Server) syncPeeringsAndBlock(ctx context.Context, logger hclog.Logger,
|
|||
|
||||
// Clean up active streams of peerings that were deleted from the state store.
|
||||
// TODO(peering): This is going to trigger shutting down peerings we generated a token for. Is that OK?
|
||||
for stream, doneCh := range s.peeringService.ConnectedStreams() {
|
||||
for stream, doneCh := range connectedStreams {
|
||||
if _, ok := stored[stream]; ok {
|
||||
// Active stream is in the state store, nothing to do.
|
||||
continue
|
||||
|
@ -146,6 +190,8 @@ func (s *Server) syncPeeringsAndBlock(ctx context.Context, logger hclog.Logger,
|
|||
}
|
||||
|
||||
func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, peer *pbpeering.Peering, cancelFns map[string]context.CancelFunc) error {
|
||||
logger = logger.With("peer_name", peer.Name, "peer_id", peer.ID)
|
||||
|
||||
tlsOption := grpc.WithInsecure()
|
||||
if len(peer.PeerCAPems) > 0 {
|
||||
var haveCerts bool
|
||||
|
@ -175,7 +221,7 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, peer
|
|||
buffer = buffer.Next()
|
||||
}
|
||||
|
||||
logger.Trace("establishing stream to peer", "peer_id", peer.ID)
|
||||
logger.Trace("establishing stream to peer")
|
||||
|
||||
retryCtx, cancel := context.WithCancel(ctx)
|
||||
cancelFns[peer.ID] = cancel
|
||||
|
@ -191,7 +237,7 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, peer
|
|||
return fmt.Errorf("peer server address type %T is not a string", buffer.Value)
|
||||
}
|
||||
|
||||
logger.Trace("dialing peer", "peer_id", peer.ID, "addr", addr)
|
||||
logger.Trace("dialing peer", "addr", addr)
|
||||
conn, err := grpc.DialContext(retryCtx, addr,
|
||||
grpc.WithContextDialer(newPeerDialer(addr)),
|
||||
grpc.WithBlock(),
|
||||
|
@ -208,16 +254,23 @@ func (s *Server) establishStream(ctx context.Context, logger hclog.Logger, peer
|
|||
return err
|
||||
}
|
||||
|
||||
err = s.peeringService.HandleStream(peering.HandleStreamRequest{
|
||||
streamReq := peering.HandleStreamRequest{
|
||||
LocalID: peer.ID,
|
||||
RemoteID: peer.PeerID,
|
||||
PeerName: peer.Name,
|
||||
Partition: peer.Partition,
|
||||
Stream: stream,
|
||||
})
|
||||
}
|
||||
err = s.peeringService.HandleStream(streamReq)
|
||||
// A nil error indicates that the peering was deleted and the stream needs to be gracefully shutdown.
|
||||
if err == nil {
|
||||
stream.CloseSend()
|
||||
s.peeringService.DrainStream(streamReq)
|
||||
|
||||
// This will cancel the retry-er context, letting us break out of this loop when we want to shut down the stream.
|
||||
cancel()
|
||||
|
||||
logger.Info("closed outbound stream")
|
||||
}
|
||||
return err
|
||||
|
||||
|
@ -249,3 +302,156 @@ func newPeerDialer(peerAddr string) func(context.Context, string) (net.Conn, err
|
|||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) startPeeringDeferredDeletion(ctx context.Context) {
|
||||
s.leaderRoutineManager.Start(ctx, peeringDeletionRoutineName, s.runPeeringDeletions)
|
||||
}
|
||||
|
||||
// runPeeringDeletions watches for peerings marked for deletions and then cleans up data for them.
|
||||
func (s *Server) runPeeringDeletions(ctx context.Context) error {
|
||||
logger := s.loggers.Named(logging.Peering)
|
||||
|
||||
// This limiter's purpose is to control the rate of raft applies caused by the deferred deletion
|
||||
// process. This includes deletion of the peerings themselves in addition to any peering data
|
||||
raftLimiter := rate.NewLimiter(defaultDeletionApplyRate, int(defaultDeletionApplyRate))
|
||||
for {
|
||||
ws := memdb.NewWatchSet()
|
||||
state := s.fsm.State()
|
||||
_, peerings, err := s.fsm.State().PeeringListDeleted(ws)
|
||||
if err != nil {
|
||||
logger.Warn("encountered an error while searching for deleted peerings", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(peerings) == 0 {
|
||||
ws.Add(state.AbandonCh())
|
||||
|
||||
// wait for a peering to be deleted or the routine to be cancelled
|
||||
if err := ws.WatchCtx(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range peerings {
|
||||
s.removePeeringAndData(ctx, logger, raftLimiter, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removepPeeringAndData removes data imported for a peering and the peering itself.
|
||||
func (s *Server) removePeeringAndData(ctx context.Context, logger hclog.Logger, limiter *rate.Limiter, peer *pbpeering.Peering) {
|
||||
logger = logger.With("peer_name", peer.Name, "peer_id", peer.ID)
|
||||
entMeta := *structs.NodeEnterpriseMetaInPartition(peer.Partition)
|
||||
|
||||
// First delete all imported data.
|
||||
// By deleting all imported nodes we also delete all services and checks registered on them.
|
||||
if err := s.deleteAllNodes(ctx, limiter, entMeta, peer.Name); err != nil {
|
||||
logger.Error("Failed to remove Nodes for peer", "error", err)
|
||||
return
|
||||
}
|
||||
if err := s.deleteTrustBundleFromPeer(ctx, limiter, entMeta, peer.Name); err != nil {
|
||||
logger.Error("Failed to remove trust bundle for peer", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if peer.State == pbpeering.PeeringState_TERMINATED {
|
||||
// For peerings terminated by our peer we only clean up the local data, we do not delete the peering itself.
|
||||
// This is to avoid a situation where the peering disappears without the local operator's knowledge.
|
||||
return
|
||||
}
|
||||
|
||||
// Once all imported data is deleted, the peering itself is also deleted.
|
||||
req := &pbpeering.PeeringDeleteRequest{
|
||||
Name: peer.Name,
|
||||
Partition: acl.PartitionOrDefault(peer.Partition),
|
||||
}
|
||||
_, err := s.raftApplyProtobuf(structs.PeeringDeleteType, req)
|
||||
if err != nil {
|
||||
logger.Error("failed to apply full peering deletion", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// deleteAllNodes will delete all nodes in a partition or all nodes imported from a given peer name.
|
||||
func (s *Server) deleteAllNodes(ctx context.Context, limiter *rate.Limiter, entMeta acl.EnterpriseMeta, peerName string) error {
|
||||
// Same as ACL batch upsert size
|
||||
nodeBatchSizeBytes := 256 * 1024
|
||||
|
||||
_, nodes, err := s.fsm.State().NodeDump(nil, &entMeta, peerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
i := 0
|
||||
for {
|
||||
var ops structs.TxnOps
|
||||
for batchSize := 0; batchSize < nodeBatchSizeBytes && i < len(nodes); i++ {
|
||||
entry := nodes[i]
|
||||
|
||||
op := structs.TxnOp{
|
||||
Node: &structs.TxnNodeOp{
|
||||
Verb: api.NodeDelete,
|
||||
Node: structs.Node{
|
||||
Node: entry.Node,
|
||||
Partition: entry.Partition,
|
||||
PeerName: entry.PeerName,
|
||||
},
|
||||
},
|
||||
}
|
||||
ops = append(ops, &op)
|
||||
|
||||
// Add entries to the transaction until it reaches the max batch size
|
||||
batchSize += len(entry.Node) + len(entry.Partition) + len(entry.PeerName)
|
||||
}
|
||||
|
||||
// Send each batch as a TXN Req to avoid sending one at a time
|
||||
req := structs.TxnRequest{
|
||||
Datacenter: s.config.Datacenter,
|
||||
Ops: ops,
|
||||
}
|
||||
if len(req.Ops) > 0 {
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := s.raftApplyMsgpack(structs.TxnRequestType, &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteTrustBundleFromPeer deletes the trust bundle imported from a peer, if present.
|
||||
func (s *Server) deleteTrustBundleFromPeer(ctx context.Context, limiter *rate.Limiter, entMeta acl.EnterpriseMeta, peerName string) error {
|
||||
_, bundle, err := s.fsm.State().PeeringTrustBundleRead(nil, state.Query{Value: peerName, EnterpriseMeta: entMeta})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bundle == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := limiter.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &pbpeering.PeeringTrustBundleDeleteRequest{
|
||||
Name: peerName,
|
||||
Partition: entMeta.PartitionOrDefault(),
|
||||
}
|
||||
_, err = s.raftApplyProtobuf(structs.PeeringTrustBundleDeleteType, req)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,8 +10,10 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/consul/state"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/proto/pbpeering"
|
||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
|
@ -60,6 +62,10 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) {
|
|||
_, found := s1.peeringService.StreamStatus(token.PeerID)
|
||||
require.False(t, found)
|
||||
|
||||
var (
|
||||
s2PeerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d"
|
||||
)
|
||||
|
||||
// Bring up s2 and store s1's token so that it attempts to dial.
|
||||
_, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.NodeName = "s2.dc2"
|
||||
|
@ -71,6 +77,7 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) {
|
|||
// Simulate a peering initiation event by writing a peering with data from a peering token.
|
||||
// Eventually the leader in dc2 should dial and connect to the leader in dc1.
|
||||
p := &pbpeering.Peering{
|
||||
ID: s2PeerID,
|
||||
Name: "my-peer-s1",
|
||||
PeerID: token.PeerID,
|
||||
PeerCAPems: token.CA,
|
||||
|
@ -88,10 +95,13 @@ func TestLeader_PeeringSync_Lifecycle_ClientDeletion(t *testing.T) {
|
|||
require.True(r, status.Connected)
|
||||
})
|
||||
|
||||
// Delete the peering to trigger the termination sequence
|
||||
require.NoError(t, s2.fsm.State().PeeringDelete(2000, state.Query{
|
||||
Value: "my-peer-s1",
|
||||
}))
|
||||
// Delete the peering to trigger the termination sequence.
|
||||
deleted := &pbpeering.Peering{
|
||||
ID: s2PeerID,
|
||||
Name: "my-peer-s1",
|
||||
DeletedAt: structs.TimeToProto(time.Now()),
|
||||
}
|
||||
require.NoError(t, s2.fsm.State().PeeringWrite(2000, deleted))
|
||||
s2.logger.Trace("deleted peering for my-peer-s1")
|
||||
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
|
@ -147,6 +157,11 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) {
|
|||
var token structs.PeeringToken
|
||||
require.NoError(t, json.Unmarshal(tokenJSON, &token))
|
||||
|
||||
var (
|
||||
s1PeerID = token.PeerID
|
||||
s2PeerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d"
|
||||
)
|
||||
|
||||
// Bring up s2 and store s1's token so that it attempts to dial.
|
||||
_, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.NodeName = "s2.dc2"
|
||||
|
@ -158,6 +173,7 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) {
|
|||
// Simulate a peering initiation event by writing a peering with data from a peering token.
|
||||
// Eventually the leader in dc2 should dial and connect to the leader in dc1.
|
||||
p := &pbpeering.Peering{
|
||||
ID: s2PeerID,
|
||||
Name: "my-peer-s1",
|
||||
PeerID: token.PeerID,
|
||||
PeerCAPems: token.CA,
|
||||
|
@ -175,10 +191,13 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) {
|
|||
require.True(r, status.Connected)
|
||||
})
|
||||
|
||||
// Delete the peering from the server peer to trigger the termination sequence
|
||||
require.NoError(t, s1.fsm.State().PeeringDelete(2000, state.Query{
|
||||
Value: "my-peer-s2",
|
||||
}))
|
||||
// Delete the peering from the server peer to trigger the termination sequence.
|
||||
deleted := &pbpeering.Peering{
|
||||
ID: s1PeerID,
|
||||
Name: "my-peer-s2",
|
||||
DeletedAt: structs.TimeToProto(time.Now()),
|
||||
}
|
||||
require.NoError(t, s1.fsm.State().PeeringWrite(2000, deleted))
|
||||
s2.logger.Trace("deleted peering for my-peer-s1")
|
||||
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
|
@ -186,7 +205,7 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) {
|
|||
require.False(r, found)
|
||||
})
|
||||
|
||||
// s2 should have received the termination message and updated the peering state
|
||||
// s2 should have received the termination message and updated the peering state.
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
_, peering, err := s2.fsm.State().PeeringRead(nil, state.Query{
|
||||
Value: "my-peer-s1",
|
||||
|
@ -195,3 +214,162 @@ func TestLeader_PeeringSync_Lifecycle_ServerDeletion(t *testing.T) {
|
|||
require.Equal(r, pbpeering.PeeringState_TERMINATED, peering.State)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLeader_Peering_DeferredDeletion(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
|
||||
// TODO(peering): Configure with TLS
|
||||
_, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.NodeName = "s1.dc1"
|
||||
c.Datacenter = "dc1"
|
||||
c.TLSConfig.Domain = "consul"
|
||||
})
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
var (
|
||||
peerID = "cc56f0b8-3885-4e78-8d7b-614a0c45712d"
|
||||
peerName = "my-peer-s2"
|
||||
defaultMeta = acl.DefaultEnterpriseMeta()
|
||||
lastIdx = uint64(0)
|
||||
)
|
||||
|
||||
// Simulate a peering initiation event by writing a peering to the state store.
|
||||
lastIdx++
|
||||
require.NoError(t, s1.fsm.State().PeeringWrite(lastIdx, &pbpeering.Peering{
|
||||
ID: peerID,
|
||||
Name: peerName,
|
||||
}))
|
||||
|
||||
// Insert imported data: nodes, services, checks, trust bundle
|
||||
lastIdx = insertTestPeeringData(t, s1.fsm.State(), peerName, lastIdx)
|
||||
|
||||
// Mark the peering for deletion to trigger the termination sequence.
|
||||
lastIdx++
|
||||
require.NoError(t, s1.fsm.State().PeeringWrite(lastIdx, &pbpeering.Peering{
|
||||
ID: peerID,
|
||||
Name: peerName,
|
||||
DeletedAt: structs.TimeToProto(time.Now()),
|
||||
}))
|
||||
|
||||
// Ensure imported data is gone:
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
_, csn, err := s1.fsm.State().ServiceDump(nil, "", false, defaultMeta, peerName)
|
||||
require.NoError(r, err)
|
||||
require.Len(r, csn, 0)
|
||||
|
||||
_, checks, err := s1.fsm.State().ChecksInState(nil, api.HealthAny, defaultMeta, peerName)
|
||||
require.NoError(r, err)
|
||||
require.Len(r, checks, 0)
|
||||
|
||||
_, nodes, err := s1.fsm.State().NodeDump(nil, defaultMeta, peerName)
|
||||
require.NoError(r, err)
|
||||
require.Len(r, nodes, 0)
|
||||
|
||||
_, tb, err := s1.fsm.State().PeeringTrustBundleRead(nil, state.Query{Value: peerName})
|
||||
require.NoError(r, err)
|
||||
require.Nil(r, tb)
|
||||
})
|
||||
|
||||
// The leader routine should pick up the deletion and finish deleting the peering.
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
_, peering, err := s1.fsm.State().PeeringRead(nil, state.Query{
|
||||
Value: peerName,
|
||||
})
|
||||
require.NoError(r, err)
|
||||
require.Nil(r, peering)
|
||||
})
|
||||
}
|
||||
|
||||
func insertTestPeeringData(t *testing.T, store *state.Store, peer string, lastIdx uint64) uint64 {
|
||||
lastIdx++
|
||||
require.NoError(t, store.PeeringTrustBundleWrite(lastIdx, &pbpeering.PeeringTrustBundle{
|
||||
TrustDomain: "952e6bd1-f4d6-47f7-83ff-84b31babaa17",
|
||||
PeerName: peer,
|
||||
RootPEMs: []string{"certificate bundle"},
|
||||
}))
|
||||
|
||||
lastIdx++
|
||||
require.NoError(t, store.EnsureRegistration(lastIdx, &structs.RegisterRequest{
|
||||
Node: "aaa",
|
||||
Address: "10.0.0.1",
|
||||
PeerName: peer,
|
||||
Service: &structs.NodeService{
|
||||
Service: "a-service",
|
||||
ID: "a-service-1",
|
||||
Port: 8080,
|
||||
PeerName: peer,
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
{
|
||||
CheckID: "a-service-1-check",
|
||||
ServiceName: "a-service",
|
||||
ServiceID: "a-service-1",
|
||||
Node: "aaa",
|
||||
PeerName: peer,
|
||||
},
|
||||
{
|
||||
CheckID: structs.SerfCheckID,
|
||||
Node: "aaa",
|
||||
PeerName: peer,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
lastIdx++
|
||||
require.NoError(t, store.EnsureRegistration(lastIdx, &structs.RegisterRequest{
|
||||
Node: "bbb",
|
||||
Address: "10.0.0.2",
|
||||
PeerName: peer,
|
||||
Service: &structs.NodeService{
|
||||
Service: "b-service",
|
||||
ID: "b-service-1",
|
||||
Port: 8080,
|
||||
PeerName: peer,
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
{
|
||||
CheckID: "b-service-1-check",
|
||||
ServiceName: "b-service",
|
||||
ServiceID: "b-service-1",
|
||||
Node: "bbb",
|
||||
PeerName: peer,
|
||||
},
|
||||
{
|
||||
CheckID: structs.SerfCheckID,
|
||||
Node: "bbb",
|
||||
PeerName: peer,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
lastIdx++
|
||||
require.NoError(t, store.EnsureRegistration(lastIdx, &structs.RegisterRequest{
|
||||
Node: "ccc",
|
||||
Address: "10.0.0.3",
|
||||
PeerName: peer,
|
||||
Service: &structs.NodeService{
|
||||
Service: "c-service",
|
||||
ID: "c-service-1",
|
||||
Port: 8080,
|
||||
PeerName: peer,
|
||||
},
|
||||
Checks: structs.HealthChecks{
|
||||
{
|
||||
CheckID: "c-service-1-check",
|
||||
ServiceName: "c-service",
|
||||
ServiceID: "c-service-1",
|
||||
Node: "ccc",
|
||||
PeerName: peer,
|
||||
},
|
||||
{
|
||||
CheckID: structs.SerfCheckID,
|
||||
Node: "ccc",
|
||||
PeerName: peer,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
return lastIdx
|
||||
}
|
||||
|
|
|
@ -2258,7 +2258,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
vip, err := state.VirtualIPForService(structs.NewServiceName("api", nil))
|
||||
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("api", nil)}
|
||||
vip, err := state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", vip)
|
||||
|
||||
|
@ -2287,7 +2288,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) {
|
|||
|
||||
// Make sure the service referenced in the terminating gateway config doesn't have
|
||||
// a virtual IP yet.
|
||||
vip, err = state.VirtualIPForService(structs.NewServiceName("bar", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("bar", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", vip)
|
||||
|
||||
|
@ -2316,8 +2318,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) {
|
|||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
vip, err = state.VirtualIPForService(structs.NewServiceName("api", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("api", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "240.0.0.1", vip)
|
||||
|
||||
|
@ -2345,7 +2347,8 @@ func TestLeader_EnableVirtualIPs(t *testing.T) {
|
|||
|
||||
// Make sure the baz service (only referenced in the config entry so far)
|
||||
// has a virtual IP.
|
||||
vip, err = state.VirtualIPForService(structs.NewServiceName("baz", nil))
|
||||
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("baz", nil)}
|
||||
vip, err = state.VirtualIPForService(psn)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "240.0.0.2", vip)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue