Merge branch 'main' into southworks/qa-consul

pull/17694/head
Ashesh Vidyut 1 year ago committed by GitHub
commit 13e5433e1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,3 @@
```release-note:feature
cli: Adds new command - `consul services export` - for exporting a service to a peer or partition
```

@ -0,0 +1,3 @@
```release-note:improvement
systemd: set service type to notify.
```

@ -0,0 +1,3 @@
```release-note:improvement
agent: add new metrics to track cpu disk and memory usage for server hosts (defaults to: enabled)
```

@ -0,0 +1,3 @@
```release-note:bug
gateways: Fix an bug where targeting a virtual service defined by a service-resolver was broken for HTTPRoutes.
```

@ -1,3 +0,0 @@
```release-note:bug
connect: fix a bug with Envoy potentially starting with incomplete configuration by not waiting enough for initial xDS configuration.
```

@ -0,0 +1,3 @@
```release-note:feature
mesh: Support configuring JWT authentication in Envoy.
```

@ -0,0 +1,3 @@
```release-note:feature
xds: Add a built-in Envoy extension that inserts Wasm network filters.
```

@ -0,0 +1,3 @@
```release-note:security
Update to UBI base image to 9.2.
```

@ -0,0 +1,3 @@
```release-note:improvement
http: accept query parameters `datacenter`, `ap` (enterprise-only), and `namespace` (enterprise-only). Both short-hand and long-hand forms of these query params are now supported via the HTTP API (dc/datacenter, ap/partition, ns/namespace).
```

@ -0,0 +1,3 @@
```release-note:improvement
connect: update supported envoy versions to 1.23.10, 1.24.8, 1.25.7, 1.26.2
```

@ -0,0 +1,3 @@
```release-note:bug
xds: Fixed a bug where modifying ACLs on a token being actively used for an xDS connection caused all xDS updates to fail.
```

@ -0,0 +1,3 @@
```release-note:improvement
fix metric names in /docs/agent/telemetry
```

@ -0,0 +1,3 @@
```release-note:bug
gateways: **(Enterprise only)** Fixed a bug in API gateways where gateway configuration objects in non-default partitions did not reconcile properly.
```

@ -0,0 +1,3 @@
```release-note:bug
docs: fix list of telemetry metrics
```

@ -0,0 +1,3 @@
```release-note:improvement
debug: change default setting of consul debug command. now default duration is 5ms and default log level is 'TRACE'
```

@ -0,0 +1,4 @@
```release-note:bug
gateways: Fixed a bug in API gateways where binding a route that only targets a service imported from a peer results
in the programmed gateway having no routes.
```

@ -0,0 +1,3 @@
```release-note:bug
gateways: Fixed a bug where API gateways were not being taken into account in determining xDS rate limits.
```

@ -0,0 +1,3 @@
```release-note:feature
server: **(Enterprise Only)** added server side RPC requests IP based read/write rate-limiter.
```

@ -0,0 +1,3 @@
```release-note:feature
server: **(Enterprise Only)** allow automatic license utilization reporting.
```

@ -0,0 +1,3 @@
```release-note:improvement
audit-logging: **(Enterprise only)** enable error response and request body logging
```

@ -0,0 +1,3 @@
```release-note:feature
api: (Enterprise only) Add `POST /v1/operator/audit-hash` endpoint to calculate the hash of the data used by the audit log hash function and salt.
```

@ -0,0 +1,3 @@
```release-note:feature
cli: (Enterprise only) Add a new `consul operator audit hash` command to retrieve and compare the hash of the data used by the audit log hash function and salt.
```

@ -0,0 +1,3 @@
```release-note:security
audit-logging: **(Enterprise only)** limit `v1/operator/audit-hash` endpoint to ACL token with `operator:read` privileges.
```

@ -34,16 +34,16 @@ jobs:
run: |
CONSUL_DATE=$(build-support/scripts/build-date.sh)
## TODO: This assumes `make version` outputs 1.1.1+ent-prerel
echo "::set-output name=product-date::${CONSUL_DATE}"
echo "product-date=${CONSUL_DATE}" >> "$GITHUB_OUTPUT"
- name: Set shared -ldflags
id: shared-ldflags
run: |
T="github.com/hashicorp/consul/version"
echo "::set-output name=shared-ldflags::-X ${T}.GitCommit=${GITHUB_SHA::8} \
echo "shared-ldflags=-X ${T}.GitCommit=${GITHUB_SHA::8} \
-X ${T}.GitDescribe=${{ steps.set-product-version.outputs.product-version }} \
-X ${T}.BuildDate=${{ steps.get-product-version.outputs.product-date }} \
"
" >> "$GITHUB_OUTPUT"
validate-outputs:
needs: set-product-version
runs-on: ubuntu-latest

@ -1,20 +0,0 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
name: Legacy Link Format Checker
on:
push:
paths:
- "website/content/**/*.mdx"
- "website/data/*-nav-data.json"
jobs:
check-links:
uses: hashicorp/dev-portal/.github/workflows/docs-content-check-legacy-links-format.yml@475289345d312552b745224b46895f51cc5fc490
with:
repo-owner: "hashicorp"
repo-name: "consul"
commit-sha: ${{ github.sha }}
mdx-directory: "website/content"
nav-data-directory: "website/data"

@ -1,7 +1,7 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
name: Nightly Test 1.12.x
name: Nightly Test 1.16.x
on:
schedule:
- cron: '0 4 * * *'
@ -9,8 +9,8 @@ on:
env:
EMBER_PARTITION_TOTAL: 4 # Has to be changed in tandem with the matrix.partition
BRANCH: "release/1.12.x"
BRANCH_NAME: "release-1.12.x" # Used for naming artifacts
BRANCH: "release/1.16.x"
BRANCH_NAME: "release-1.16.x" # Used for naming artifacts
jobs:
frontend-test-workspace-node:

@ -239,7 +239,7 @@ jobs:
# this is further going to multiplied in envoy-integration tests by the
# other dimensions in the matrix. Currently TOTAL_RUNNERS would be
# multiplied by 8 based on these values:
# envoy-version: ["1.23.8", "1.24.6", "1.25.4", "1.26.0"]
# envoy-version: ["1.23.10", "1.24.8", "1.25.7", "1.26.2"]
# xds-target: ["server", "client"]
TOTAL_RUNNERS: 4
JQ_SLICER: '[ inputs ] | [_nwise(length / $runnercount | floor)]'
@ -273,7 +273,7 @@ jobs:
strategy:
fail-fast: false
matrix:
envoy-version: ["1.23.8", "1.24.6", "1.25.4", "1.26.0"]
envoy-version: ["1.23.10", "1.24.8", "1.25.7", "1.26.2"]
xds-target: ["server", "client"]
test-cases: ${{ fromJSON(needs.generate-envoy-job-matrices.outputs.envoy-matrix) }}
env:

2
.gitignore vendored

@ -66,3 +66,5 @@ override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
/go.work
/go.work.sum

@ -88,6 +88,9 @@ linters-settings:
- github.com/hashicorp/go-msgpack:
recommendations:
- github.com/hashicorp/consul-net-rpc/go-msgpack
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
depguard:
list-type: denylist
@ -101,7 +104,9 @@ linters-settings:
# Default: []
packages-with-error-message:
- net/rpc: "only use forked copy in github.com/hashicorp/consul-net-rpc/net/rpc"
- github.com/golang/protobuf: "only use google.golang.org/protobuf"
run:
timeout: 10m
concurrency: 4
skip-dirs-use-default: false

@ -6,6 +6,7 @@ After=network-online.target
ConditionFileNotEmpty=/etc/consul.d/consul.hcl
[Service]
Type=notify
EnvironmentFile=-/etc/consul.d/consul.env
User=consul
Group=consul

