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:
Alessandro De Blasis 2022-08-15 08:09:56 +01:00
commit a131741ada
712 changed files with 33473 additions and 8404 deletions

3
.changelog/12399.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
catalog: Add per-node indexes to reduce watchset firing for unrelated nodes and services.
```

3
.changelog/13051.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
deps: Update go-grpc/grpc, resolving connection memory leak
```

4
.changelog/13357.txt Normal file
View File

@ -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.
```

3
.changelog/13394.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: upgrade ember-composable-helpers to v5.x
```

3
.changelog/13409.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fix incorrect text on certain page empty states
```

3
.changelog/13421.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
dns: Added support for specifying admin partition in node lookups.
```

3
.changelog/13431.txt Normal file
View File

@ -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)
```

3
.changelog/13450.txt Normal file
View File

@ -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).
```

4
.changelog/13481.txt Normal file
View File

@ -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.
```

3
.changelog/13532.txt Normal file
View File

@ -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.
```

3
.changelog/13607.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
xds: Fix a bug that resulted in Lambda services not using the payload-passthrough option as expected.
```

3
.changelog/13658.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
streaming: Added topics for `ingress-gateway`, `mesh`, `service-intentions` and `service-resolver` config entry events.
```

View File

@ -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}"

View File

@ -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 >>

View File

@ -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

View File

@ -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")

View File

@ -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"
;;

View File

@ -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 }}

View File

@ -5,7 +5,7 @@ container {
}
binary {
secrets = true
secrets = false
go_modules = false
osv = true
oss_index = true

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

27
acl/resolver/result.go Normal file
View File

@ -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()}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -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")
})
})
})
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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) })

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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
}

View File

@ -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{
{

View File

@ -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) })

View File

@ -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) })

View File

@ -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

View File

@ -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,

1
agent/cache/type.go vendored
View File

@ -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.
//

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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"`

View File

@ -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.

View File

@ -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 }

View File

@ -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{

View File

@ -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": [],

View File

@ -662,7 +662,6 @@ telemetry {
prometheus_retention_time = "15s"
statsd_address = "drce87cy"
statsite_address = "HpFwKB8R"
disable_compat_1.9 = true
}
tls {
defaults {

View File

@ -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": {

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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())
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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",

View File

@ -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
}

View File

@ -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 {

View File

@ -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",

View File

@ -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,

View File

@ -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 {

View File

@ -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)
}
})
}

View File

@ -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.

View File

@ -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

View File

@ -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 {

View File

@ -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) })

View File

@ -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) })

View File

@ -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) })

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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),

View File

@ -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",

View File

@ -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
}

View File

@ -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))
}

View File

@ -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")

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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() {

View File

@ -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.

View File

@ -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)
}

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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