@ -1,3 +1,128 @@
## 1.16.0-rc1 (June 12, 2023)
BREAKING CHANGES:
* api: The `/v1/health/connect/` and `/v1/health/ingress/` endpoints now immediately return 403 "Permission Denied" errors whenever a token with insufficient `service:read` permissions is provided. Prior to this change, the endpoints returned a success code with an empty result list when a token with insufficient permissions was provided. [[GH-17424](https://github.com/hashicorp/consul/issues/17424)]
* peering: Removed deprecated backward-compatibility behavior.
Upstream overrides in service-defaults will now only apply to peer upstreams when the `peer` field is provided.
Visit the 1.16.x [upgrade instructions](https://developer.hashicorp.com/consul/docs/upgrading/upgrade-specific) for more information. [[GH-16957](https://github.com/hashicorp/consul/issues/16957)]
SECURITY:
* audit-logging: **(Enterprise only)** limit `v1/operator/audit-hash` endpoint to ACL token with `operator:read` privileges.
FEATURES:
* api: (Enterprise only) Add `POST /v1/operator/audit-hash` endpoint to calculate the hash of the data used by the audit log hash function and salt.
* cli: (Enterprise only) Add a new `consul operator audit hash` command to retrieve and compare the hash of the data used by the audit log hash function and salt.
* cli: Adds new command - `consul services export` - for exporting a service to a peer or partition [[GH-15654](https://github.com/hashicorp/consul/issues/15654)]
* connect: **(Consul Enterprise only)** Implement order-by-locality failover.
* mesh: Add new permissive mTLS mode that allows sidecar proxies to forward incoming traffic unmodified to the application. This adds `AllowEnablingPermissiveMutualTLS` setting to the mesh config entry and the `MutualTLSMode` setting to proxy-defaults and service-defaults. [[GH-17035](https://github.com/hashicorp/consul/issues/17035)]
* mesh: Support configuring JWT authentication in Envoy. [[GH-17452](https://github.com/hashicorp/consul/issues/17452)]
* server: **(Enterprise Only)** added server side RPC requests IP based read/write rate-limiter. [[GH-4633](https://github.com/hashicorp/consul/issues/4633)]
* server: **(Enterprise Only)** allow automatic license utilization reporting. [[GH-5102](https://github.com/hashicorp/consul/issues/5102)]
* server: added server side RPC requests global read/write rate-limiter. [[GH-16292](https://github.com/hashicorp/consul/issues/16292)]
* xds: Add `property-override` built-in Envoy extension that directly patches Envoy resources. [[GH-17487](https://github.com/hashicorp/consul/issues/17487)]
* xds: Add a built-in Envoy extension that inserts External Authorization (ext_authz) network and HTTP filters. [[GH-17495](https://github.com/hashicorp/consul/issues/17495)]
* xds: Add a built-in Envoy extension that inserts Wasm HTTP filters. [[GH-16877](https://github.com/hashicorp/consul/issues/16877)]
* xds: Add a built-in Envoy extension that inserts Wasm network filters. [[GH-17505](https://github.com/hashicorp/consul/issues/17505)]
IMPROVEMENTS:
* * api: Support filtering for config entries. [[GH-17183](https://github.com/hashicorp/consul/issues/17183)]
* * cli: Add `-filter` option to `consul config list` for filtering config entries. [[GH-17183](https://github.com/hashicorp/consul/issues/17183)]
* api: Enable setting query options on agent force-leave endpoint. [[GH-15987](https://github.com/hashicorp/consul/issues/15987)]
* audit-logging: (Enterprise only) enable error response and request body logging [[GH-5669](https://github.com/hashicorp/consul/issues/5669)]
* audit-logging: **(Enterprise only)** enable error response and request body logging
* ca: automatically set up Vault's auto-tidy setting for tidy_expired_issuers when using Vault as a CA provider. [[GH-17138](https://github.com/hashicorp/consul/issues/17138)]
* ca: support Vault agent auto-auth config for Vault CA provider using AliCloud authentication. [[GH-16224](https://github.com/hashicorp/consul/issues/16224)]
* ca: support Vault agent auto-auth config for Vault CA provider using AppRole authentication. [[GH-16259](https://github.com/hashicorp/consul/issues/16259)]
* ca: support Vault agent auto-auth config for Vault CA provider using Azure MSI authentication. [[GH-16298](https://github.com/hashicorp/consul/issues/16298)]
* ca: support Vault agent auto-auth config for Vault CA provider using JWT authentication. [[GH-16266](https://github.com/hashicorp/consul/issues/16266)]
* ca: support Vault agent auto-auth config for Vault CA provider using Kubernetes authentication. [[GH-16262](https://github.com/hashicorp/consul/issues/16262)]
* command: Adds ACL enabled to status output on agent startup. [[GH-17086](https://github.com/hashicorp/consul/issues/17086)]
* command: Allow creating ACL Token TTL with greater than 24 hours with the -expires-ttl flag. [[GH-17066](https://github.com/hashicorp/consul/issues/17066)]
* connect: **(Enterprise Only)** Add support for specifying "Partition" and "Namespace" in Prepared Queries failover rules.
* connect: update supported envoy versions to 1.23.10, 1.24.8, 1.25.7, 1.26.2 [[GH-17546](https://github.com/hashicorp/consul/issues/17546)]
* connect: update supported envoy versions to 1.23.8, 1.24.6, 1.25.4, 1.26.0 [[GH-5200](https://github.com/hashicorp/consul/issues/5200)]
* fix metric names in /docs/agent/telemetry [[GH-17577](https://github.com/hashicorp/consul/issues/17577)]
* gateway: Change status condition reason for invalid certificate on a listener from "Accepted" to "ResolvedRefs". [[GH-17115](https://github.com/hashicorp/consul/issues/17115)]
* http: accept query parameters `datacenter`, `ap` (enterprise-only), and `namespace` (enterprise-only). Both short-hand and long-hand forms of these query params are now supported via the HTTP API (dc/datacenter, ap/partition, ns/namespace). [[GH-17525](https://github.com/hashicorp/consul/issues/17525)]
* systemd: set service type to notify. [[GH-16845](https://github.com/hashicorp/consul/issues/16845)]
* ui: Update alerts to Hds::Alert component [[GH-16412](https://github.com/hashicorp/consul/issues/16412)]
* ui: Update to use Hds::Toast component to show notifications [[GH-16519](https://github.com/hashicorp/consul/issues/16519)]
* ui: update from <button> and <a> to design-system-components button <Hds::Button> [[GH-16251](https://github.com/hashicorp/consul/issues/16251)]
* ui: update typography to styles from hds [[GH-16577](https://github.com/hashicorp/consul/issues/16577)]
BUG FIXES:
* Fix a race condition where an event is published before the data associated is commited to memdb. [[GH-16871](https://github.com/hashicorp/consul/issues/16871)]
* gateways: **(Enterprise only)** Fixed a bug in API gateways where gateway configuration objects in non-default partitions did not reconcile properly. [[GH-17581](https://github.com/hashicorp/consul/issues/17581)]
* gateways: Fixed a bug in API gateways where binding a route that only targets a service imported from a peer results
in the programmed gateway having no routes. [[GH-17609](https://github.com/hashicorp/consul/issues/17609)]
* gateways: Fixed a bug where API gateways were not being taken into account in determining xDS rate limits. [[GH-17631](https://github.com/hashicorp/consul/issues/17631)]
* peering: Fixes a bug where the importing partition was not added to peered failover targets, which causes issues when the importing partition is a non-default partition. [[GH-16673](https://github.com/hashicorp/consul/issues/16673)]
* ui: fixes ui tests run on CI [[GH-16428](https://github.com/hashicorp/consul/issues/16428)]
* xds: Fixed a bug where modifying ACLs on a token being actively used for an xDS connection caused all xDS updates to fail. [[GH-17566](https://github.com/hashicorp/consul/issues/17566)]
## 1.15.3 (June 1, 2023)
BREAKING CHANGES:
* extensions: The Lua extension now targets local proxy listeners for the configured service's upstreams, rather than remote downstream listeners for the configured service, when ListenerType is set to outbound in extension configuration. See [CVE-2023-2816](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-2816) changelog entry for more details. [[GH-17415](https://github.com/hashicorp/consul/issues/17415)]
SECURITY:
* Update to UBI base image to 9.2. [[GH-17513](https://github.com/hashicorp/consul/issues/17513)]
* Upgrade golang.org/x/net to address [CVE-2022-41723](https://nvd.nist.gov/vuln/detail/CVE-2022-41723) [[GH-16754](https://github.com/hashicorp/consul/issues/16754)]
* Upgrade to use Go 1.20.4.
This resolves vulnerabilities [CVE-2023-24537](https://github.com/advisories/GHSA-9f7g-gqwh-jpf5)(`go/scanner`),
[CVE-2023-24538](https://github.com/advisories/GHSA-v4m2-x4rp-hv22)(`html/template`),
[CVE-2023-24534](https://github.com/advisories/GHSA-8v5j-pwr7-w5f8)(`net/textproto`) and
[CVE-2023-24536](https://github.com/advisories/GHSA-9f7g-gqwh-jpf5)(`mime/multipart`).
Also, `golang.org/x/net` has been updated to v0.7.0 to resolve CVEs [CVE-2022-41721
](https://github.com/advisories/GHSA-fxg5-wq6x-vr4w
), [CVE-2022-27664](https://github.com/advisories/GHSA-69cg-p879-7622) and [CVE-2022-41723
](https://github.com/advisories/GHSA-vvpx-j8f3-3w6h
.) [[GH-17240](https://github.com/hashicorp/consul/issues/17240)]
* extensions: Disable remote downstream proxy patching by Envoy Extensions other than AWS Lambda. Previously, an operator with service:write ACL permissions for an upstream service could modify Envoy proxy config for downstream services without equivalent permissions for those services. This issue only impacts the Lua extension. [[CVE-2023-2816](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-2816)] [[GH-17415](https://github.com/hashicorp/consul/issues/17415)]
FEATURES:
* hcp: Add new metrics sink to collect, aggregate and export server metrics to HCP in OTEL format. [[GH-17460](https://github.com/hashicorp/consul/issues/17460)]
IMPROVEMENTS:
* Fixes a performance issue in Raft where commit latency can increase by 100x or more when under heavy load. For more details see https://github.com/hashicorp/raft/pull/541. [[GH-17081](https://github.com/hashicorp/consul/issues/17081)]
* agent: add a configurable maximimum age (default: 7 days) to prevent servers re-joining a cluster with stale data [[GH-17171](https://github.com/hashicorp/consul/issues/17171)]
* agent: add new metrics to track cpu disk and memory usage for server hosts (defaults to: enabled) [[GH-17038](https://github.com/hashicorp/consul/issues/17038)]
* connect: update supported envoy versions to 1.22.11, 1.23.8, 1.24.6, 1.25.4 [[GH-16889](https://github.com/hashicorp/consul/issues/16889)]
* envoy: add `MaxEjectionPercent` and `BaseEjectionTime` to passive health check configs. [[GH-15979](https://github.com/hashicorp/consul/issues/15979)]
* hcp: Add support for linking existing Consul clusters to HCP management plane. [[GH-16916](https://github.com/hashicorp/consul/issues/16916)]
* logging: change snapshot log header from `agent.server.snapshot` to `agent.server.raft.snapshot` [[GH-17236](https://github.com/hashicorp/consul/issues/17236)]
* peering: allow re-establishing terminated peering from new token without deleting existing peering first. [[GH-16776](https://github.com/hashicorp/consul/issues/16776)]
* peering: gRPC queries for TrustBundleList, TrustBundleRead, PeeringList, and PeeringRead now support blocking semantics,
reducing network and CPU demand.
The HTTP APIs for Peering List and Read have been updated to support blocking. [[GH-17426](https://github.com/hashicorp/consul/issues/17426)]
* raft: Remove expensive reflection from raft/mesh hot path [[GH-16552](https://github.com/hashicorp/consul/issues/16552)]
* xds: rename envoy_hcp_metrics_bind_socket_dir to envoy_telemetry_collector_bind_socket_dir to remove HCP naming references. [[GH-17327](https://github.com/hashicorp/consul/issues/17327)]
BUG FIXES:
* Fix an bug where decoding some Config structs with unset pointer fields could fail with `reflect: call of reflect.Value.Type on zero Value`. [[GH-17048](https://github.com/hashicorp/consul/issues/17048)]
* acl: **(Enterprise only)** Check permissions in correct partition/namespace when resolving service in non-default partition/namespace
* acl: Fix an issue where the anonymous token was synthesized in non-primary datacenters which could cause permission errors when federating clusters with ACL replication enabled. [[GH-17231](https://github.com/hashicorp/consul/issues/17231)]
* acls: Fix ACL bug that can result in sidecar proxies having incorrect endpoints.
* connect: Fix multiple inefficient behaviors when querying service health. [[GH-17241](https://github.com/hashicorp/consul/issues/17241)]
* gateways: Fix an bug where targeting a virtual service defined by a service-resolver was broken for HTTPRoutes. [[GH-17055](https://github.com/hashicorp/consul/issues/17055)]
* grpc: ensure grpc resolver correctly uses lan/wan addresses on servers [[GH-17270](https://github.com/hashicorp/consul/issues/17270)]
* namespaces: adjusts the return type from HTTP list API to return the `api` module representation of a namespace.
This fixes an error with the `consul namespace list` command when a namespace has a deferred deletion timestamp.
* peering: Fix issue where modifying the list of exported services did not correctly replicate changes for services that exist in a non-default namespace. [[GH-17456](https://github.com/hashicorp/consul/issues/17456)]
* peering: Fix issue where peer streams could incorrectly deregister services in various scenarios. [[GH-17235](https://github.com/hashicorp/consul/issues/17235)]
* peering: ensure that merged central configs of peered upstreams for partitioned downstreams work [[GH-17179](https://github.com/hashicorp/consul/issues/17179)]
* xds: Fix possible panic that can when generating clusters before the root certificates have been fetched. [[GH-17185](https://github.com/hashicorp/consul/issues/17185)]
## 1.14.7 (May 16, 2023)
SECURITY:

@ -201,7 +201,7 @@ CMD ["agent", "-dev", "-client", "0.0.0.0"]
# Red Hat UBI-based image
# This target is used to build a Consul image for use on OpenShift.
FROM registry.access.redhat.com/ubi9-minimal:9.1.0 as ubi
FROM registry.access.redhat.com/ubi9-minimal:9.2 as ubi
ARG PRODUCT_NAME
ARG PRODUCT_VERSION

@ -3,6 +3,8 @@
SHELL = bash
GO_MODULES := $(shell find . -name go.mod -exec dirname {} \; | grep -v "proto-gen-rpc-glue/e2e" | sort)
###
# These version variables can either be a valid string for "go install <module>@<version>"
# or the string @DEV to imply use what is currently installed locally.
@ -249,19 +251,13 @@ cov: other-consul dev-build
test: other-consul dev-build lint test-internal
go-mod-tidy:
@echo "--> Running go mod tidy"
@cd sdk && go mod tidy
@cd api && go mod tidy
@go mod tidy
@cd test/integration/consul-container && go mod tidy
@cd test/integration/connect/envoy/test-sds-server && go mod tidy
@cd proto-public && go mod tidy
@cd internal/tools/proto-gen-rpc-glue && go mod tidy
@cd internal/tools/proto-gen-rpc-glue/e2e && go mod tidy
@cd internal/tools/proto-gen-rpc-glue/e2e/consul && go mod tidy
@cd internal/tools/protoc-gen-consul-rate-limit && go mod tidy
.PHONY: go-mod-tidy
go-mod-tidy: $(foreach mod,$(GO_MODULES),go-mod-tidy/$(mod))
.PHONY: mod-tidy/%
go-mod-tidy/%:
@echo "--> Running go mod tidy ($*)"
@cd $* && go mod tidy
test-internal:
@echo "--> Running go test"
@ -292,6 +288,12 @@ test-internal:
@grep '^FAIL' test.log || true
@if [ "$$(cat exit-code)" == "0" ] ; then echo "PASS" ; exit 0 ; else exit 1 ; fi
test-all: other-consul dev-build lint $(foreach mod,$(GO_MODULES),test-module/$(mod))
test-module/%:
@echo "--> Running go test ($*)"
cd $* && go test $(GOTEST_FLAGS) -tags '$(GOTAGS)' ./...
test-race:
$(MAKE) GOTEST_FLAGS=-race
@ -328,21 +330,26 @@ other-consul:
echo "Found other running consul agents. This may affect your tests." ; \
exit 1 ; \
fi
lint: -lint-main lint-container-test-deps
.PHONY: -lint-main
-lint-main: lint-tools
@echo "--> Running golangci-lint"
@golangci-lint run --build-tags '$(GOTAGS)' && \
(cd api && golangci-lint run --build-tags '$(GOTAGS)') && \
(cd sdk && golangci-lint run --build-tags '$(GOTAGS)')
@echo "--> Running golangci-lint (container tests)"
@cd test/integration/consul-container && golangci-lint run --build-tags '$(GOTAGS)'
@echo "--> Running lint-consul-retry"
@lint-consul-retry
@echo "--> Running enumcover"
@enumcover ./...
.PHONY: fmt
fmt: $(foreach mod,$(GO_MODULES),fmt/$(mod))
.PHONY: fmt/%
fmt/%:
@echo "--> Running go fmt ($*)"
@cd $* && gofmt -s -l -w .
.PHONY: lint
lint: $(foreach mod,$(GO_MODULES),lint/$(mod)) lint-container-test-deps
.PHONY: lint/%
lint/%:
@echo "--> Running golangci-lint ($*)"
@cd $* && GOWORK=off golangci-lint run --build-tags '$(GOTAGS)'
@echo "--> Running lint-consul-retry ($*)"
@cd $* && GOWORK=off lint-consul-retry
@echo "--> Running enumcover ($*)"
@cd $* && GOWORK=off enumcover ./...
.PHONY: lint-container-test-deps
lint-container-test-deps:

@ -597,6 +597,12 @@ func (a *Agent) Start(ctx context.Context) error {
// regular and on-demand state synchronizations (anti-entropy).
a.sync = ae.NewStateSyncer(a.State, c.AEInterval, a.shutdownCh, a.logger)
err = validateFIPSConfig(a.config)
if err != nil {
// Log warning, rather than force breaking
a.logger.Warn("FIPS 140-2 Compliance", "issue", err)
}
// create the config for the rpc server/client
consulCfg, err := newConsulConfig(a.config, a.logger)
if err != nil {
@ -1615,7 +1621,18 @@ func (a *Agent) RPC(ctx context.Context, method string, args interface{}, reply
method = e + "." + p[1]
}
}
// audit log only on consul clients
_, ok := a.delegate.(*consul.Client)
if ok {
a.writeAuditRPCEvent(method, "OperationStart")
}
a.endpointsLock.RUnlock()
defer func() {
a.writeAuditRPCEvent(method, "OperationComplete")
}()
return a.delegate.RPC(ctx, method, args, reply)
}
@ -4565,13 +4582,13 @@ func (a *Agent) persistServerMetadata() {
f, err := consul.OpenServerMetadata(file)
if err != nil {
a.logger.Error("failed to open existing server metadata: %w", err)
a.logger.Error("failed to open existing server metadata", "error", err)
continue
}
if err := consul.WriteServerMetadata(f); err != nil {
f.Close()
a.logger.Error("failed to write server metadata: %w", err)
a.logger.Error("failed to write server metadata", "error", err)
continue
}

@ -11,14 +11,12 @@ import (
"strings"
"time"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"github.com/mitchellh/hashstructure"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/serf/coordinate"
"github.com/hashicorp/serf/serf"
"github.com/mitchellh/hashstructure"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -29,11 +27,13 @@ import (
"github.com/hashicorp/consul/agent/structs"
token_store "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/logging/monitor"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/version"
)
type Self struct {
@ -1683,3 +1683,12 @@ func (s *HTTPHandlers) AgentHost(resp http.ResponseWriter, req *http.Request) (i
return debug.CollectHostInfo(), nil
}
// AgentVersion
//
// GET /v1/agent/version
//
// Retrieves Consul version information.
func (s *HTTPHandlers) AgentVersion(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return version.GetBuildInfo(), nil
}

@ -21,7 +21,6 @@ import (
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/serf/serf"
@ -41,12 +40,14 @@ import (
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/version"
)
func createACLTokenWithAgentReadPolicy(t *testing.T, srv *HTTPHandlers) string {
@ -1819,7 +1820,7 @@ func TestAgent_ReloadDoesNotTriggerWatch(t *testing.T) {
for i := 1; i < 7; i++ {
contents, err := os.ReadFile(tmpFile)
if err != nil {
t.Fatalf("should be able to read file, but had: %#v", err)
r.Fatalf("should be able to read file, but had: %#v", err)
}
contentsStr = string(contents)
if contentsStr != "" {
@ -1906,14 +1907,14 @@ func TestAgent_ReloadDoesNotTriggerWatch(t *testing.T) {
ensureNothingCritical(r, "red-is-dead")
if err := a.reloadConfigInternal(cfg2); err != nil {
t.Fatalf("got error %v want nil", err)
r.Fatalf("got error %v want nil", err)
}
// We check that reload does not go to critical
ensureNothingCritical(r, "red-is-dead")
ensureNothingCritical(r, "testing-agent-reload-001")
require.NoError(t, a.updateTTLCheck(checkID, api.HealthPassing, "testing-agent-reload-002"))
require.NoError(r, a.updateTTLCheck(checkID, api.HealthPassing, "testing-agent-reload-002"))
ensureNothingCritical(r, "red-is-dead")
})
@ -2923,7 +2924,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req, _ := http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(nodeCheck))
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusForbidden, resp.Code)
require.Equal(r, http.StatusForbidden, resp.Code)
})
})
@ -2933,7 +2934,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req.Header.Add("X-Consul-Token", svcToken.SecretID)
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusForbidden, resp.Code)
require.Equal(r, http.StatusForbidden, resp.Code)
})
})
@ -2943,7 +2944,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req.Header.Add("X-Consul-Token", nodeToken.SecretID)
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
require.Equal(r, http.StatusOK, resp.Code)
})
})
@ -2952,7 +2953,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req, _ := http.NewRequest("PUT", "/v1/agent/check/register", jsonReader(svcCheck))
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusForbidden, resp.Code)
require.Equal(r, http.StatusForbidden, resp.Code)
})
})
@ -2962,7 +2963,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req.Header.Add("X-Consul-Token", nodeToken.SecretID)
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusForbidden, resp.Code)
require.Equal(r, http.StatusForbidden, resp.Code)
})
})
@ -2972,7 +2973,7 @@ func TestAgent_RegisterCheck_ACLDeny(t *testing.T) {
req.Header.Add("X-Consul-Token", svcToken.SecretID)
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
require.Equal(r, http.StatusOK, resp.Code)
})
})
}
@ -5973,17 +5974,17 @@ func TestAgent_Monitor(t *testing.T) {
res := httptest.NewRecorder()
a.srv.h.ServeHTTP(res, registerReq)
if http.StatusOK != res.Code {
t.Fatalf("expected 200 but got %v", res.Code)
r.Fatalf("expected 200 but got %v", res.Code)
}
// Wait until we have received some type of logging output
require.Eventually(t, func() bool {
require.Eventually(r, func() bool {
return len(resp.Body.Bytes()) > 0
}, 3*time.Second, 100*time.Millisecond)
cancelFunc()
code := <-codeCh
require.Equal(t, http.StatusOK, code)
require.Equal(r, http.StatusOK, code)
got := resp.Body.String()
// Only check a substring that we are highly confident in finding
@ -6023,11 +6024,11 @@ func TestAgent_Monitor(t *testing.T) {
res := httptest.NewRecorder()
a.srv.h.ServeHTTP(res, registerReq)
if http.StatusOK != res.Code {
t.Fatalf("expected 200 but got %v", res.Code)
r.Fatalf("expected 200 but got %v", res.Code)
}
// Wait until we have received some type of logging output
require.Eventually(t, func() bool {
require.Eventually(r, func() bool {
return len(resp.Body.Bytes()) > 0
}, 3*time.Second, 100*time.Millisecond)
cancelFunc()
@ -6060,24 +6061,24 @@ func TestAgent_Monitor(t *testing.T) {
res := httptest.NewRecorder()
a.srv.h.ServeHTTP(res, registerReq)
if http.StatusOK != res.Code {
t.Fatalf("expected 200 but got %v", res.Code)
r.Fatalf("expected 200 but got %v", res.Code)
}
// Wait until we have received some type of logging output
require.Eventually(t, func() bool {
require.Eventually(r, func() bool {
return len(resp.Body.Bytes()) > 0
}, 3*time.Second, 100*time.Millisecond)
cancelFunc()
code := <-codeCh
require.Equal(t, http.StatusOK, code)
require.Equal(r, http.StatusOK, code)
// Each line is output as a separate JSON object, we grab the first and
// make sure it can be unmarshalled.
firstLine := bytes.Split(resp.Body.Bytes(), []byte("\n"))[0]
var output map[string]interface{}
if err := json.Unmarshal(firstLine, &output); err != nil {
t.Fatalf("err: %v", err)
r.Fatalf("err: %v", err)
}
})
})
@ -6669,7 +6670,7 @@ func TestAgentConnectCARoots_list(t *testing.T) {
dec := json.NewDecoder(resp.Body)
value := &structs.IndexedCARoots{}
require.NoError(t, dec.Decode(value))
require.NoError(r, dec.Decode(value))
if ca.ID != value.ActiveRootID {
r.Fatalf("%s != %s", ca.ID, value.ActiveRootID)
}
@ -7077,7 +7078,7 @@ func TestAgentConnectCALeafCert_goodNotLocal(t *testing.T) {
dec := json.NewDecoder(resp.Body)
issued2 := &structs.IssuedCert{}
require.NoError(t, dec.Decode(issued2))
require.NoError(r, dec.Decode(issued2))
if issued.CertPEM == issued2.CertPEM {
r.Fatalf("leaf has not updated")
}
@ -7089,9 +7090,9 @@ func TestAgentConnectCALeafCert_goodNotLocal(t *testing.T) {
}
// Verify that the cert is signed by the new CA
requireLeafValidUnderCA(t, issued2, ca)
requireLeafValidUnderCA(r, issued2, ca)
require.NotEqual(t, issued, issued2)
require.NotEqual(r, issued, issued2)
})
}
}
@ -7468,11 +7469,11 @@ func TestAgentConnectCALeafCert_secondaryDC_good(t *testing.T) {
// Try and sign again (note no index/wait arg since cache should update in
// background even if we aren't actively blocking)
a2.srv.h.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
require.Equal(r, http.StatusOK, resp.Code)
dec := json.NewDecoder(resp.Body)
issued2 := &structs.IssuedCert{}
require.NoError(t, dec.Decode(issued2))
require.NoError(r, dec.Decode(issued2))
if issued.CertPEM == issued2.CertPEM {
r.Fatalf("leaf has not updated")
}
@ -7484,9 +7485,9 @@ func TestAgentConnectCALeafCert_secondaryDC_good(t *testing.T) {
}
// Verify that the cert is signed by the new CA
requireLeafValidUnderCA(t, issued2, dc1_ca2)
requireLeafValidUnderCA(r, issued2, dc1_ca2)
require.NotEqual(t, issued, issued2)
require.NotEqual(r, issued, issued2)
})
}
@ -7496,12 +7497,12 @@ func waitForActiveCARoot(t *testing.T, srv *HTTPHandlers, expect *structs.CARoot
resp := httptest.NewRecorder()
srv.h.ServeHTTP(resp, req)
if http.StatusOK != resp.Code {
t.Fatalf("expected 200 but got %v", resp.Code)
r.Fatalf("expected 200 but got %v", resp.Code)
}
dec := json.NewDecoder(resp.Body)
roots := &structs.IndexedCARoots{}
require.NoError(t, dec.Decode(roots))
require.NoError(r, dec.Decode(roots))
var root *structs.CARoot
for _, r := range roots.Roots {
@ -8081,6 +8082,32 @@ func TestAgent_HostBadACL(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.Code)
}
func TestAgent_Version(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
dc1 := "dc1"
a := NewTestAgent(t, `
primary_datacenter = "`+dc1+`"
`)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
req, _ := http.NewRequest("GET", "/v1/agent/version", nil)
// req.Header.Add("X-Consul-Token", "initial-management")
resp := httptest.NewRecorder()
respRaw, err := a.srv.AgentVersion(resp, req)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.Code)
assert.NotNil(t, respRaw)
obj := respRaw.(*version.BuildInfo)
assert.NotNil(t, obj.HumanVersion)
}
// Thie tests that a proxy with an ExposeConfig is returned as expected.
func TestAgent_Services_ExposeConfig(t *testing.T) {
if testing.Short() {

@ -40,6 +40,11 @@ func (a *Agent) reloadEnterprise(conf *config.RuntimeConfig) error {
func enterpriseConsulConfig(_ *consul.Config, _ *config.RuntimeConfig) {
}
// validateFIPSConfig is a noop stub for the func defined in agent_ent.go
func validateFIPSConfig(_ *config.RuntimeConfig) error {
return nil
}
// WriteEvent is a noop stub for the func defined agent_ent.go
func (a *Agent) WriteEvent(eventType string, payload interface{}) {
}
@ -64,3 +69,7 @@ func (a *Agent) AgentEnterpriseMeta() *acl.EnterpriseMeta {
func (a *Agent) registerEntCache() {}
func (*Agent) fillEnterpriseProxyDataSources(*proxycfg.DataSources) {}
func (a *Agent) writeAuditRPCEvent(_ string, _ string) interface{} {
return nil
}

@ -828,6 +828,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
Version: stringVal(c.Version),
VersionPrerelease: stringVal(c.VersionPrerelease),
VersionMetadata: stringVal(c.VersionMetadata),
Experiments: c.Experiments,
// What is a sensible default for BuildDate?
BuildDate: timeValWithDefault(c.BuildDate, time.Date(1970, 1, 00, 00, 00, 01, 0, time.UTC)),
@ -1106,6 +1107,9 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
LocalProxyConfigResyncInterval: 30 * time.Second,
}
// host metrics are enabled by default if consul is configured with HashiCorp Cloud Platform integration
rt.Telemetry.EnableHostMetrics = boolValWithDefault(c.Telemetry.EnableHostMetrics, rt.IsCloudEnabled())
rt.TLS, err = b.buildTLSConfig(rt, c.TLS)
if err != nil {
return RuntimeConfig{}, err

@ -556,3 +556,22 @@ func TestBuilder_parsePrefixFilter(t *testing.T) {
}
})
}
func TestBuidler_hostMetricsWithCloud(t *testing.T) {
devMode := true
builderOpts := LoadOpts{
DevMode: &devMode,
DefaultConfig: FileSource{
Name: "test",
Format: "hcl",
Data: `cloud{ resource_id = "abc" client_id = "abc" client_secret = "abc"}`,
},
}
result, err := Load(builderOpts)
require.NoError(t, err)
require.Empty(t, result.Warnings)
cfg := result.RuntimeConfig
require.NotNil(t, cfg)
require.True(t, cfg.Telemetry.EnableHostMetrics)
}

@ -183,6 +183,7 @@ type Config struct {
EncryptKey *string `mapstructure:"encrypt" json:"encrypt,omitempty"`
EncryptVerifyIncoming *bool `mapstructure:"encrypt_verify_incoming" json:"encrypt_verify_incoming,omitempty"`
EncryptVerifyOutgoing *bool `mapstructure:"encrypt_verify_outgoing" json:"encrypt_verify_outgoing,omitempty"`
Experiments []string `mapstructure:"experiments" json:"experiments,omitempty"`
GossipLAN GossipLANConfig `mapstructure:"gossip_lan" json:"-"`
GossipWAN GossipWANConfig `mapstructure:"gossip_wan" json:"-"`
HTTPConfig HTTPConfig `mapstructure:"http_config" json:"-"`
@ -691,6 +692,7 @@ type Telemetry struct {
CirconusSubmissionInterval *string `mapstructure:"circonus_submission_interval" json:"circonus_submission_interval,omitempty"`
CirconusSubmissionURL *string `mapstructure:"circonus_submission_url" json:"circonus_submission_url,omitempty"`
DisableHostname *bool `mapstructure:"disable_hostname" json:"disable_hostname,omitempty"`
EnableHostMetrics *bool `mapstructure:"enable_host_metrics" json:"enable_host_metrics,omitempty"`
DogstatsdAddr *string `mapstructure:"dogstatsd_addr" json:"dogstatsd_addr,omitempty"`
DogstatsdTags []string `mapstructure:"dogstatsd_tags" json:"dogstatsd_tags,omitempty"`
RetryFailedConfiguration *bool `mapstructure:"retry_failed_connection" json:"retry_failed_connection,omitempty"`
@ -806,8 +808,9 @@ type ConfigEntries struct {
// Audit allows us to enable and define destinations for auditing
type Audit struct {
Enabled *bool `mapstructure:"enabled"`
Sinks map[string]AuditSink `mapstructure:"sink"`
Enabled *bool `mapstructure:"enabled"`
Sinks map[string]AuditSink `mapstructure:"sink"`
RPCEnabled *bool `mapstructure:"rpc_enabled"`
}
// AuditSink can be provided multiple times to define pipelines for auditing

@ -209,6 +209,9 @@ func DevSource() Source {
ports = {
grpc = 8502
}
experiments = [
"resource-apis"
]
`,
}
}

@ -1498,6 +1498,9 @@ type RuntimeConfig struct {
Reporting ReportingConfig
// List of experiments to enable
Experiments []string
EnterpriseRuntimeConfig
}

@ -46,6 +46,7 @@ type testCase struct {
desc string
args []string
setup func() // TODO: accept a testing.T instead of panic
cleanup func()
expected func(rt *RuntimeConfig)
expectedErr string
expectedWarnings []string
@ -324,6 +325,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
rt.DisableAnonymousSignature = true
rt.DisableKeyringFile = true
rt.EnableDebug = true
rt.Experiments = []string{"resource-apis"}
rt.UIConfig.Enabled = true
rt.LeaveOnTerm = false
rt.Logging.LogLevel = "DEBUG"
@ -2308,9 +2310,9 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
},
setup: func() {
os.Setenv("HCP_RESOURCE_ID", "env-id")
t.Cleanup(func() {
os.Unsetenv("HCP_RESOURCE_ID")
})
},
cleanup: func() {
os.Unsetenv("HCP_RESOURCE_ID")
},
expected: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
@ -2321,6 +2323,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
// server things
rt.ServerMode = true
rt.Telemetry.EnableHostMetrics = true
rt.TLS.ServerMode = true
rt.LeaveOnTerm = false
rt.SkipLeaveOnInt = true
@ -2337,9 +2340,9 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
},
setup: func() {
os.Setenv("HCP_RESOURCE_ID", "env-id")
t.Cleanup(func() {
os.Unsetenv("HCP_RESOURCE_ID")
})
},
cleanup: func() {
os.Unsetenv("HCP_RESOURCE_ID")
},
json: []string{`{
"cloud": {
@ -2360,6 +2363,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
// server things
rt.ServerMode = true
rt.Telemetry.EnableHostMetrics = true
rt.TLS.ServerMode = true
rt.LeaveOnTerm = false
rt.SkipLeaveOnInt = true
@ -6032,6 +6036,9 @@ func (tc testCase) run(format string, dataDir string) func(t *testing.T) {
expected.ACLResolverSettings.EnterpriseMeta = *structs.NodeEnterpriseMetaInPartition(expected.PartitionOrDefault())
prototest.AssertDeepEqual(t, expected, actual, cmpopts.EquateEmpty())
if tc.cleanup != nil {
tc.cleanup()
}
}
}
@ -6349,6 +6356,7 @@ func TestLoad_FullConfig(t *testing.T) {
EnableRemoteScriptChecks: true,
EnableLocalScriptChecks: true,
EncryptKey: "A4wELWqH",
Experiments: []string{"foo"},
StaticRuntimeConfig: StaticRuntimeConfig{
EncryptVerifyIncoming: true,
EncryptVerifyOutgoing: true,
@ -6754,6 +6762,7 @@ func TestLoad_FullConfig(t *testing.T) {
Expiration: 15 * time.Second,
Name: "ftO6DySn", // notice this is the same as the metrics prefix
},
EnableHostMetrics: true,
},
TLS: tlsutil.Config{
InternalRPC: tlsutil.ProtocolConfig{

@ -199,6 +199,7 @@
"EnableRemoteScriptChecks": false,
"EncryptKey": "hidden",
"EnterpriseRuntimeConfig": {},
"Experiments": [],
"ExposeMaxPort": 0,
"ExposeMinPort": 0,
"GRPCAddrs": [],
@ -465,6 +466,7 @@
"DisableHostname": false,
"DogstatsdAddr": "",
"DogstatsdTags": [],
"EnableHostMetrics": false,
"FilterDefault": false,
"MetricsPrefix": "",
"PrometheusOpts": {

@ -285,6 +285,9 @@ enable_syslog = true
encrypt = "A4wELWqH"
encrypt_verify_incoming = true
encrypt_verify_outgoing = true
experiments = [
"foo"
]
http_config {
block_endpoints = [ "RBvAFcGD", "fWOWFznh" ]
allow_write_http_from = [ "127.0.0.1/8", "22.33.44.55/32", "0.0.0.0/0" ]
@ -690,6 +693,7 @@ telemetry {
circonus_check_tags = "prvO4uBl"
circonus_submission_interval = "DolzaflP"
circonus_submission_url = "gTcbS93G"
enable_host_metrics = true
disable_hostname = true
dogstatsd_addr = "0wSndumK"
dogstatsd_tags = [ "3N81zSUB","Xtj8AnXZ" ]

@ -327,6 +327,9 @@
"encrypt": "A4wELWqH",
"encrypt_verify_incoming": true,
"encrypt_verify_outgoing": true,
"experiments": [
"foo"
],
"http_config": {
"block_endpoints": [
"RBvAFcGD",
@ -407,17 +410,17 @@
"raft_snapshot_interval": "30s",
"raft_trailing_logs": 83749,
"raft_logstore": {
"backend" : "wal",
"disable_log_cache": true,
"backend": "wal",
"disable_log_cache": true,
"verification": {
"enabled": true,
"interval":"12345s"
"enabled": true,
"interval": "12345s"
},
"boltdb": {
"no_freelist_sync": true
"no_freelist_sync": true
},
"wal": {
"segment_size_mb": 15
"segment_size_mb": 15
}
},
"read_replica": true,
@ -808,6 +811,7 @@
"circonus_check_tags": "prvO4uBl",
"circonus_submission_interval": "DolzaflP",
"circonus_submission_url": "gTcbS93G",
"enable_host_metrics": true,
"disable_hostname": true,
"dogstatsd_addr": "0wSndumK",
"dogstatsd_tags": [
@ -926,4 +930,4 @@
"xds": {
"update_max_per_second": 9526.2
}
}
}

@ -143,7 +143,7 @@ func TestACLEndpoint_ReplicationStatus(t *testing.T) {
retry.Run(t, func(r *retry.R) {
var status structs.ACLReplicationStatus
err := msgpackrpc.CallWithCodec(codec, "ACL.ReplicationStatus", &getR, &status)
require.NoError(t, err)
require.NoError(r, err)
require.True(r, status.Enabled)
require.True(r, status.Running)
@ -220,7 +220,7 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
time.Sleep(200 * time.Millisecond)
err := aclEp.TokenRead(&req, &resp)
require.Error(r, err)
require.ErrorContains(t, err, "ACL not found")
require.ErrorContains(r, err, "ACL not found")
require.Nil(r, resp.Token)
})
})

@ -179,7 +179,7 @@ func TestClient_LANReap(t *testing.T) {
retry.Run(t, func(r *retry.R) {
require.Len(r, c1.LANMembersInAgentPartition(), 1)
server := c1.router.FindLANServer()
require.Nil(t, server)
require.Nil(r, server)
})
}

@ -249,6 +249,13 @@ func targetForResolverNode(nodeName string, chains []*structs.CompiledDiscoveryC
splitterName := splitterPrefix + strings.TrimPrefix(nodeName, resolverPrefix)
for _, c := range chains {
targetChainPrefix := resolverPrefix + c.ServiceName + "."
if strings.HasPrefix(nodeName, targetChainPrefix) && len(c.Nodes) == 1 {
// we have a virtual resolver that just maps to another resolver, return
// the given node name
return c.StartNode
}
for name, node := range c.Nodes {
if node.IsSplitter() && strings.HasPrefix(splitterName, name) {
return name

@ -599,6 +599,118 @@ func TestGatewayChainSynthesizer_Synthesize(t *testing.T) {
},
}},
},
"HTTPRoute with virtual resolver": {
synthesizer: NewGatewayChainSynthesizer("dc1", "domain", "suffix", &structs.APIGatewayConfigEntry{
Kind: structs.APIGateway,
Name: "gateway",
}),
httpRoutes: []*structs.HTTPRouteConfigEntry{
{
Kind: structs.HTTPRoute,
Name: "http-route",
Rules: []structs.HTTPRouteRule{{
Services: []structs.HTTPService{{
Name: "foo",
}},
}},
},
},
chain: &structs.CompiledDiscoveryChain{
ServiceName: "foo",
Namespace: "default",
Datacenter: "dc1",
StartNode: "resolver:foo-2.default.default.dc2",
Nodes: map[string]*structs.DiscoveryGraphNode{
"resolver:foo-2.default.default.dc2": {
Type: "resolver",
Name: "foo-2.default.default.dc2",
Resolver: &structs.DiscoveryResolver{
Target: "foo-2.default.default.dc2",
Default: true,
ConnectTimeout: 5000000000,
},
},
},
},
extra: []*structs.CompiledDiscoveryChain{},
expectedIngressServices: []structs.IngressService{{
Name: "gateway-suffix-9b9265b",
Hosts: []string{"*"},
}},
expectedDiscoveryChains: []*structs.CompiledDiscoveryChain{{
ServiceName: "gateway-suffix-9b9265b",
Partition: "default",
Namespace: "default",
Datacenter: "dc1",
Protocol: "http",
StartNode: "router:gateway-suffix-9b9265b.default.default",
Nodes: map[string]*structs.DiscoveryGraphNode{
"router:gateway-suffix-9b9265b.default.default": {
Type: "router",
Name: "gateway-suffix-9b9265b.default.default",
Routes: []*structs.DiscoveryRoute{{
Definition: &structs.ServiceRoute{
Match: &structs.ServiceRouteMatch{
HTTP: &structs.ServiceRouteHTTPMatch{
PathPrefix: "/",
},
},
Destination: &structs.ServiceRouteDestination{
Service: "foo",
Partition: "default",
Namespace: "default",
RequestHeaders: &structs.HTTPHeaderModifiers{
Add: make(map[string]string),
Set: make(map[string]string),
},
},
},
NextNode: "resolver:foo-2.default.default.dc2",
}},
},
"resolver:foo.default.default.dc1": {
Type: "resolver",
Name: "foo.default.default.dc1",
Resolver: &structs.DiscoveryResolver{
Target: "foo.default.default.dc1",
Default: true,
ConnectTimeout: 5000000000,
},
},
"resolver:foo-2.default.default.dc2": {
Type: "resolver",
Name: "foo-2.default.default.dc2",
Resolver: &structs.DiscoveryResolver{
Target: "foo-2.default.default.dc2",
Default: true,
ConnectTimeout: 5000000000,
},
},
},
Targets: map[string]*structs.DiscoveryTarget{
"gateway-suffix-9b9265b.default.default.dc1": {
ID: "gateway-suffix-9b9265b.default.default.dc1",
Service: "gateway-suffix-9b9265b",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "gateway-suffix-9b9265b.default.dc1.internal.domain",
Name: "gateway-suffix-9b9265b.default.dc1.internal.domain",
},
"foo.default.default.dc1": {
ID: "foo.default.default.dc1",
Service: "foo",
Datacenter: "dc1",
Partition: "default",
Namespace: "default",
ConnectTimeout: 5000000000,
SNI: "foo.default.dc1.internal.domain",
Name: "foo.default.dc1.internal.domain",
},
},
}},
},
}
for name, tc := range cases {

@ -44,6 +44,8 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
StorageBackend: storageBackend,
})
fsm.state.SystemMetadataSet(10, &structs.SystemMetadataEntry{Key: structs.SystemMetadataVirtualIPsEnabled, Value: "true"})
// Add some state
node1 := &structs.Node{
ID: "610918a6-464f-fa9b-1a95-03bd6e88ed92",
@ -79,8 +81,14 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
Connect: connectConf,
})
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("web", nil)}
vip, err := fsm.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.1")
fsm.state.EnsureService(4, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: []string{"primary"}, Address: "127.0.0.1", Port: 5000})
fsm.state.EnsureService(5, "baz", &structs.NodeService{ID: "web", Service: "web", Tags: nil, Address: "127.0.0.2", Port: 80})
fsm.state.EnsureService(6, "baz", &structs.NodeService{ID: "db", Service: "db", Tags: []string{"secondary"}, Address: "127.0.0.2", Port: 5000})
fsm.state.EnsureCheck(7, &structs.HealthCheck{
Node: "foo",
@ -442,6 +450,10 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
},
}
require.NoError(t, fsm.state.EnsureConfigEntry(26, serviceIxn))
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", nil)}
vip, err = fsm.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.2")
// mesh config entry
meshConfig := &structs.MeshConfigEntry{
@ -465,10 +477,10 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
Port: 8000,
Connect: connectConf,
})
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)}
vip, err := fsm.state.VirtualIPForService(psn)
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")
require.Equal(t, vip, "240.0.0.3")
fsm.state.EnsureService(30, "foo", &structs.NodeService{
ID: "backend",
@ -480,7 +492,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
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")
require.Equal(t, vip, "240.0.0.4")
_, serviceNames, err := fsm.state.ServiceNamesOfKind(nil, structs.ServiceKindTypical)
require.NoError(t, err)
@ -534,15 +546,15 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
},
}))
// Add a service-resolver entry to get a virtual IP for service foo
// Add a service-resolver entry to get a virtual IP for service goo
resolverEntry := &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: "foo",
Name: "goo",
}
require.NoError(t, fsm.state.EnsureConfigEntry(34, resolverEntry))
vip, err = fsm.state.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", nil)})
vip, err = fsm.state.VirtualIPForService(structs.PeeredServiceName{ServiceName: structs.NewServiceName("goo", nil)})
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.3")
require.Equal(t, vip, "240.0.0.5")
// Resources
resource, err := storageBackend.WriteCAS(context.Background(), &pbresource.Resource{
@ -665,18 +677,26 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
require.Equal(t, uint64(25), checks[0].ModifyIndex)
// Verify virtual IPs are consistent.
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("frontend", nil)}
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("web", nil)}
vip, err = fsm2.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.1")
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("backend", nil)}
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", nil)}
vip, err = fsm2.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.2")
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("foo", 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.3")
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.4")
psn = structs.PeeredServiceName{ServiceName: structs.NewServiceName("goo", nil)}
vip, err = fsm2.state.VirtualIPForService(psn)
require.NoError(t, err)
require.Equal(t, vip, "240.0.0.5")
// Verify key is set
_, d, err := fsm2.state.KVSGet(nil, "/test", nil)

@ -93,7 +93,7 @@ func (r *apiGatewayReconciler) enqueueCertificateReferencedGateways(store *state
logger.Trace("certificate changed, enqueueing dependent gateways")
defer logger.Trace("finished enqueuing gateways")
_, entries, err := store.ConfigEntriesByKind(nil, structs.APIGateway, acl.WildcardEnterpriseMeta())
_, entries, err := store.ConfigEntriesByKind(nil, structs.APIGateway, wildcardMeta())
if err != nil {
logger.Warn("error retrieving api gateways", "error", err)
return err
@ -564,11 +564,11 @@ type gatewayMeta struct {
// tuples based on the state coming from the store. Any gateway that does not have
// a corresponding bound-api-gateway config entry will be filtered out.
func getAllGatewayMeta(store *state.Store) ([]*gatewayMeta, error) {
_, gateways, err := store.ConfigEntriesByKind(nil, structs.APIGateway, acl.WildcardEnterpriseMeta())
_, gateways, err := store.ConfigEntriesByKind(nil, structs.APIGateway, wildcardMeta())
if err != nil {
return nil, err
}
_, boundGateways, err := store.ConfigEntriesByKind(nil, structs.BoundAPIGateway, acl.WildcardEnterpriseMeta())
_, boundGateways, err := store.ConfigEntriesByKind(nil, structs.BoundAPIGateway, wildcardMeta())
if err != nil {
return nil, err
}
@ -1074,12 +1074,12 @@ func requestToResourceRef(req controller.Request) structs.ResourceReference {
// retrieveAllRoutesFromStore retrieves all HTTP and TCP routes from the given store
func retrieveAllRoutesFromStore(store *state.Store) ([]structs.BoundRoute, error) {
_, httpRoutes, err := store.ConfigEntriesByKind(nil, structs.HTTPRoute, acl.WildcardEnterpriseMeta())
_, httpRoutes, err := store.ConfigEntriesByKind(nil, structs.HTTPRoute, wildcardMeta())
if err != nil {
return nil, err
}
_, tcpRoutes, err := store.ConfigEntriesByKind(nil, structs.TCPRoute, acl.WildcardEnterpriseMeta())
_, tcpRoutes, err := store.ConfigEntriesByKind(nil, structs.TCPRoute, wildcardMeta())
if err != nil {
return nil, err
}
@ -1141,3 +1141,9 @@ func routeLogger(logger hclog.Logger, route structs.ConfigEntry) hclog.Logger {
meta := route.GetEnterpriseMeta()
return logger.With("route.kind", route.GetKind(), "route.name", route.GetName(), "route.namespace", meta.NamespaceOrDefault(), "route.partition", meta.PartitionOrDefault())
}
func wildcardMeta() *acl.EnterpriseMeta {
meta := acl.WildcardEnterpriseMeta()
meta.OverridePartition(acl.WildcardPartitionName)
return meta
}

@ -1921,7 +1921,7 @@ func Test_Leader_PeeringSync_ServerAddressUpdates(t *testing.T) {
require.True(r, found)
// We assert for this error to be set which would indicate that we iterated
// through a bad address.
require.Contains(r, status.LastSendErrorMessage, "transport: Error while dialing dial tcp: address bad: missing port in address")
require.Contains(r, status.LastSendErrorMessage, "transport: Error while dialing: dial tcp: address bad: missing port in address")
require.False(r, status.Connected)
})
})

@ -13,6 +13,10 @@ import (
)
func (md *lanMergeDelegate) enterpriseNotifyMergeMember(m *serf.Member) error {
if memberFIPS := m.Tags["fips"]; memberFIPS != "" {
return fmt.Errorf("Member '%s' is FIPS Consul; FIPS Consul is only available in Consul Enterprise",
m.Name)
}
if memberPartition := m.Tags["ap"]; memberPartition != "" {
return fmt.Errorf("Member '%s' part of partition '%s'; Partitions are a Consul Enterprise feature",
m.Name, memberPartition)

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/consul/version"
)
func TestMerge_LAN(t *testing.T) {
@ -282,6 +283,7 @@ func makeTestNode(t *testing.T, tm testMember) *serf.Member {
"vsn": "2",
"vsn_max": "3",
"vsn_min": "2",
"fips": version.GetFIPSInfo(),
},
}
if tm.partition != "" {

@ -95,7 +95,7 @@ func TestRateLimiterCleanup(t *testing.T) {
retry.RunWith(&retry.Timer{Wait: 100 * time.Millisecond, Timeout: 10 * time.Second}, t, func(r *retry.R) {
v, ok := limiters.Get(key)
require.True(r, ok)
require.NotNil(t, v)
require.NotNil(r, v)
})
time.Sleep(c.ReconcileCheckInterval)

@ -39,6 +39,8 @@ type Deps struct {
// HCP contains the dependencies required when integrating with the HashiCorp Cloud Platform
HCP hcp.Deps
Experiments []string
EnterpriseDeps
}

@ -1539,8 +1539,7 @@ func TestPreparedQuery_Execute(t *testing.T) {
assert.Len(t, reply.Nodes, 0)
})
expectNodes := func(t *testing.T, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
t.Helper()
expectNodes := func(t require.TestingT, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
assert.Len(t, reply.Nodes, n)
assert.Equal(t, "dc1", reply.Datacenter)
assert.Equal(t, 0, reply.Failovers)
@ -1548,8 +1547,7 @@ func TestPreparedQuery_Execute(t *testing.T) {
assert.Equal(t, query.Query.DNS, reply.DNS)
assert.True(t, reply.QueryMeta.KnownLeader)
}
expectFailoverNodes := func(t *testing.T, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
t.Helper()
expectFailoverNodes := func(t require.TestingT, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
assert.Len(t, reply.Nodes, n)
assert.Equal(t, "dc2", reply.Datacenter)
assert.Equal(t, 1, reply.Failovers)
@ -1558,8 +1556,7 @@ func TestPreparedQuery_Execute(t *testing.T) {
assert.True(t, reply.QueryMeta.KnownLeader)
}
expectFailoverPeerNodes := func(t *testing.T, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
t.Helper()
expectFailoverPeerNodes := func(t require.TestingT, query *structs.PreparedQueryRequest, reply *structs.PreparedQueryExecuteResponse, n int) {
assert.Len(t, reply.Nodes, n)
assert.Equal(t, "", reply.Datacenter)
assert.Equal(t, es.peeringServer.acceptingPeerName, reply.PeerName)
@ -2372,13 +2369,13 @@ func TestPreparedQuery_Execute(t *testing.T) {
}
var reply structs.PreparedQueryExecuteResponse
require.NoError(t, msgpackrpc.CallWithCodec(es.server.codec, "PreparedQuery.Execute", &req, &reply))
require.NoError(r, msgpackrpc.CallWithCodec(es.server.codec, "PreparedQuery.Execute", &req, &reply))
for _, node := range reply.Nodes {
assert.NotEqual(t, "node3", node.Node.Node)
assert.NotEqual(r, "node3", node.Node.Node)
}
expectNodes(t, &query, &reply, 9)
expectNodes(r, &query, &reply, 9)
})
})
}

@ -119,15 +119,15 @@ const (
OperationCategoryKV OperationCategory = "KV"
OperationCategoryPreparedQuery OperationCategory = "PreparedQuery"
OperationCategorySession OperationCategory = "Session"
OperationCategoryStatus OperationCategory = "Status"
OperationCategoryStatus OperationCategory = "Status" // not limited
OperationCategoryTxn OperationCategory = "Txn"
OperationCategoryAutoConfig OperationCategory = "AutoConfig"
OperationCategoryFederationState OperationCategory = "FederationState"
OperationCategoryInternal OperationCategory = "Internal"
OperationCategoryOperator OperationCategory = "Operator"
OperationCategoryOperator OperationCategory = "Operator" // not limited
OperationCategoryPeerStream OperationCategory = "PeerStream"
OperationCategoryPeering OperationCategory = "Peering"
OperationCategoryPartition OperationCategory = "Partition"
OperationCategoryPartition OperationCategory = "Tenancy"
OperationCategoryDataPlane OperationCategory = "DataPlane"
OperationCategoryDNS OperationCategory = "DNS"
OperationCategorySubscribe OperationCategory = "Subscribe"

@ -79,6 +79,7 @@ import (
raftstorage "github.com/hashicorp/consul/internal/storage/raft"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/routine"
"github.com/hashicorp/consul/lib/stringslice"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/pbsubscribe"
@ -131,6 +132,8 @@ const (
reconcileChSize = 256
LeaderTransferMinVersion = "1.6.0"
catalogResourceExperimentName = "resource-apis"
)
const (
@ -807,7 +810,7 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server, incom
s.internalResourceServiceClient,
logger.Named(logging.ControllerRuntime),
)
s.registerResources()
s.registerResources(flat)
go s.controllerManager.Run(&lib.StopChannelContext{StopCh: shutdownCh})
go s.trackLeaderChanges()
@ -858,11 +861,14 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server, incom
return s, nil
}
func (s *Server) registerResources() {
catalog.RegisterTypes(s.typeRegistry)
catalog.RegisterControllers(s.controllerManager, catalog.DefaultControllerDependencies())
func (s *Server) registerResources(deps Deps) {
if stringslice.Contains(deps.Experiments, catalogResourceExperimentName) {
catalog.RegisterTypes(s.typeRegistry)
catalog.RegisterControllers(s.controllerManager, catalog.DefaultControllerDependencies())
mesh.RegisterTypes(s.typeRegistry)
}
mesh.RegisterTypes(s.typeRegistry)
reaper.RegisterControllers(s.controllerManager)
if s.config.DevMode {
@ -1679,12 +1685,18 @@ func (s *Server) IsLeader() bool {
// IsServer checks if this addr is of a server
func (s *Server) IsServer(addr string) bool {
for _, s := range s.raft.GetConfiguration().Configuration().Servers {
a, err := net.ResolveTCPAddr("tcp", string(s.Address))
for _, ss := range s.raft.GetConfiguration().Configuration().Servers {
a, err := net.ResolveTCPAddr("tcp", string(ss.Address))
if err != nil {
continue
}
localIP, err := net.ResolveTCPAddr("tcp", string(s.config.RaftConfig.LocalID))
if err != nil {
continue
}
if string(metadata.GetIP(a)) == addr {
// only return true if it's another server and not our local address
if string(metadata.GetIP(a)) == addr && string(metadata.GetIP(localIP)) != addr {
return true
}
}

@ -915,7 +915,7 @@ func ensureServiceTxn(tx WriteTxn, idx uint64, node string, preserveIndexes bool
if err != nil {
return err
}
if supported {
if supported && sn.Name != "" {
psn := structs.PeeredServiceName{Peer: svc.PeerName, ServiceName: sn}
vip, err := assignServiceVirtualIP(tx, idx, psn)
if err != nil {
@ -2110,7 +2110,13 @@ func freeServiceVirtualIP(
// Don't deregister the virtual IP if at least one resolver/router/splitter config entry still
// references this service.
configEntryVIPKinds := []string{structs.ServiceResolver, structs.ServiceRouter, structs.ServiceSplitter}
configEntryVIPKinds := []string{
structs.ServiceResolver,
structs.ServiceRouter,
structs.ServiceSplitter,
structs.ServiceDefaults,
structs.ServiceIntentions,
}
for _, kind := range configEntryVIPKinds {
_, entry, err := configEntryTxn(tx, nil, kind, psn.ServiceName.Name, &psn.ServiceName.EnterpriseMeta)
if err != nil {
@ -3051,14 +3057,15 @@ func (s *Store) ServiceVirtualIPs() (uint64, []ServiceVirtualIP, error) {
tx := s.db.Txn(false)
defer tx.Abort()
return servicesVirtualIPsTxn(tx)
return servicesVirtualIPsTxn(tx, nil)
}
func servicesVirtualIPsTxn(tx ReadTxn) (uint64, []ServiceVirtualIP, error) {
func servicesVirtualIPsTxn(tx ReadTxn, ws memdb.WatchSet) (uint64, []ServiceVirtualIP, error) {
iter, err := tx.Get(tableServiceVirtualIPs, indexID)
if err != nil {
return 0, nil, err
}
ws.Add(iter.WatchCh())
var vips []ServiceVirtualIP
for raw := iter.Next(); raw != nil; raw = iter.Next() {

@ -6,6 +6,7 @@ package state
import (
"errors"
"fmt"
"strings"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-multierror"
@ -465,9 +466,8 @@ func deleteConfigEntryTxn(tx WriteTxn, idx uint64, kind, name string, entMeta *a
return fmt.Errorf("failed updating index: %s", err)
}
// If this is a resolver/router/splitter, attempt to delete the virtual IP associated
// with this service.
if kind == structs.ServiceResolver || kind == structs.ServiceRouter || kind == structs.ServiceSplitter {
// Attempt to delete the virtual IP associated with this service, if applicable.
if configEntryHasVirtualIP(c) {
psn := structs.PeeredServiceName{ServiceName: sn}
if err := freeServiceVirtualIP(tx, idx, psn, nil); err != nil {
return fmt.Errorf("failed to clean up virtual IP for %q: %v", psn.String(), err)
@ -519,11 +519,14 @@ func insertConfigEntryWithTxn(tx WriteTxn, idx uint64, conf structs.ConfigEntry)
if err != nil {
return err
}
case structs.ServiceResolver:
fallthrough
case structs.ServiceRouter:
fallthrough
case structs.ServiceSplitter:
}
// Assign virtual-ips, if needed
supported, err := virtualIPsSupported(tx, nil)
if err != nil {
return err
}
if supported && configEntryHasVirtualIP(conf) {
psn := structs.PeeredServiceName{ServiceName: structs.NewServiceName(conf.GetName(), conf.GetEnterpriseMeta())}
if _, err := assignServiceVirtualIP(tx, idx, psn); err != nil {
return err
@ -541,6 +544,28 @@ func insertConfigEntryWithTxn(tx WriteTxn, idx uint64, conf structs.ConfigEntry)
return nil
}
func configEntryHasVirtualIP(c structs.ConfigEntry) bool {
if c == nil || c.GetName() == "" {
return false
}
switch c.GetKind() {
case structs.ServiceRouter:
return true
case structs.ServiceResolver:
return true
case structs.ServiceSplitter:
return true
case structs.ServiceDefaults:
return true
case structs.ServiceIntentions:
entMeta := c.GetEnterpriseMeta()
return !strings.Contains(c.GetName(), "*") &&
!strings.Contains(entMeta.NamespaceOrDefault(), "*") &&
!strings.Contains(entMeta.PartitionOrDefault(), "*")
}
return false
}
// validateProposedConfigEntryInGraph can be used to verify graph integrity for
// a proposed graph create/update/delete.
//

@ -1106,7 +1106,7 @@ func (s *Store) intentionTopologyTxn(
// We only need to do this for upstreams currently, so that tproxy can find which discovery chains should be
// contacted for failover scenarios. Virtual services technically don't need to be considered as downstreams,
// because they will take on the identity of the calling service, rather than the chain itself.
vipIndex, vipServices, err := servicesVirtualIPsTxn(tx)
vipIndex, vipServices, err := servicesVirtualIPsTxn(tx, ws)
if err != nil {
return index, nil, fmt.Errorf("failed to list service virtual IPs: %v", err)
}

@ -2097,6 +2097,7 @@ func disableLegacyIntentions(s *Store) error {
func testConfigStateStore(t *testing.T) *Store {
s := testStateStore(t)
s.SystemMetadataSet(5, &structs.SystemMetadataEntry{Key: structs.SystemMetadataVirtualIPsEnabled, Value: "true"})
disableLegacyIntentions(s)
return s
}
@ -2651,6 +2652,7 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
func TestStore_IntentionTopology_Watches(t *testing.T) {
s := testConfigStateStore(t)
s.SystemMetadataSet(10, &structs.SystemMetadataEntry{Key: structs.SystemMetadataVirtualIPsEnabled, Value: "true"})
var i uint64 = 1
require.NoError(t, s.EnsureNode(i, &structs.Node{
@ -2687,7 +2689,8 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny, structs.IntentionTargetService)
require.NoError(t, err)
require.Equal(t, uint64(2), index)
require.Empty(t, got)
// Because API is a virtual service, it is included in this output.
require.Equal(t, structs.ServiceList{structs.NewServiceName("api", nil)}, got)
// Watch should not fire after unrelated intention changes
require.NoError(t, s.EnsureConfigEntry(i, &structs.ServiceIntentionsConfigEntry{
@ -2701,7 +2704,6 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
},
}))
i++
// TODO(freddy) Why is this firing?
// require.False(t, watchFired(ws))
@ -2709,7 +2711,7 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny, structs.IntentionTargetService)
require.NoError(t, err)
require.Equal(t, uint64(3), index)
require.Empty(t, got)
require.Equal(t, structs.ServiceList{structs.NewServiceName("api", nil)}, got)
// Watch should fire after service list changes
require.NoError(t, s.EnsureService(i, "foo", &structs.NodeService{

@ -203,11 +203,27 @@ func testRegisterConnectService(t *testing.T, s *Store, idx uint64, nodeID, serv
})
}
func testRegisterAPIService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) {
testRegisterGatewayService(t, s, structs.ServiceKindAPIGateway, idx, nodeID, serviceID)
}
func testRegisterTerminatingService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) {
testRegisterGatewayService(t, s, structs.ServiceKindTerminatingGateway, idx, nodeID, serviceID)
}
func testRegisterIngressService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) {
testRegisterGatewayService(t, s, structs.ServiceKindIngressGateway, idx, nodeID, serviceID)
}
func testRegisterMeshService(t *testing.T, s *Store, idx uint64, nodeID, serviceID string) {
testRegisterGatewayService(t, s, structs.ServiceKindMeshGateway, idx, nodeID, serviceID)
}
func testRegisterGatewayService(t *testing.T, s *Store, kind structs.ServiceKind, idx uint64, nodeID, serviceID string) {
svc := &structs.NodeService{
ID: serviceID,
Service: serviceID,
Kind: structs.ServiceKindIngressGateway,
Kind: kind,
Address: "1.1.1.1",
Port: 1111,
}
@ -227,6 +243,7 @@ func testRegisterIngressService(t *testing.T, s *Store, idx uint64, nodeID, serv
t.Fatalf("bad service: %#v", result)
}
}
func testRegisterCheck(t *testing.T, s *Store, idx uint64,
nodeID string, serviceID string, checkID types.CheckID, state string) {
testRegisterCheckWithPartition(t, s, idx,

@ -25,6 +25,7 @@ var allConnectKind = []string{
string(structs.ServiceKindIngressGateway),
string(structs.ServiceKindMeshGateway),
string(structs.ServiceKindTerminatingGateway),
string(structs.ServiceKindAPIGateway),
connectNativeInstancesTable,
}

@ -179,16 +179,25 @@ func TestStateStore_Usage_ServiceUsage(t *testing.T) {
testRegisterConnectNativeService(t, s, 13, "node1", "service-native")
testRegisterConnectNativeService(t, s, 14, "node2", "service-native")
testRegisterConnectNativeService(t, s, 15, "node2", "service-native-1")
testRegisterIngressService(t, s, 16, "node1", "ingress")
testRegisterMeshService(t, s, 17, "node1", "mesh")
testRegisterTerminatingService(t, s, 18, "node1", "terminating")
testRegisterAPIService(t, s, 19, "node1", "api")
testRegisterAPIService(t, s, 20, "node2", "api")
ws := memdb.NewWatchSet()
idx, usage, err := s.ServiceUsage(ws)
require.NoError(t, err)
require.Equal(t, idx, uint64(15))
require.Equal(t, 5, usage.Services)
require.Equal(t, 8, usage.ServiceInstances)
require.Equal(t, idx, uint64(20))
require.Equal(t, 9, usage.Services)
require.Equal(t, 13, usage.ServiceInstances)
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindConnectProxy)])
require.Equal(t, 3, usage.ConnectServiceInstances[connectNativeInstancesTable])
require.Equal(t, 6, usage.BillableServiceInstances)
require.Equal(t, 2, usage.ConnectServiceInstances[string(structs.ServiceKindAPIGateway)])
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindIngressGateway)])
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindTerminatingGateway)])
require.Equal(t, 1, usage.ConnectServiceInstances[string(structs.ServiceKindMeshGateway)])
testRegisterSidecarProxy(t, s, 16, "node2", "service2")

@ -149,6 +149,22 @@ var baseCases = map[string]testCase{
{Name: "kind", Value: "ingress-gateway"},
},
},
"consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=api-gateway": { // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
},
"consul.usage.test.state.connect_instances;datacenter=dc1;kind=api-gateway": {
Name: "consul.usage.test.state.connect_instances",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
},
"consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 0,
@ -624,6 +640,22 @@ var baseCases = map[string]testCase{
{Name: "kind", Value: "ingress-gateway"},
},
},
"consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=api-gateway": { // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
},
"consul.usage.test.state.connect_instances;datacenter=dc1;kind=api-gateway": {
Name: "consul.usage.test.state.connect_instances",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
},
"consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway": { // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 0,
@ -1127,6 +1159,9 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) {
require.NoError(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"}))
require.NoError(t, s.EnsureNode(4, &structs.Node{Node: "qux", Address: "127.0.0.3"}))
apigw := structs.TestNodeServiceAPIGateway(t)
apigw.ID = "api-gateway"
mgw := structs.TestNodeServiceMeshGateway(t)
mgw.ID = "mesh-gateway"
@ -1141,16 +1176,17 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) {
require.NoError(t, s.EnsureRegistration(10, structs.TestRegisterIngressGateway(t)))
require.NoError(t, s.EnsureService(11, "foo", mgw))
require.NoError(t, s.EnsureService(12, "foo", tgw))
require.NoError(t, s.EnsureService(13, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}}))
require.NoError(t, s.EnsureConfigEntry(14, &structs.IngressGatewayConfigEntry{
require.NoError(t, s.EnsureService(13, "foo", apigw))
require.NoError(t, s.EnsureService(14, "bar", &structs.NodeService{ID: "db-native", Service: "db", Tags: nil, Address: "", Port: 5000, Connect: structs.ServiceConnect{Native: true}}))
require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{
Kind: structs.IngressGateway,
Name: "foo",
}))
require.NoError(t, s.EnsureConfigEntry(15, &structs.IngressGatewayConfigEntry{
require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{
Kind: structs.IngressGateway,
Name: "bar",
}))
require.NoError(t, s.EnsureConfigEntry(16, &structs.IngressGatewayConfigEntry{
require.NoError(t, s.EnsureConfigEntry(17, &structs.IngressGatewayConfigEntry{
Kind: structs.IngressGateway,
Name: "baz",
}))
@ -1191,22 +1227,22 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) {
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.services;datacenter=dc1"] = metrics.GaugeValue{ // Legacy
Name: "consul.usage.test.consul.state.services",
Value: 7,
Value: 8,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.state.services;datacenter=dc1"] = metrics.GaugeValue{
Name: "consul.usage.test.state.services",
Value: 7,
Value: 8,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.service_instances;datacenter=dc1"] = metrics.GaugeValue{ // Legacy
Name: "consul.usage.test.consul.state.service_instances",
Value: 9,
Value: 10,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.state.service_instances;datacenter=dc1"] = metrics.GaugeValue{
Name: "consul.usage.test.state.service_instances",
Value: 9,
Value: 10,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=connect-proxy"] = metrics.GaugeValue{ // Legacy
@ -1257,6 +1293,22 @@ func TestUsageReporter_emitServiceUsage_OSS(t *testing.T) {
{Name: "kind", Value: "ingress-gateway"},
},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=api-gateway"] = metrics.GaugeValue{ // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 1,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.state.connect_instances;datacenter=dc1;kind=api-gateway"] = metrics.GaugeValue{
Name: "consul.usage.test.state.connect_instances",
Value: 1,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
{Name: "kind", Value: "api-gateway"},
},
}
nodesAndSvcsCase.expectedGauges["consul.usage.test.consul.state.connect_instances;datacenter=dc1;kind=mesh-gateway"] = metrics.GaugeValue{ // Legacy
Name: "consul.usage.test.consul.state.connect_instances",
Value: 1,

@ -3189,7 +3189,7 @@ func TestDNS_ServiceLookup_WanTranslation(t *testing.T) {
}
var out struct{}
require.NoError(t, a2.RPC(context.Background(), "Catalog.Register", args, &out))
require.NoError(r, a2.RPC(context.Background(), "Catalog.Register", args, &out))
})
// Look up the SRV record via service and prepared query.
@ -3517,11 +3517,11 @@ func TestDNS_CaseInsensitiveServiceLookup(t *testing.T) {
retry.Run(t, func(r *retry.R) {
in, _, err := c.Exchange(m, a.DNSAddr())
if err != nil {
t.Fatalf("err: %v", err)
r.Fatalf("err: %v", err)
}
if len(in.Answer) != 1 {
t.Fatalf("question %v, empty lookup: %#v", question, in)
r.Fatalf("question %v, empty lookup: %#v", question, in)
}
})
}

@ -72,16 +72,19 @@ func (a *awsLambda) CanApply(config *extensioncommon.RuntimeConfig) bool {
// PatchRoute modifies the routing configuration for a service of kind TerminatingGateway. If the kind is
// not TerminatingGateway, then it can not be modified.
func (a *awsLambda) PatchRoute(r *extensioncommon.RuntimeConfig, route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) {
if r.Kind != api.ServiceKindTerminatingGateway {
return route, false, nil
func (a *awsLambda) PatchRoute(p extensioncommon.RoutePayload) (*envoy_route_v3.RouteConfiguration, bool, error) {
cfg := p.RuntimeConfig
if cfg.Kind != api.ServiceKindTerminatingGateway {
return p.Message, false, nil
}
// Only patch outbound routes.
if extensioncommon.IsRouteToLocalAppCluster(route) {
return route, false, nil
if p.IsInbound() {
return p.Message, false, nil
}
route := p.Message
for _, virtualHost := range route.VirtualHosts {
for _, route := range virtualHost.Routes {
action, ok := route.Action.(*envoy_route_v3.Route_Route)
@ -101,16 +104,18 @@ func (a *awsLambda) PatchRoute(r *extensioncommon.RuntimeConfig, route *envoy_ro
}
// PatchCluster patches the provided envoy cluster with data required to support an AWS lambda function
func (a *awsLambda) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) {
func (a *awsLambda) PatchCluster(p extensioncommon.ClusterPayload) (*envoy_cluster_v3.Cluster, bool, error) {
// Only patch outbound clusters.
if extensioncommon.IsLocalAppCluster(c) {
return c, false, nil
if p.IsInbound() {
return p.Message, false, nil
}
transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com",
})
c := p.Message
if err != nil {
return c, false, fmt.Errorf("failed to make transport socket: %w", err)
}
@ -168,9 +173,11 @@ func (a *awsLambda) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_clus
// PatchFilter patches the provided envoy filter with an inserted lambda filter being careful not to
// overwrite the http filters.
func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) {
func (a *awsLambda) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener_v3.Filter, bool, error) {
filter := p.Message
// Only patch outbound filters.
if isInboundListener {
if p.IsInbound() {
return filter, false, nil
}

@ -202,7 +202,10 @@ func TestPatchCluster(t *testing.T) {
// Test patching the cluster
rc := extensioncommon.RuntimeConfig{}
patchedCluster, patchSuccess, err := tc.lambda.PatchCluster(&rc, tc.input)
patchedCluster, patchSuccess, err := tc.lambda.PatchCluster(extensioncommon.ClusterPayload{
RuntimeConfig: &rc,
Message: tc.input,
})
if tc.isErrExpected {
assert.Error(t, err)
assert.False(t, patchSuccess)
@ -307,7 +310,10 @@ func TestPatchRoute(t *testing.T) {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
l := awsLambda{}
r, ok, err := l.PatchRoute(tc.conf, tc.route)
r, ok, err := l.PatchRoute(extensioncommon.RoutePayload{
RuntimeConfig: tc.conf,
Message: tc.route,
})
require.NoError(t, err)
require.Equal(t, tc.expectRoute, r)
require.Equal(t, tc.expectBool, ok)
@ -456,7 +462,14 @@ func TestPatchFilter(t *testing.T) {
PayloadPassthrough: true,
InvocationMode: "asynchronous",
}
f, ok, err := l.PatchFilter(nil, tc.filter, tc.isInboundFilter)
d := extensioncommon.TrafficDirectionOutbound
if tc.isInboundFilter {
d = extensioncommon.TrafficDirectionInbound
}
f, ok, err := l.PatchFilter(extensioncommon.FilterPayload{
Message: tc.filter,
TrafficDirection: d,
})
require.Equal(t, tc.expectBool, ok)
if tc.expectErr == "" {
require.NoError(t, err)

@ -167,7 +167,7 @@ func (c extAuthzConfig) isHTTP() bool {
//
// If the extension is configured with the ext_authz service as an upstream there is no need to insert
// a new cluster so this method returns nil.
func (c *extAuthzConfig) toEnvoyCluster(cfg *cmn.RuntimeConfig) (*envoy_cluster_v3.Cluster, error) {
func (c *extAuthzConfig) toEnvoyCluster(_ *cmn.RuntimeConfig) (*envoy_cluster_v3.Cluster, error) {
var target *Target
if c.isHTTP() {
target = c.HttpService.Target
@ -601,7 +601,7 @@ func (t Target) clusterName(cfg *cmn.RuntimeConfig) (string, error) {
for service, upstream := range cfg.Upstreams {
if service == t.Service {
for sni := range upstream.SNI {
for sni := range upstream.SNIs {
return sni, nil
}
}
@ -678,8 +678,10 @@ type TransportApiVersion string
func (t TransportApiVersion) toEnvoy() envoy_core_v3.ApiVersion {
switch strings.ToLower(string(t)) {
case "v2":
//nolint:staticcheck
return envoy_core_v3.ApiVersion_V2
case "auto":
//nolint:staticcheck
return envoy_core_v3.ApiVersion_AUTO
default:
return envoy_core_v3.ApiVersion_V3

@ -14,10 +14,10 @@ import (
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
@ -60,6 +60,9 @@ func (r *ratelimit) fromArguments(args map[string]interface{}) error {
if err := mapstructure.Decode(args, r); err != nil {
return fmt.Errorf("error decoding extension arguments: %v", err)
}
if r.ProxyType == "" {
r.ProxyType = string(api.ServiceKindConnectProxy)
}
return r.validate()
}
@ -102,10 +105,11 @@ func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool {
// PatchFilter inserts a http local rate_limit filter at the head of
// envoy.filters.network.http_connection_manager filters
func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) {
func (r ratelimit) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener_v3.Filter, bool, error) {
filter := p.Message
// rate limit is only applied to the inbound listener of the service itself
// since the limit is aggregated from all downstream connections.
if !isInboundListener {
if !p.IsInbound() {
return filter, false, nil
}
@ -123,34 +127,34 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l
tokenBucket := envoy_type_v3.TokenBucket{}
if p.TokensPerFill != nil {
tokenBucket.TokensPerFill = &wrappers.UInt32Value{
Value: uint32(*p.TokensPerFill),
if r.TokensPerFill != nil {
tokenBucket.TokensPerFill = &wrapperspb.UInt32Value{
Value: uint32(*r.TokensPerFill),
}
}
if p.MaxTokens != nil {
tokenBucket.MaxTokens = uint32(*p.MaxTokens)
if r.MaxTokens != nil {
tokenBucket.MaxTokens = uint32(*r.MaxTokens)
}
if p.FillInterval != nil {
tokenBucket.FillInterval = durationpb.New(time.Duration(*p.FillInterval) * time.Second)
if r.FillInterval != nil {
tokenBucket.FillInterval = durationpb.New(time.Duration(*r.FillInterval) * time.Second)
}
var FilterEnabledDefault *envoy_core_v3.RuntimeFractionalPercent
if p.FilterEnabled != nil {
if r.FilterEnabled != nil {
FilterEnabledDefault = &envoy_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: *p.FilterEnabled,
Numerator: *r.FilterEnabled,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
}
}
var FilterEnforcedDefault *envoy_core_v3.RuntimeFractionalPercent
if p.FilterEnforced != nil {
if r.FilterEnforced != nil {
FilterEnforcedDefault = &envoy_core_v3.RuntimeFractionalPercent{
DefaultValue: &envoy_type_v3.FractionalPercent{
Numerator: *p.FilterEnforced,
Numerator: *r.FilterEnforced,
Denominator: envoy_type_v3.FractionalPercent_HUNDRED,
},
}
@ -187,7 +191,7 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l
}
func validateProxyType(t string) error {
if t != "connect-proxy" {
if t != string(api.ServiceKindConnectProxy) {
return fmt.Errorf("unexpected ProxyType %q", t)
}

@ -111,6 +111,30 @@ func TestConstructor(t *testing.T) {
expectedErrMsg: "cannot parse 'FilterEnforced', -1 overflows uint",
ok: false,
},
"invalid proxy type": {
arguments: makeArguments(map[string]interface{}{
"ProxyType": "invalid",
"FillInterval": 30,
"MaxTokens": 20,
"TokensPerFill": 5,
}),
expectedErrMsg: `unexpected ProxyType "invalid"`,
ok: false,
},
"default proxy type": {
arguments: makeArguments(map[string]interface{}{
"FillInterval": 30,
"MaxTokens": 20,
"TokensPerFill": 5,
}),
expected: ratelimit{
ProxyType: "connect-proxy",
MaxTokens: intPointer(20),
FillInterval: intPointer(30),
TokensPerFill: intPointer(5),
},
ok: true,
},
"valid everything": {
arguments: makeArguments(map[string]interface{}{
"ProxyType": "connect-proxy",

@ -45,6 +45,9 @@ func (l *lua) fromArguments(args map[string]interface{}) error {
if err := mapstructure.Decode(args, l); err != nil {
return fmt.Errorf("error decoding extension arguments: %v", err)
}
if l.ProxyType == "" {
l.ProxyType = string(api.ServiceKindConnectProxy)
}
return l.validate()
}
@ -53,7 +56,7 @@ func (l *lua) validate() error {
if l.Script == "" {
resultErr = multierror.Append(resultErr, fmt.Errorf("missing Script value"))
}
if l.ProxyType != "connect-proxy" {
if l.ProxyType != string(api.ServiceKindConnectProxy) {
resultErr = multierror.Append(resultErr, fmt.Errorf("unexpected ProxyType %q", l.ProxyType))
}
if l.Listener != "inbound" && l.Listener != "outbound" {
@ -67,14 +70,16 @@ func (l *lua) CanApply(config *extensioncommon.RuntimeConfig) bool {
return string(config.Kind) == l.ProxyType
}
func (l *lua) matchesListenerDirection(isInboundListener bool) bool {
func (l *lua) matchesListenerDirection(p extensioncommon.FilterPayload) bool {
isInboundListener := p.IsInbound()
return (!isInboundListener && l.Listener == "outbound") || (isInboundListener && l.Listener == "inbound")
}
// PatchFilter inserts a lua filter directly prior to envoy.filters.http.router.
func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) {
func (l *lua) PatchFilter(p extensioncommon.FilterPayload) (*envoy_listener_v3.Filter, bool, error) {
filter := p.Message
// Make sure filter matches extension config.
if !l.matchesListenerDirection(isInboundListener) {
if !l.matchesListenerDirection(p) {
return filter, false, nil
}

@ -54,6 +54,15 @@ func TestConstructor(t *testing.T) {
arguments: makeArguments(map[string]interface{}{"Listener": "invalid"}),
ok: false,
},
"default proxy type": {
arguments: makeArguments(map[string]interface{}{"ProxyType": ""}),
expected: lua{
ProxyType: "connect-proxy",
Listener: "inbound",
Script: "lua-script",
},
ok: true,
},
"valid everything": {
arguments: makeArguments(map[string]interface{}{}),
expected: lua{

@ -2,26 +2,22 @@ package propertyoverride
import (
"fmt"
"strings"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/consul/lib/maps"
)
type stringSet map[string]struct{}
type propertyOverride struct {
extensioncommon.BasicExtensionAdapter
// Patches are an array of Patch operations to be applied to the target resource(s).
Patches []Patch
// Debug controls error messages when Path matching fails.
@ -43,7 +39,42 @@ type ResourceFilter struct {
// TrafficDirection determines whether the patch will be applied to a service's inbound
// or outbound resources.
// This field is required.
TrafficDirection TrafficDirection
TrafficDirection extensioncommon.TrafficDirection
// Services indicates which upstream services will have corresponding Envoy resources patched.
// This includes directly targeted and discovery chain services. If Services is omitted or
// empty, all resources matching the filter will be targeted (including TProxy, which
// implicitly corresponds to any number of upstreams). Services must be omitted unless
// TrafficDirection is set to outbound.
Services []*ServiceName
}
func matchesResourceFilter[K proto.Message](rf ResourceFilter, resourceType ResourceType, payload extensioncommon.Payload[K]) bool {
if resourceType != rf.ResourceType {
return false
}
if payload.TrafficDirection != rf.TrafficDirection {
return false
}
if len(rf.Services) == 0 {
return true
}
for _, s := range rf.Services {
if payload.ServiceName == nil || s.CompoundServiceName != *payload.ServiceName {
continue
}
return true
}
return false
}
type ServiceName struct {
api.CompoundServiceName `mapstructure:",squash"`
}
// ResourceType is the type of Envoy resource being patched.
@ -56,23 +87,13 @@ const (
ResourceTypeRoute ResourceType = "route"
)
var ResourceTypes = stringSet{
var ResourceTypes = extensioncommon.StringSet{
string(ResourceTypeCluster): {},
string(ResourceTypeClusterLoadAssignment): {},
string(ResourceTypeListener): {},
string(ResourceTypeRoute): {},
string(ResourceTypeListener): {},
}
// TrafficDirection determines whether inbound or outbound Envoy resources will be patched.
type TrafficDirection string
const (
TrafficDirectionInbound TrafficDirection = "inbound"
TrafficDirectionOutbound TrafficDirection = "outbound"
)
var TrafficDirections = stringSet{string(TrafficDirectionInbound): {}, string(TrafficDirectionOutbound): {}}
// Op is the type of JSON Patch operation being applied.
type Op string
@ -81,12 +102,12 @@ const (
OpRemove Op = "remove"
)
var Ops = stringSet{string(OpAdd): {}, string(OpRemove): {}}
var Ops = extensioncommon.StringSet{string(OpAdd): {}, string(OpRemove): {}}
// validProxyTypes is the set of supported proxy types for this extension.
var validProxyTypes = stringSet{
string(api.ServiceKindConnectProxy): struct{}{},
string(api.ServiceKindTerminatingGateway): struct{}{},
var validProxyTypes = extensioncommon.StringSet{
// For now, we only support `connect-proxy`.
string(api.ServiceKindConnectProxy): struct{}{},
}
// Patch describes a single patch operation to modify the specific field of matching
@ -139,27 +160,58 @@ type Patch struct {
var _ extensioncommon.BasicExtension = (*propertyOverride)(nil)
func (c *stringSet) checkRequired(v, fieldName string) error {
if _, ok := (*c)[v]; !ok {
if v == "" {
return fmt.Errorf("field %s is required", fieldName)
}
return fmt.Errorf("invalid %s '%q'; supported values: %s",
fieldName, v, strings.Join(maps.SliceOfKeys(*c), ", "))
func (f *ResourceFilter) isEmpty() bool {
if f == nil {
return true
}
return nil
if len(f.Services) > 0 {
return false
}
if string(f.TrafficDirection) != "" {
return false
}
if string(f.ResourceType) != "" {
return false
}
return true
}
func (f *ResourceFilter) validate() error {
if f == nil || *f == (ResourceFilter{}) {
if f == nil || f.isEmpty() {
return fmt.Errorf("field ResourceFilter is required")
}
if err := ResourceTypes.checkRequired(string(f.ResourceType), "ResourceType"); err != nil {
if err := ResourceTypes.CheckRequired(string(f.ResourceType), "ResourceType"); err != nil {
return err
}
if err := TrafficDirections.checkRequired(string(f.TrafficDirection), "TrafficDirection"); err != nil {
if err := extensioncommon.TrafficDirections.CheckRequired(string(f.TrafficDirection), "TrafficDirection"); err != nil {
return err
}
for i := range f.Services {
sn := f.Services[i]
sn.normalize()
if err := sn.validate(); err != nil {
return err
}
}
return nil
}
func (sn *ServiceName) normalize() {
extensioncommon.NormalizeServiceName(&sn.CompoundServiceName)
}
func (sn *ServiceName) validate() error {
if sn.Name == "" {
return fmt.Errorf("service name is required")
}
return nil
}
@ -169,7 +221,7 @@ func (p *Patch) validate(debug bool) error {
return err
}
if err := Ops.checkRequired(string(p.Op), "Op"); err != nil {
if err := Ops.CheckRequired(string(p.Op), "Op"); err != nil {
return err
}
@ -209,7 +261,10 @@ func (p *propertyOverride) validate() error {
}
}
if err := validProxyTypes.checkRequired(string(p.ProxyType), "ProxyType"); err != nil {
if p.ProxyType == "" {
p.ProxyType = api.ServiceKindConnectProxy
}
if err := validProxyTypes.CheckRequired(string(p.ProxyType), "ProxyType"); err != nil {
resultErr = multierror.Append(resultErr, err)
}
@ -224,7 +279,21 @@ func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error)
if name := ext.Name; name != api.BuiltinPropertyOverrideExtension {
return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinPropertyOverrideExtension, name)
}
if err := mapstructure.WeakDecode(ext.Arguments, &p); err != nil {
// This avoids issues with decoding nested slices, which are error-prone
// due to slice<->map coercion by mapstructure. See HookWeakDecodeFromSlice
// and WeaklyTypedInput docs for more details.
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookWeakDecodeFromSlice,
decode.HookTranslateKeys,
),
WeaklyTypedInput: true,
Result: &p,
})
if err != nil {
return nil, fmt.Errorf("error configuring decoder: %v", err)
}
if err := d.Decode(ext.Arguments); err != nil {
return nil, fmt.Errorf("error decoding extension arguments: %v", err)
}
if err := p.validate(); err != nil {
@ -243,57 +312,35 @@ func (p *propertyOverride) CanApply(config *extensioncommon.RuntimeConfig) bool
}
// PatchRoute patches the provided Envoy Route with any applicable `route` ResourceType patches.
func (p *propertyOverride) PatchRoute(_ *extensioncommon.RuntimeConfig, r *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) {
d := TrafficDirectionOutbound
if extensioncommon.IsRouteToLocalAppCluster(r) {
d = TrafficDirectionInbound
}
return patchResourceType[*envoy_route_v3.RouteConfiguration](r, p, ResourceTypeRoute, d, &defaultStructPatcher[*envoy_route_v3.RouteConfiguration]{})
func (p *propertyOverride) PatchRoute(payload extensioncommon.RoutePayload) (*envoy_route_v3.RouteConfiguration, bool, error) {
return patchResourceType[*envoy_route_v3.RouteConfiguration](p, ResourceTypeRoute, payload, &defaultStructPatcher[*envoy_route_v3.RouteConfiguration]{})
}
// PatchCluster patches the provided Envoy Cluster with any applicable `cluster` ResourceType patches.
func (p *propertyOverride) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) {
d := TrafficDirectionOutbound
if extensioncommon.IsLocalAppCluster(c) {
d = TrafficDirectionInbound
}
return patchResourceType[*envoy_cluster_v3.Cluster](c, p, ResourceTypeCluster, d, &defaultStructPatcher[*envoy_cluster_v3.Cluster]{})
func (p *propertyOverride) PatchCluster(payload extensioncommon.ClusterPayload) (*envoy_cluster_v3.Cluster, bool, error) {
return patchResourceType[*envoy_cluster_v3.Cluster](p, ResourceTypeCluster, payload, &defaultStructPatcher[*envoy_cluster_v3.Cluster]{})
}
// PatchClusterLoadAssignment patches the provided Envoy ClusterLoadAssignment with any applicable `cluster-load-assignment` ResourceType patches.
func (p *propertyOverride) PatchClusterLoadAssignment(_ *extensioncommon.RuntimeConfig, c *envoy_endpoint_v3.ClusterLoadAssignment) (*envoy_endpoint_v3.ClusterLoadAssignment, bool, error) {
d := TrafficDirectionOutbound
if extensioncommon.IsLocalAppClusterLoadAssignment(c) {
d = TrafficDirectionInbound
}
return patchResourceType[*envoy_endpoint_v3.ClusterLoadAssignment](c, p, ResourceTypeClusterLoadAssignment, d, &defaultStructPatcher[*envoy_endpoint_v3.ClusterLoadAssignment]{})
func (p *propertyOverride) PatchClusterLoadAssignment(payload extensioncommon.ClusterLoadAssignmentPayload) (*envoy_endpoint_v3.ClusterLoadAssignment, bool, error) {
return patchResourceType[*envoy_endpoint_v3.ClusterLoadAssignment](p, ResourceTypeClusterLoadAssignment, payload, &defaultStructPatcher[*envoy_endpoint_v3.ClusterLoadAssignment]{})
}
// PatchListener patches the provided Envoy Listener with any applicable `listener` ResourceType patches.
func (p *propertyOverride) PatchListener(_ *extensioncommon.RuntimeConfig, l *envoy_listener_v3.Listener) (*envoy_listener_v3.Listener, bool, error) {
d := TrafficDirectionOutbound
if extensioncommon.IsInboundPublicListener(l) {
d = TrafficDirectionInbound
}
return patchResourceType[*envoy_listener_v3.Listener](l, p, ResourceTypeListener, d, &defaultStructPatcher[*envoy_listener_v3.Listener]{})
}
// PatchFilter does nothing as this extension does not target Filters directly.
func (p *propertyOverride) PatchFilter(_ *extensioncommon.RuntimeConfig, f *envoy_listener_v3.Filter, _ bool) (*envoy_listener_v3.Filter, bool, error) {
return f, false, nil
func (p *propertyOverride) PatchListener(payload extensioncommon.ListenerPayload) (*envoy_listener_v3.Listener, bool, error) {
return patchResourceType[*envoy_listener_v3.Listener](p, ResourceTypeListener, payload, &defaultStructPatcher[*envoy_listener_v3.Listener]{})
}
// patchResourceType applies Patches matching the given ResourceType to the target K.
// This helper simplifies implementation of the above per-type patch methods defined by BasicExtension.
func patchResourceType[K proto.Message](k K, p *propertyOverride, t ResourceType, d TrafficDirection, patcher structPatcher[K]) (K, bool, error) {
func patchResourceType[K proto.Message](p *propertyOverride, resourceType ResourceType, payload extensioncommon.Payload[K], patcher structPatcher[K]) (K, bool, error) {
resultPatched := false
var resultErr error
k := payload.Message
for _, patch := range p.Patches {
if patch.ResourceFilter.ResourceType != t {
continue
}
if patch.ResourceFilter.TrafficDirection != d {
if !matchesResourceFilter(patch.ResourceFilter, resourceType, payload) {
continue
}
newK, err := patcher.applyPatch(k, patch, p.Debug)

@ -2,11 +2,13 @@ package propertyoverride
import (
"fmt"
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"strings"
"testing"
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/stretchr/testify/require"
@ -32,7 +34,7 @@ func TestConstructor(t *testing.T) {
makeResourceFilter := func(overrides map[string]any) map[string]any {
f := map[string]any{
"ResourceType": ResourceTypeRoute,
"TrafficDirection": TrafficDirectionOutbound,
"TrafficDirection": extensioncommon.TrafficDirectionOutbound,
}
return applyOverrides(f, overrides)
}
@ -63,7 +65,7 @@ func TestConstructor(t *testing.T) {
errMsg string
}
validTestCase := func(o Op, d TrafficDirection, t ResourceType) testCase {
validTestCase := func(o Op, d extensioncommon.TrafficDirection, t ResourceType) testCase {
var v any = "foo"
if o != OpAdd {
v = nil
@ -214,6 +216,19 @@ func TestConstructor(t *testing.T) {
ok: false,
errMsg: fmt.Sprintf("field Value is not supported for %s operation", OpRemove),
},
"empty service name": {
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
makePatch(map[string]any{
"ResourceFilter": makeResourceFilter(map[string]any{
"Services": []map[string]any{
{},
},
}),
}),
}}),
ok: false,
errMsg: "service name is required",
},
// See decode.HookWeakDecodeFromSlice for more details. In practice, we can end up
// with a "Patches" field decoded to the single "Patch" value contained in the
// serialized slice (raised from the containing slice). Using WeakDecode solves
@ -221,10 +236,46 @@ func TestConstructor(t *testing.T) {
// enforces expected behavior until we do. Multi-member slices should be unaffected
// by WeakDecode as it is a more-permissive version of the default behavior.
"single value Patches decoded as map construction succeeds": {
arguments: makeArguments(map[string]any{"Patches": makePatch(map[string]any{})}),
expected: validTestCase(OpAdd, TrafficDirectionOutbound, ResourceTypeRoute).expected,
arguments: makeArguments(map[string]any{"Patches": makePatch(map[string]any{}), "ProxyType": nil}),
expected: validTestCase(OpAdd, extensioncommon.TrafficDirectionOutbound, ResourceTypeRoute).expected,
ok: true,
},
// Ensure that embedded api struct used for Services is parsed correctly.
// See also above comment on decode.HookWeakDecodeFromSlice.
"single value Services decoded as map construction succeeds": {
arguments: makeArguments(map[string]any{"Patches": []map[string]any{
makePatch(map[string]any{
"ResourceFilter": makeResourceFilter(map[string]any{
"Services": []map[string]any{
{"Name": "foo"},
},
}),
}),
}}),
expected: propertyOverride{
Patches: []Patch{
{
ResourceFilter: ResourceFilter{
ResourceType: ResourceTypeRoute,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Services: []*ServiceName{
{CompoundServiceName: api.CompoundServiceName{
Name: "foo",
Namespace: "default",
Partition: "default",
}},
},
},
Op: OpAdd,
Path: "/name",
Value: "foo",
},
},
Debug: true,
ProxyType: api.ServiceKindConnectProxy,
},
ok: true,
},
"invalid ProxyType": {
arguments: makeArguments(map[string]any{
"Patches": []map[string]any{
@ -248,10 +299,10 @@ func TestConstructor(t *testing.T) {
}
for o := range Ops {
for d := range TrafficDirections {
for d := range extensioncommon.TrafficDirections {
for t := range ResourceTypes {
cases["valid everything: "+strings.Join([]string{o, d, t}, ",")] =
validTestCase(Op(o), TrafficDirection(d), ResourceType(t))
validTestCase(Op(o), extensioncommon.TrafficDirection(d), ResourceType(t))
}
}
}
@ -294,32 +345,62 @@ func Test_patchResourceType(t *testing.T) {
Patches: patches,
}
}
makePatchWithPath := func(t ResourceType, d TrafficDirection, p string) Patch {
makePatchWithPath := func(filter ResourceFilter, p string) Patch {
return Patch{
ResourceFilter: ResourceFilter{
ResourceType: t,
TrafficDirection: d,
},
Op: OpAdd,
Path: p,
Value: 1,
ResourceFilter: filter,
Op: OpAdd,
Path: p,
Value: 1,
}
}
makePatch := func(t ResourceType, d TrafficDirection) Patch {
return makePatchWithPath(t, d, "/foo")
makePatch := func(filter ResourceFilter) Patch {
return makePatchWithPath(filter, "/foo")
}
svc1 := ServiceName{
CompoundServiceName: api.CompoundServiceName{Name: "svc1"},
}
svc2 := ServiceName{
CompoundServiceName: api.CompoundServiceName{Name: "svc2"},
}
clusterOutbound := makePatch(ResourceTypeCluster, TrafficDirectionOutbound)
clusterInbound := makePatch(ResourceTypeCluster, TrafficDirectionInbound)
routeOutbound := makePatch(ResourceTypeRoute, TrafficDirectionOutbound)
routeOutbound2 := makePatchWithPath(ResourceTypeRoute, TrafficDirectionOutbound, "/bar")
routeInbound := makePatch(ResourceTypeRoute, TrafficDirectionInbound)
clusterOutbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeCluster,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
})
clusterInbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeCluster,
TrafficDirection: extensioncommon.TrafficDirectionInbound,
})
listenerOutbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeListener,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
})
listenerOutbound2 := makePatchWithPath(ResourceFilter{
ResourceType: ResourceTypeListener,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
}, "/bar")
listenerInbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeListener,
TrafficDirection: extensioncommon.TrafficDirectionInbound,
})
routeOutbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeRoute,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
})
routeOutbound2 := makePatchWithPath(ResourceFilter{
ResourceType: ResourceTypeRoute,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
}, "/bar")
routeInbound := makePatch(ResourceFilter{
ResourceType: ResourceTypeRoute,
TrafficDirection: extensioncommon.TrafficDirectionInbound,
})
type args struct {
d TrafficDirection
k proto.Message
p *propertyOverride
t ResourceType
resourceType ResourceType
payload extensioncommon.Payload[proto.Message]
p *propertyOverride
}
type testCase struct {
args args
@ -329,79 +410,179 @@ func Test_patchResourceType(t *testing.T) {
cases := map[string]testCase{
"outbound gets matching patch": {
args: args{
d: TrafficDirectionOutbound,
k: &clusterv3.Cluster{},
resourceType: ResourceTypeCluster,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Message: &clusterv3.Cluster{},
},
p: makeExtension(clusterOutbound),
t: ResourceTypeCluster,
},
expectPatched: true,
wantApplied: []Patch{clusterOutbound},
},
"inbound gets matching patch": {
args: args{
d: TrafficDirectionInbound,
k: &clusterv3.Cluster{},
resourceType: ResourceTypeCluster,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionInbound,
Message: &clusterv3.Cluster{},
},
p: makeExtension(clusterInbound),
t: ResourceTypeCluster,
},
expectPatched: true,
wantApplied: []Patch{clusterInbound},
},
"multiple resources same direction only gets matching resource": {
args: args{
d: TrafficDirectionOutbound,
k: &clusterv3.Cluster{},
p: makeExtension(clusterOutbound, routeOutbound),
t: ResourceTypeCluster,
resourceType: ResourceTypeCluster,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Message: &clusterv3.Cluster{},
},
p: makeExtension(clusterOutbound, listenerOutbound),
},
expectPatched: true,
wantApplied: []Patch{clusterOutbound},
},
"multiple directions same resource only gets matching direction": {
args: args{
d: TrafficDirectionOutbound,
k: &clusterv3.Cluster{},
resourceType: ResourceTypeCluster,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Message: &clusterv3.Cluster{},
},
p: makeExtension(clusterOutbound, clusterInbound),
t: ResourceTypeCluster,
},
expectPatched: true,
wantApplied: []Patch{clusterOutbound},
},
"multiple directions and resources only gets matching patch": {
args: args{
d: TrafficDirectionInbound,
k: &routev3.RouteConfiguration{},
p: makeExtension(clusterOutbound, clusterInbound, routeOutbound, routeInbound),
t: ResourceTypeRoute,
resourceType: ResourceTypeRoute,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionInbound,
Message: &routev3.RouteConfiguration{},
},
p: makeExtension(clusterOutbound, clusterInbound, listenerOutbound, listenerInbound, routeOutbound, routeOutbound2, routeInbound),
},
expectPatched: true,
wantApplied: []Patch{routeInbound},
},
"multiple directions and resources multiple matches gets all matching patches": {
args: args{
d: TrafficDirectionOutbound,
k: &routev3.RouteConfiguration{},
p: makeExtension(clusterOutbound, clusterInbound, routeOutbound, routeInbound, routeOutbound2),
t: ResourceTypeRoute,
resourceType: ResourceTypeRoute,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Message: &routev3.RouteConfiguration{},
},
p: makeExtension(clusterOutbound, clusterInbound, listenerOutbound, listenerInbound, listenerOutbound2, routeOutbound, routeOutbound2, routeInbound),
},
expectPatched: true,
wantApplied: []Patch{routeOutbound, routeOutbound2},
},
"multiple directions and resources no matches gets no patches": {
args: args{
d: TrafficDirectionOutbound,
k: &routev3.RouteConfiguration{},
p: makeExtension(clusterInbound, routeOutbound, routeInbound, routeOutbound2),
t: ResourceTypeCluster,
resourceType: ResourceTypeCluster,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Message: &clusterv3.Cluster{},
},
p: makeExtension(clusterInbound, listenerOutbound, listenerInbound, listenerOutbound2, routeInbound, routeOutbound),
},
expectPatched: false,
wantApplied: nil,
},
}
type resourceTypeServiceMatch struct {
resourceType ResourceType
message proto.Message
}
resourceTypeCases := []resourceTypeServiceMatch{
{
resourceType: ResourceTypeCluster,
message: &clusterv3.Cluster{},
},
{
resourceType: ResourceTypeListener,
message: &listenerv3.Listener{},
},
{
resourceType: ResourceTypeRoute,
message: &routev3.RouteConfiguration{},
},
{
resourceType: ResourceTypeClusterLoadAssignment,
message: &endpointv3.ClusterLoadAssignment{},
},
}
for _, tc := range resourceTypeCases {
{
patch := makePatch(ResourceFilter{
ResourceType: tc.resourceType,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Services: []*ServiceName{
{CompoundServiceName: svc2.CompoundServiceName},
},
})
cases[fmt.Sprintf("%s - no match", tc.resourceType)] = testCase{
args: args{
resourceType: tc.resourceType,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
ServiceName: &svc1.CompoundServiceName,
Message: tc.message,
RuntimeConfig: &extensioncommon.RuntimeConfig{
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
svc1.CompoundServiceName: {},
},
},
},
p: makeExtension(patch),
},
expectPatched: false,
wantApplied: nil,
}
}
{
patch := makePatch(ResourceFilter{
ResourceType: tc.resourceType,
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
Services: []*ServiceName{
{CompoundServiceName: svc2.CompoundServiceName},
{CompoundServiceName: svc1.CompoundServiceName},
},
})
cases[fmt.Sprintf("%s - match", tc.resourceType)] = testCase{
args: args{
resourceType: tc.resourceType,
payload: extensioncommon.Payload[proto.Message]{
TrafficDirection: extensioncommon.TrafficDirectionOutbound,
ServiceName: &svc1.CompoundServiceName,
Message: tc.message,
RuntimeConfig: &extensioncommon.RuntimeConfig{
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
svc1.CompoundServiceName: {},
},
},
},
p: makeExtension(patch),
},
expectPatched: true,
wantApplied: []Patch{patch},
}
}
}
for n, tc := range cases {
t.Run(n, func(t *testing.T) {
mockPatcher := MockPatcher[proto.Message]{}
_, patched, err := patchResourceType[proto.Message](tc.args.k, tc.args.p, tc.args.t, tc.args.d, &mockPatcher)
_, patched, err := patchResourceType[proto.Message](tc.args.p, tc.args.resourceType, tc.args.payload, &mockPatcher)
require.NoError(t, err, "unexpected error from mock")
require.Equal(t, tc.expectPatched, patched)
@ -414,6 +595,7 @@ type MockPatcher[K proto.Message] struct {
appliedPatches []Patch
}
//nolint:unparam
func (m *MockPatcher[K]) applyPatch(k K, p Patch, _ bool) (result K, e error) {
m.appliedPatches = append(m.appliedPatches, p)
return k, nil
@ -436,7 +618,7 @@ func TestCanApply(t *testing.T) {
},
"invalid proxy type": {
ext: &propertyOverride{
ProxyType: api.ServiceKindTerminatingGateway,
ProxyType: api.ServiceKindConnectProxy,
},
conf: &extensioncommon.RuntimeConfig{
Kind: api.ServiceKindMeshGateway,

@ -2,18 +2,19 @@ package propertyoverride
import (
"fmt"
"testing"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
_struct "github.com/golang/protobuf/ptypes/struct"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
_struct "google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"testing"
)
func TestPatchStruct(t *testing.T) {

@ -195,7 +195,7 @@ func (p *pluginConfig) asyncDataSource(rtCfg *extensioncommon.RuntimeConfig) (*e
clusterSNI := ""
for service, upstream := range rtCfg.Upstreams {
if service == remote.HttpURI.Service {
for sni := range upstream.SNI {
for sni := range upstream.SNIs {
clusterSNI = sni
break
}

@ -4,21 +4,19 @@
package wasm
import (
"errors"
"fmt"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
cmn "github.com/hashicorp/consul/envoyextensions/extensioncommon"
)
// wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins.
type wasm struct {
extensioncommon.BasicExtensionAdapter
cmn.BasicExtensionAdapter
name string
wasmConfig *wasmConfig
@ -26,14 +24,14 @@ type wasm struct {
var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"}
var _ extensioncommon.BasicExtension = (*wasm)(nil)
var _ cmn.BasicExtension = (*wasm)(nil)
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
func Constructor(ext api.EnvoyExtension) (cmn.EnvoyExtender, error) {
w, err := construct(ext)
if err != nil {
return nil, err
}
return &extensioncommon.BasicEnvoyExtender{
return &cmn.BasicEnvoyExtender{
Extension: &w,
}, nil
}
@ -67,72 +65,56 @@ func (w *wasm) fromArguments(args map[string]any) error {
}
// CanApply indicates if the WASM extension can be applied to the given extension configuration.
// Currently the Wasm extension can be applied if the extension configuration is for an inbound
// listener (checked below) on a local connect-proxy.
func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool {
return config.Kind == w.wasmConfig.ProxyType
func (w wasm) CanApply(cfg *cmn.RuntimeConfig) bool {
return cfg.Kind == w.wasmConfig.ProxyType &&
cfg.Protocol == w.wasmConfig.Protocol
}
func (w wasm) matchesConfigDirection(isInboundListener bool) bool {
return isInboundListener && w.wasmConfig.ListenerType == "inbound"
return (isInboundListener && w.wasmConfig.ListenerType == "inbound") ||
(!isInboundListener && w.wasmConfig.ListenerType == "outbound")
}
// PatchFilter adds a Wasm filter to the HTTP filter chain.
// TODO (wasm/tcp): Add support for TCP filters.
func (w wasm) PatchFilter(cfg *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) {
// PatchFilters adds a Wasm HTTP or TCP filter to the filter chain.
func (w wasm) PatchFilters(cfg *cmn.RuntimeConfig, filters []*envoy_listener_v3.Filter, isInboundListener bool) ([]*envoy_listener_v3.Filter, error) {
if !w.matchesConfigDirection(isInboundListener) {
return filter, false, nil
return filters, nil
}
if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil
}
if typedConfig := filter.GetTypedConfig(); typedConfig == nil {
return filter, false, errors.New("failed to get typed config for http filter")
}
httpConnMgr := envoy_resource_v3.GetHTTPConnectionManager(filter)
if httpConnMgr == nil {
return filter, false, errors.New("failed to get HTTP connection manager")
// Check that the Wasm plugin protocol matches the service protocol.
// It is a runtime error if the extension is configured to apply a Wasm plugin
// that doesn't match the service's protocol.
// This shouldn't happen because the caller should check CanApply first, but just in case.
if cfg.Protocol != w.wasmConfig.Protocol {
return filters, fmt.Errorf("failed to apply Wasm filter: service protocol for %q is %q but protocol for the Wasm extension is %q. Please ensure the protocols match",
cfg.ServiceName.Name, cfg.Protocol, w.wasmConfig.Protocol)
}
// Generate the Wasm plugin configuration. It is the same config for HTTP and network filters.
wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg)
if err != nil {
return filter, false, fmt.Errorf("failed to encode Envoy Wasm configuration: %w", err)
return filters, fmt.Errorf("failed to encode Envoy Wasm plugin configuration: %w", err)
}
extHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.wasm",
&envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig},
)
if err != nil {
return filter, false, err
}
// Insert the filter immediately before the terminal filter.
insertOptions := cmn.InsertOptions{Location: cmn.InsertBeforeFirstMatch}
var (
changedFilters = make([]*envoy_http_v3.HttpFilter, 0, len(httpConnMgr.HttpFilters)+1)
changed bool
)
// We need to be careful about overwriting http filters completely because
// http filters validates intentions with the RBAC filter. This inserts the
// filter before `envoy.filters.http.router` while keeping everything
// else intact.
for _, httpFilter := range httpConnMgr.HttpFilters {
if httpFilter.Name == "envoy.filters.http.router" {
changedFilters = append(changedFilters, extHttpFilter)
changed = true
switch cfg.Protocol {
case "grpc", "http2", "http":
insertOptions.FilterName = "envoy.filters.http.router"
filter, err := cmn.MakeEnvoyHTTPFilter("envoy.filters.http.wasm", &envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig})
if err != nil {
return filters, fmt.Errorf("failed to make Wasm HTTP filter: %w", err)
}
changedFilters = append(changedFilters, httpFilter)
}
if changed {
httpConnMgr.HttpFilters = changedFilters
}
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", httpConnMgr)
if err != nil {
return filter, false, errors.New("error making new filter")
return cmn.InsertHTTPFilter(filters, filter, insertOptions)
case "tcp":
fallthrough
default:
insertOptions.FilterName = "envoy.filters.network.tcp_proxy"
filter, err := cmn.MakeFilter("envoy.filters.network.wasm", &envoy_wasm_v3.Wasm{Config: wasmPluginConfig})
if err != nil {
return filters, fmt.Errorf("failed to make Wasm network filter: %w", err)
}
return cmn.InsertNetworkFilter(filters, filter, insertOptions)
}
return newFilter, true, nil
}

@ -15,8 +15,8 @@ import (
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_network_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/wasm/v3"
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
@ -35,157 +35,121 @@ func TestHttpWasmExtension(t *testing.T) {
cases := map[string]struct {
extName string
canApply bool
args func(bool) map[string]any
rtCfg func(bool) *extensioncommon.RuntimeConfig
cfg func(string, bool) *testWasmConfig
rtCfg func(string, bool) *extensioncommon.RuntimeConfig
isInboundFilter bool
inputFilters func() []*envoy_http_v3.HttpFilter
expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter
expPatched bool
errStr string
debug bool
}{
"http remote file": {
"remote file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
cfg: func(proto string, ent bool) *testWasmConfig { return makeTestWasmConfig(proto, ent) },
rtCfg: func(proto string, ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(proto, ent) },
isInboundFilter: true,
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{
Name: "envoy.filters.http.wasm",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_http_wasm_v3.Wasm{
Config: tc.toHttpWasmFilter(t),
}),
},
},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
},
expPatched: true,
},
"local file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.Protocol = "http"
cfg: func(proto string, ent bool) *testWasmConfig {
cfg := newTestWasmConfig(proto, ent)
cfg.ListenerType = "inbound"
cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm"
return cfg.toMap(t)
return cfg
},
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
rtCfg: func(proto string, ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(proto, ent) },
isInboundFilter: true,
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{
Name: "envoy.filters.http.wasm",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_http_wasm_v3.Wasm{
Config: tc.toHttpWasmFilter(t),
}),
},
},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
},
expPatched: true,
},
"inbound filters ignored": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
cfg: func(proto string, ent bool) *testWasmConfig { return makeTestWasmConfig(proto, ent) },
rtCfg: func(proto string, ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(proto, ent) },
isInboundFilter: false,
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
},
expPatched: false,
},
"no cluster for remote file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig {
rt := makeTestRuntimeConfig(ent)
cfg: func(proto string, ent bool) *testWasmConfig { return makeTestWasmConfig(proto, ent) },
rtCfg: func(proto string, ent bool) *extensioncommon.RuntimeConfig {
rt := makeTestRuntimeConfig(proto, ent)
rt.Upstreams = nil
return rt
},
isInboundFilter: true,
inputFilters: makeTestHttpFilters,
errStr: "no upstream found for remote service",
expPatched: false,
},
"protocol mismatch": {
extName: api.BuiltinWasmExtension,
canApply: false,
cfg: func(proto string, ent bool) *testWasmConfig { return makeTestWasmConfig(proto, ent) },
rtCfg: func(proto string, ent bool) *extensioncommon.RuntimeConfig {
rt := makeTestRuntimeConfig(proto, ent)
switch proto {
case "http":
rt.Protocol = "tcp"
case "tcp":
rt.Protocol = "http"
}
return rt
},
isInboundFilter: true,
},
}
for _, enterprise := range []bool{false, true} {
for _, protocol := range []string{"tcp", "http"} {
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_%s_ent_%t", name, protocol, enterprise), func(t *testing.T) {
cfg := c.cfg(protocol, enterprise)
rtCfg := c.rtCfg(protocol, enterprise)
rtCfg.EnvoyExtension = api.EnvoyExtension{
Name: c.extName,
Arguments: cfg.toMap(t),
}
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_ent_%t", name, enterprise), func(t *testing.T) {
t.Parallel()
rtCfg := c.rtCfg(enterprise)
rtCfg.EnvoyExtension = api.EnvoyExtension{
Name: c.extName,
Arguments: c.args(enterprise),
}
w, err := construct(rtCfg.EnvoyExtension)
require.NoError(t, err)
require.Equal(t, c.canApply, w.CanApply(rtCfg))
if !c.canApply {
return
}
w, err := construct(rtCfg.EnvoyExtension)
require.NoError(t, err)
require.Equal(t, c.canApply, w.CanApply(rtCfg))
if !c.canApply {
return
}
route, patched, err := w.PatchRoute(c.rtCfg(enterprise), nil)
require.Nil(t, route)
require.False(t, patched)
require.NoError(t, err)
var inputFilters []*envoy_listener_v3.Filter
if protocol == "http" {
inputFilters = append(inputFilters, makeHttpConMgr(t, makeTestHttpFilters()))
} else {
inputFilters = makeTestFilters()
}
cluster, patched, err := w.PatchCluster(c.rtCfg(enterprise), nil)
require.Nil(t, cluster)
require.False(t, patched)
require.NoError(t, err)
obsFilters, err := w.PatchFilters(rtCfg, inputFilters, c.isInboundFilter)
if c.errStr == "" {
require.NoError(t, err)
inputHttpConMgr := makeHttpConMgr(t, c.inputFilters())
obsHttpConMgr, patched, err := w.PatchFilter(c.rtCfg(enterprise), inputHttpConMgr, c.isInboundFilter)
if c.errStr == "" {
require.NoError(t, err)
require.Equal(t, c.expPatched, patched)
expFilters := cfg.expFilters(t)
if !cfg.matchesDirection(c.isInboundFilter) {
// If the listener type does not match the filter direction then the
// filter should not be applied and the output should match the input.
expFilters = inputFilters
}
cfg := testWasmConfigFromMap(t, c.args(enterprise))
expHttpConMgr := makeHttpConMgr(t, c.expFilters(cfg))
if c.debug {
t.Logf("cfg =\n%s\n\n", cfg.toJSON(t))
require.Equal(t, len(expFilters), len(obsFilters))
for idx, expFilter := range expFilters {
t.Logf("expFilterJSON[%d] =\n%s\n\n", idx, protoToJSON(t, expFilter))
t.Logf("obsfilterJSON[%d] =\n%s\n\n", idx, protoToJSON(t, obsFilters[idx]))
}
}
if c.debug {
t.Logf("cfg =\n%s\n\n", cfg.toJSON(t))
t.Logf("expFilterJSON =\n%s\n\n", protoToJSON(t, expHttpConMgr))
t.Logf("obsfilterJSON =\n%s\n\n", protoToJSON(t, obsHttpConMgr))
prototest.AssertDeepEqual(t, expFilters, obsFilters)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr)
}
prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr)
}
})
})
}
}
}
}
@ -194,18 +158,18 @@ func TestWasmConstructor(t *testing.T) {
t.Parallel()
cases := map[string]struct {
name string
args func(bool) map[string]any
args func(string, bool) map[string]any
errStr string
}{
"with no arguments": {
name: api.BuiltinWasmExtension,
args: func(_ bool) map[string]any { return nil },
args: func(_ string, _ bool) map[string]any { return nil },
errStr: "VmConfig.Code must provide exactly one of Local or Remote data source",
},
"invalid protocol": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.Protocol = "invalid"
return cfg.toMap(t)
},
@ -213,8 +177,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid proxy type": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.ProxyType = "invalid"
return cfg.toMap(t)
},
@ -222,8 +186,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid listener type": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.ListenerType = "invalid"
return cfg.toMap(t)
},
@ -231,8 +195,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid runtime": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Runtime = "invalid"
return cfg.toMap(t)
},
@ -240,8 +204,8 @@ func TestWasmConstructor(t *testing.T) {
},
"both local and remote files": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "http://file-server/plugin.wasm"
@ -251,8 +215,8 @@ func TestWasmConstructor(t *testing.T) {
},
"service and uri required for remote files": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
return cfg.toMap(t)
},
@ -260,8 +224,8 @@ func TestWasmConstructor(t *testing.T) {
},
"no sha for remote file": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "http://file-server/plugin.wasm"
return cfg.toMap(t)
@ -270,8 +234,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid url for remote file": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "://bogus.url.com/error"
return cfg.toMap(t)
@ -280,8 +244,8 @@ func TestWasmConstructor(t *testing.T) {
},
"decoding error": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
a := makeTestWasmConfig(ent).toMap(t)
args: func(proto string, ent bool) map[string]any {
a := makeTestWasmConfig(proto, ent).toMap(t)
setField(a, "PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval", 1000)
return a
},
@ -289,8 +253,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid http timeout": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout = "invalid"
return cfg.toMap(t)
},
@ -298,8 +262,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid num retries": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries = -1
return cfg.toMap(t)
},
@ -307,8 +271,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid base interval": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "0s"
return cfg.toMap(t)
},
@ -316,8 +280,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid max interval": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "10s"
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "5s"
return cfg.toMap(t)
@ -326,8 +290,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid base interval duration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "invalid"
return cfg.toMap(t)
},
@ -335,8 +299,8 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid max interval duration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
args: func(proto string, ent bool) map[string]any {
cfg := newTestWasmConfig(proto, ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "invalid"
return cfg.toMap(t)
},
@ -344,39 +308,39 @@ func TestWasmConstructor(t *testing.T) {
},
"invalid extension name": {
name: "invalid",
args: func(ent bool) map[string]any { return newTestWasmConfig(ent).toMap(t) },
args: func(proto string, ent bool) map[string]any { return newTestWasmConfig(proto, ent).toMap(t) },
errStr: `expected extension name "builtin/wasm" but got "invalid"`,
},
"valid configuration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
args: func(proto string, ent bool) map[string]any { return makeTestWasmConfig(proto, ent).toMap(t) },
},
}
for _, enterprise := range []bool{false, true} {
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_ent_%t", name, enterprise), func(t *testing.T) {
t.Parallel()
svc := api.CompoundServiceName{Name: "svc"}
ext := extensioncommon.RuntimeConfig{
ServiceName: svc,
EnvoyExtension: api.EnvoyExtension{
Name: c.name,
Arguments: c.args(enterprise),
},
}
for _, protocol := range []string{"tcp", "http"} {
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_%s_ent_%t", name, protocol, enterprise), func(t *testing.T) {
svc := api.CompoundServiceName{Name: "svc"}
ext := extensioncommon.RuntimeConfig{
ServiceName: svc,
EnvoyExtension: api.EnvoyExtension{
Name: c.name,
Arguments: c.args(protocol, enterprise),
},
}
e, err := Constructor(ext.EnvoyExtension)
e, err := Constructor(ext.EnvoyExtension)
if c.errStr == "" {
require.NoError(t, err)
require.NotNil(t, e)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr)
}
})
if c.errStr == "" {
require.NoError(t, err)
require.NotNil(t, e)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr)
}
})
}
}
}
}
@ -425,13 +389,6 @@ type testWasmConfig struct {
}
}
func testWasmConfigFromMap(t *testing.T, m map[string]any) testWasmConfig {
t.Helper()
var cfg testWasmConfig
require.NoError(t, mapstructure.Decode(m, &cfg))
return cfg
}
func (c testWasmConfig) toMap(t *testing.T) map[string]any {
t.Helper()
var m map[string]any
@ -446,7 +403,7 @@ func (c testWasmConfig) toJSON(t *testing.T) []byte {
return b
}
func (cfg testWasmConfig) toHttpWasmFilter(t *testing.T) *envoy_wasm_v3.PluginConfig {
func (cfg testWasmConfig) toWasmPluginConfig(t *testing.T) *envoy_wasm_v3.PluginConfig {
t.Helper()
var code *envoy_core_v3.AsyncDataSource
if cfg.PluginConfig.VmConfig.Code.Local.Filename != "" {
@ -543,6 +500,63 @@ func (cfg testWasmConfig) toHttpWasmFilter(t *testing.T) *envoy_wasm_v3.PluginCo
}
}
func (cfg testWasmConfig) toHttpFilter(t *testing.T) *envoy_http_v3.HttpFilter {
return &envoy_http_v3.HttpFilter{
Name: "envoy.filters.http.wasm",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_http_wasm_v3.Wasm{
Config: cfg.toWasmPluginConfig(t),
}),
},
}
}
func (cfg testWasmConfig) toNetworkFilter(t *testing.T) *envoy_listener_v3.Filter {
return &envoy_listener_v3.Filter{
Name: "envoy.filters.network.wasm",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_network_wasm_v3.Wasm{
Config: cfg.toWasmPluginConfig(t),
}),
},
}
}
func (cfg testWasmConfig) expFilters(t *testing.T) []*envoy_listener_v3.Filter {
if cfg.Protocol == "http" {
return []*envoy_listener_v3.Filter{makeHttpConMgr(t, cfg.expHttpFilters(t))}
} else {
return cfg.expNetworkFilters(t)
}
}
func (cfg testWasmConfig) expHttpFilters(t *testing.T) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
cfg.toHttpFilter(t),
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
}
func (cfg testWasmConfig) expNetworkFilters(t *testing.T) []*envoy_listener_v3.Filter {
return []*envoy_listener_v3.Filter{
{Name: "one"},
{Name: "two"},
cfg.toNetworkFilter(t),
{Name: "envoy.filters.network.tcp_proxy"},
{Name: "three"},
}
}
func (cfg testWasmConfig) matchesDirection(isInbound bool) bool {
return (isInbound && cfg.ListenerType == "inbound") ||
(!isInbound && cfg.ListenerType == "outbound")
}
func makeAny(t *testing.T, m proto.Message) *anypb.Any {
t.Helper()
v, err := anypb.New(m)
@ -562,6 +576,15 @@ func makeHttpConMgr(t *testing.T, filters []*envoy_http_v3.HttpFilter) *envoy_li
}
}
func makeTestFilters() []*envoy_listener_v3.Filter {
return []*envoy_listener_v3.Filter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.network.tcp_proxy"},
{Name: "three"},
}
}
func makeTestHttpFilters() []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
@ -571,7 +594,7 @@ func makeTestHttpFilters() []*envoy_http_v3.HttpFilter {
}
}
func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig {
func makeTestRuntimeConfig(protocol string, enterprise bool) *extensioncommon.RuntimeConfig {
var ns, ap string
if enterprise {
ns = "ns1"
@ -586,17 +609,18 @@ func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig {
Namespace: acl.NamespaceOrDefault(ns),
Partition: acl.PartitionOrDefault(ap),
}: {
SNI: map[string]struct{}{"test-file-server": {}},
SNIs: map[string]struct{}{"test-file-server": {}},
EnvoyID: "test-file-server",
},
},
Protocol: protocol,
}
}
func makeTestWasmConfig(enterprise bool) *testWasmConfig {
cfg := newTestWasmConfig(enterprise)
func makeTestWasmConfig(protocol string, enterprise bool) *testWasmConfig {
cfg := newTestWasmConfig(protocol, enterprise)
cfg.Required = false
cfg.Protocol = "http"
cfg.Protocol = protocol
cfg.ProxyType = "connect-proxy"
cfg.ListenerType = "inbound"
cfg.PluginConfig.Name = "test-plugin-name"
@ -618,8 +642,8 @@ func makeTestWasmConfig(enterprise bool) *testWasmConfig {
return cfg
}
func newTestWasmConfig(enterprise bool) *testWasmConfig {
cfg := &testWasmConfig{}
func newTestWasmConfig(protocol string, enterprise bool) *testWasmConfig {
cfg := &testWasmConfig{Protocol: protocol}
if enterprise {
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Namespace = "ns1"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Partition = "ap1"

@ -6,6 +6,9 @@ package envoyextensions
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
awslambda "github.com/hashicorp/consul/agent/envoyextensions/builtin/aws-lambda"
extauthz "github.com/hashicorp/consul/agent/envoyextensions/builtin/ext-authz"
"github.com/hashicorp/consul/agent/envoyextensions/builtin/http/localratelimit"
@ -14,7 +17,6 @@ import (
"github.com/hashicorp/consul/agent/envoyextensions/builtin/wasm"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/go-multierror"
)
type extensionConstructor func(api.EnvoyExtension) (extensioncommon.EnvoyExtender, error)
@ -50,6 +52,23 @@ func ValidateExtensions(extensions []api.EnvoyExtension) error {
output = multierror.Append(output, fmt.Errorf("invalid EnvoyExtensions[%d]: Name is required", i))
continue
}
if v := ext.EnvoyVersion; v != "" {
_, err := version.NewConstraint(v)
if err != nil {
output = multierror.Append(output, fmt.Errorf("invalid EnvoyExtensions[%d].EnvoyVersion: %w", i, err))
continue
}
}
if v := ext.ConsulVersion; v != "" {
_, err := version.NewConstraint(v)
if err != nil {
output = multierror.Append(output, fmt.Errorf("invalid EnvoyExtensions[%d].ConsulVersion: %w", i, err))
continue
}
}
_, err := ConstructExtension(ext)
if err != nil {
output = multierror.Append(output, fmt.Errorf("invalid EnvoyExtensions[%d][%s]: %w", i, ext.Name, err))

@ -48,6 +48,30 @@ func TestValidateExtensions(t *testing.T) {
"missing Script value",
},
},
"invalid consul version constraint": {
input: []api.EnvoyExtension{{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "arn:aws:lambda:us-east-1:111111111111:function:lambda-1234",
},
ConsulVersion: "bad",
}},
expectErrs: []string{
"invalid EnvoyExtensions[0].ConsulVersion: Malformed constraint: bad",
},
},
"invalid envoy version constraint": {
input: []api.EnvoyExtension{{
Name: "builtin/aws/lambda",
Arguments: map[string]interface{}{
"ARN": "arn:aws:lambda:us-east-1:111111111111:function:lambda-1234",
},
EnvoyVersion: "bad",
}},
expectErrs: []string{
"invalid EnvoyExtensions[0].EnvoyVersion: Malformed constraint: bad",
},
},
}
for name, tc := range tests {

@ -11,6 +11,7 @@ import (
external "github.com/hashicorp/consul/agent/grpc-external"
"github.com/hashicorp/consul/proto-public/pbdataplane"
"github.com/hashicorp/consul/version"
)
func (s *Server) GetSupportedDataplaneFeatures(ctx context.Context, req *pbdataplane.GetSupportedDataplaneFeaturesRequest) (*pbdataplane.GetSupportedDataplaneFeaturesResponse, error) {
@ -40,6 +41,10 @@ func (s *Server) GetSupportedDataplaneFeatures(ctx context.Context, req *pbdatap
FeatureName: pbdataplane.DataplaneFeatures_DATAPLANE_FEATURES_ENVOY_BOOTSTRAP_CONFIGURATION,
Supported: true,
},
{
FeatureName: pbdataplane.DataplaneFeatures_DATAPLANE_FEATURES_FIPS,
Supported: version.IsFIPS(),
},
}
return &pbdataplane.GetSupportedDataplaneFeaturesResponse{SupportedDataplaneFeatures: supportedFeatures}, nil

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/consul/agent/grpc-external/testutils"
structs "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto-public/pbdataplane"
"github.com/hashicorp/consul/version"
)
const testACLToken = "acl-token"
@ -40,7 +41,7 @@ func TestSupportedDataplaneFeatures_Success(t *testing.T) {
client := testClient(t, server)
resp, err := client.GetSupportedDataplaneFeatures(ctx, &pbdataplane.GetSupportedDataplaneFeaturesRequest{})
require.NoError(t, err)
require.Equal(t, 3, len(resp.SupportedDataplaneFeatures))
require.Equal(t, 4, len(resp.SupportedDataplaneFeatures))
for _, feature := range resp.SupportedDataplaneFeatures {
switch feature.GetFeatureName() {
@ -50,6 +51,8 @@ func TestSupportedDataplaneFeatures_Success(t *testing.T) {
require.True(t, feature.GetSupported())
case pbdataplane.DataplaneFeatures_DATAPLANE_FEATURES_ENVOY_BOOTSTRAP_CONFIGURATION:
require.True(t, feature.GetSupported())
case pbdataplane.DataplaneFeatures_DATAPLANE_FEATURES_FIPS:
require.Equal(t, version.IsFIPS(), feature.GetSupported())
default:
require.False(t, feature.GetSupported())
}
@ -72,7 +75,7 @@ func TestSupportedDataplaneFeatures_ACLsDisabled(t *testing.T) {
client := testClient(t, server)
resp, err := client.GetSupportedDataplaneFeatures(ctx, &pbdataplane.GetSupportedDataplaneFeaturesRequest{})
require.NoError(t, err)
require.Equal(t, 3, len(resp.SupportedDataplaneFeatures))
require.Equal(t, 4, len(resp.SupportedDataplaneFeatures))
}
func TestSupportedDataplaneFeatures_InvalidACLToken(t *testing.T) {

@ -1048,7 +1048,7 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) {
require.Equal(r, mongo.Service.CompoundServiceName().String(), msg.GetResponse().ResourceID)
var nodes pbpeerstream.ExportedService
require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&nodes))
require.NoError(r, msg.GetResponse().Resource.UnmarshalTo(&nodes))
require.Len(r, nodes.Nodes, 1)
})
})
@ -1077,12 +1077,12 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) {
msg, err := client.RecvWithTimeout(100 * time.Millisecond)
require.NoError(r, err)
require.Equal(r, pbpeerstream.TypeURLExportedServiceList, msg.GetResponse().ResourceURL)
require.Equal(t, subExportedServiceList, msg.GetResponse().ResourceID)
require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation)
require.Equal(r, subExportedServiceList, msg.GetResponse().ResourceID)
require.Equal(r, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation)
var exportedServices pbpeerstream.ExportedServiceList
require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&exportedServices))
require.Equal(t, []string{structs.ServiceName{Name: "mongo"}.String()}, exportedServices.Services)
require.NoError(r, msg.GetResponse().Resource.UnmarshalTo(&exportedServices))
require.Equal(r, []string{structs.ServiceName{Name: "mongo"}.String()}, exportedServices.Services)
})
})
@ -1094,12 +1094,12 @@ func TestStreamResources_Server_ServiceUpdates(t *testing.T) {
msg, err := client.RecvWithTimeout(100 * time.Millisecond)
require.NoError(r, err)
require.Equal(r, pbpeerstream.TypeURLExportedServiceList, msg.GetResponse().ResourceURL)
require.Equal(t, subExportedServiceList, msg.GetResponse().ResourceID)
require.Equal(t, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation)
require.Equal(r, subExportedServiceList, msg.GetResponse().ResourceID)
require.Equal(r, pbpeerstream.Operation_OPERATION_UPSERT, msg.GetResponse().Operation)
var exportedServices pbpeerstream.ExportedServiceList
require.NoError(t, msg.GetResponse().Resource.UnmarshalTo(&exportedServices))
require.Len(t, exportedServices.Services, 0)
require.NoError(r, msg.GetResponse().Resource.UnmarshalTo(&exportedServices))
require.Len(r, exportedServices.Services, 0)
})
})
}

@ -96,7 +96,7 @@ func (s *ServerResolverBuilder) Build(target resolver.Target, cc resolver.Client
}
//nolint:staticcheck
serverType, datacenter, err := parseEndpoint(target.Endpoint)
serverType, datacenter, err := parseEndpoint(target.Endpoint())
if err != nil {
return nil, err
}

@ -3,6 +3,7 @@ package resolver
import (
"fmt"
"net"
"net/url"
"strings"
"testing"
@ -40,7 +41,7 @@ func TestServerResolverBuilder(t *testing.T) {
_, err := rs.Build(resolver.Target{
Scheme: "consul",
Authority: rs.Authority(),
Endpoint: endpoint,
URL: url.URL{Opaque: endpoint},
}, cc, resolver.BuildOptions{})
require.NoError(t, err)

@ -3,7 +3,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc-gen-go v1.30.0
// protoc (unknown)
// source: simple.proto

@ -115,15 +115,7 @@ func (c *hcpClient) FetchTelemetryConfig(ctx context.Context) (*TelemetryConfig,
return nil, err
}
payloadConfig := resp.Payload.TelemetryConfig
return &TelemetryConfig{
Endpoint: payloadConfig.Endpoint,
Labels: payloadConfig.Labels,
MetricsConfig: &MetricsConfig{
Filters: payloadConfig.Metrics.IncludeList,
Endpoint: payloadConfig.Metrics.Endpoint,
},
}, nil
return convertTelemetryConfig(resp)
}
func (c *hcpClient) FetchBootstrap(ctx context.Context) (*BootstrapConfig, error) {
@ -281,6 +273,29 @@ func (c *hcpClient) DiscoverServers(ctx context.Context) ([]string, error) {
return servers, nil
}
// convertTelemetryConfig validates the AgentTelemetryConfig payload and converts it into a TelemetryConfig object.
func convertTelemetryConfig(resp *hcptelemetry.AgentTelemetryConfigOK) (*TelemetryConfig, error) {
if resp.Payload == nil {
return nil, fmt.Errorf("missing payload")
}
if resp.Payload.TelemetryConfig == nil {
return nil, fmt.Errorf("missing telemetry config")
}
payloadConfig := resp.Payload.TelemetryConfig
var metricsConfig MetricsConfig
if payloadConfig.Metrics != nil {
metricsConfig.Endpoint = payloadConfig.Metrics.Endpoint
metricsConfig.Filters = payloadConfig.Metrics.IncludeList
}
return &TelemetryConfig{
Endpoint: payloadConfig.Endpoint,
Labels: payloadConfig.Labels,
MetricsConfig: &metricsConfig,
}, nil
}
// Enabled verifies if telemetry is enabled by ensuring a valid endpoint has been retrieved.
// It returns full metrics endpoint and true if a valid endpoint was obtained.
func (t *TelemetryConfig) Enabled() (string, bool) {

@ -4,6 +4,8 @@ import (
"context"
"testing"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-telemetry-gateway/preview/2023-04-14/client/consul_telemetry_service"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-telemetry-gateway/preview/2023-04-14/models"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
@ -73,3 +75,75 @@ func TestFetchTelemetryConfig(t *testing.T) {
})
}
}
func TestConvertTelemetryConfig(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
resp *consul_telemetry_service.AgentTelemetryConfigOK
expectedTelemetryCfg *TelemetryConfig
wantErr string
}{
"success": {
resp: &consul_telemetry_service.AgentTelemetryConfigOK{
Payload: &models.HashicorpCloudConsulTelemetry20230414AgentTelemetryConfigResponse{
TelemetryConfig: &models.HashicorpCloudConsulTelemetry20230414TelemetryConfig{
Endpoint: "https://test.com",
Labels: map[string]string{"test": "test"},
},
},
},
expectedTelemetryCfg: &TelemetryConfig{
Endpoint: "https://test.com",
Labels: map[string]string{"test": "test"},
MetricsConfig: &MetricsConfig{},
},
},
"successWithMetricsConfig": {
resp: &consul_telemetry_service.AgentTelemetryConfigOK{
Payload: &models.HashicorpCloudConsulTelemetry20230414AgentTelemetryConfigResponse{
TelemetryConfig: &models.HashicorpCloudConsulTelemetry20230414TelemetryConfig{
Endpoint: "https://test.com",
Labels: map[string]string{"test": "test"},
Metrics: &models.HashicorpCloudConsulTelemetry20230414TelemetryMetricsConfig{
Endpoint: "https://metrics-test.com",
IncludeList: []string{"consul.raft.apply"},
},
},
},
},
expectedTelemetryCfg: &TelemetryConfig{
Endpoint: "https://test.com",
Labels: map[string]string{"test": "test"},
MetricsConfig: &MetricsConfig{
Endpoint: "https://metrics-test.com",
Filters: []string{"consul.raft.apply"},
},
},
},
"errorsWithNilPayload": {
resp: &consul_telemetry_service.AgentTelemetryConfigOK{},
wantErr: "missing payload",
},
"errorsWithNilTelemetryConfig": {
resp: &consul_telemetry_service.AgentTelemetryConfigOK{
Payload: &models.HashicorpCloudConsulTelemetry20230414AgentTelemetryConfigResponse{},
},
wantErr: "missing telemetry config",
},
} {
test := test
t.Run(name, func(t *testing.T) {
t.Parallel()
telemetryCfg, err := convertTelemetryConfig(test.resp)
if test.wantErr != "" {
require.Error(t, err)
require.Nil(t, telemetryCfg)
require.Contains(t, err.Error(), test.wantErr)
return
}
require.NoError(t, err)
require.Equal(t, test.expectedTelemetryCfg, telemetryCfg)
})
}
}

@ -983,9 +983,12 @@ func parseConsistencyReadRequest(resp http.ResponseWriter, req *http.Request, b
}
}
// parseDC is used to parse the ?dc query param
// parseDC is used to parse the datacenter from the query params.
// ?datacenter has precedence over ?dc.
func (s *HTTPHandlers) parseDC(req *http.Request, dc *string) {
if other := req.URL.Query().Get("dc"); other != "" {
if other := req.URL.Query().Get("datacenter"); other != "" {
*dc = other
} else if other = req.URL.Query().Get("dc"); other != "" {
*dc = other
} else if *dc == "" {
*dc = s.agent.config.Datacenter

@ -29,6 +29,7 @@ func init() {
registerEndpoint("/v1/agent/token/", []string{"PUT"}, (*HTTPHandlers).AgentToken)
registerEndpoint("/v1/agent/self", []string{"GET"}, (*HTTPHandlers).AgentSelf)
registerEndpoint("/v1/agent/host", []string{"GET"}, (*HTTPHandlers).AgentHost)
registerEndpoint("/v1/agent/version", []string{"GET"}, (*HTTPHandlers).AgentVersion)
registerEndpoint("/v1/agent/maintenance", []string{"PUT"}, (*HTTPHandlers).AgentNodeMaintenance)
registerEndpoint("/v1/agent/reload", []string{"PUT"}, (*HTTPHandlers).AgentReload)
registerEndpoint("/v1/agent/monitor", []string{"GET"}, (*HTTPHandlers).AgentMonitor)

@ -881,6 +881,15 @@ func TestParseSource(t *testing.T) {
t.Fatalf("bad: %v", source)
}
// We should follow whatever datacenter parameter was given so that the node is
// looked up correctly on the receiving end.
req, _ = http.NewRequest("GET", "/v1/catalog/nodes?near=bob&datacenter=foo", nil)
source = structs.QuerySource{}
a.srv.parseSource(req, &source)
if source.Datacenter != "foo" || source.Node != "bob" {
t.Fatalf("bad: %v", source)
}
// The magic "_agent" node name will use the agent's local node name.
req, _ = http.NewRequest("GET", "/v1/catalog/nodes?near=_agent", nil)
source = structs.QuerySource{}

@ -13,20 +13,20 @@ import (
"sync/atomic"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/stringslice"
"github.com/hashicorp/consul/types"
"github.com/armon/go-metrics"
"github.com/armon/go-metrics/prometheus"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/copystructure"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/types"
)
var StateCounters = []prometheus.CounterDefinition{
@ -1252,6 +1252,7 @@ func (l *State) SyncChanges() error {
}
}
var errs error
// Sync the services
// (logging happens in the helper methods)
for id, s := range l.services {
@ -1265,7 +1266,7 @@ func (l *State) SyncChanges() error {
l.logger.Debug("Service in sync", "service", id.String())
}
if err != nil {
return err
errs = multierror.Append(errs, err)
}
}
@ -1286,10 +1287,10 @@ func (l *State) SyncChanges() error {
l.logger.Debug("Check in sync", "check", id.String())
}
if err != nil {
return err
errs = multierror.Append(errs, err)
}
}
return nil
return errs
}
// deleteService is used to delete a service from the server

@ -1320,13 +1320,13 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
require.Equal(t, chk, chk1)
require.Equal(r, chk, chk1)
case "redis":
require.Equal(t, chk, chk2)
require.Equal(r, chk, chk2)
case "web":
require.Equal(t, chk, chk3)
require.Equal(r, chk, chk3)
case "cache":
require.Equal(t, chk, chk5)
require.Equal(r, chk, chk5)
case "serfHealth":
// ignore
default:
@ -1356,9 +1356,9 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
assert.Equal(t, a.Config.NodeID, id)
assert.Equal(t, a.Config.TaggedAddresses, addrs)
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
assert.Equal(r, a.Config.NodeID, id)
assert.Equal(r, a.Config.TaggedAddresses, addrs)
assert.Equal(r, unNilMap(a.Config.NodeMeta), meta)
}
})
retry.Run(t, func(r *retry.R) {
@ -1385,11 +1385,11 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
chk.CreateIndex, chk.ModifyIndex = 0, 0
switch chk.CheckID {
case "mysql":
require.Equal(t, chk1, chk)
require.Equal(r, chk1, chk)
case "web":
require.Equal(t, chk3, chk)
require.Equal(r, chk3, chk)
case "cache":
require.Equal(t, chk5, chk)
require.Equal(r, chk5, chk)
case "serfHealth":
// ignore
default:

@ -56,6 +56,7 @@ func TestOperator_Usage(t *testing.T) {
Services: 5,
ServiceInstances: 6,
ConnectServiceInstances: map[string]int{
"api-gateway": 0,
"connect-native": 1,
"connect-proxy": 1,
"ingress-gateway": 0,

@ -5,6 +5,7 @@ package proxycfgglue
import (
"context"
"errors"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
@ -141,6 +142,12 @@ func newUpdateEvent(correlationID string, result any, err error) proxycfg.Update
if acl.IsErrNotFound(err) {
err = proxycfg.TerminalError(err)
}
// these are also errors where we should mark them
// as terminal for the sake of proxycfg, since they require
// a resubscribe.
if errors.Is(err, stream.ErrSubForceClosed) || errors.Is(err, stream.ErrShuttingDown) {
err = proxycfg.TerminalError(err)
}
return proxycfg.UpdateEvent{
CorrelationID: correlationID,
Result: result,

@ -15,7 +15,6 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/proxycfg/internal/watch"
"github.com/hashicorp/consul/agent/structs"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save