// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 //go:build !consulent package xds import ( "path/filepath" "sort" "testing" "time" 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/go-hclog" goversion "github.com/hashicorp/go-version" testinf "github.com/mitchellh/go-testing-interface" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" propertyoverride "github.com/hashicorp/consul/agent/envoyextensions/builtin/property-override" "github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/xds/extensionruntime" "github.com/hashicorp/consul/agent/xds/response" "github.com/hashicorp/consul/agent/xds/testcommon" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/envoyextensions/extensioncommon" "github.com/hashicorp/consul/envoyextensions/xdscommon" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/version" ) func TestEnvoyExtenderWithSnapshot(t *testing.T) { consulVersion, _ := goversion.NewVersion(version.Version) // If opposite is true, the returned service defaults config entry will have // payload-passthrough=true and invocation-mode=asynchronous. // Otherwise payload-passthrough=false and invocation-mode=synchronous. // This is used to test all the permutations. makeLambdaServiceDefaults := func(opposite bool) *structs.ServiceConfigEntry { payloadPassthrough := true if opposite { payloadPassthrough = false } invocationMode := "synchronous" if opposite { invocationMode = "asynchronous" } return &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", EnvoyExtensions: []structs.EnvoyExtension{ { Name: api.BuiltinAWSLambdaExtension, Arguments: map[string]interface{}{ "ARN": "arn:aws:lambda:us-east-1:111111111111:function:lambda-1234", "PayloadPassthrough": payloadPassthrough, "InvocationMode": invocationMode, }, }, }, } } // Apply Lua extension to the local service and ensure http is used so the extension can be applied. makeLuaNsFunc := func(inbound bool, envoyVersion, consulVersion string) func(ns *structs.NodeService) { listener := "inbound" if !inbound { listener = "outbound" } return func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "http" ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ { Name: api.BuiltinLuaExtension, EnvoyVersion: envoyVersion, ConsulVersion: consulVersion, Arguments: map[string]interface{}{ "ProxyType": "connect-proxy", "Listener": listener, "Script": ` function envoy_on_request(request_handle) request_handle:headers():add("test", "test") end`, }, }, } } } // Apply Prop Override extension to the local service and ensure http is used so the extension can be applied. makePropOverrideNsFunc := func(args map[string]interface{}) func(ns *structs.NodeService) { return func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "http" if _, ok := args["ProxyType"]; !ok { args["ProxyType"] = api.ServiceKindConnectProxy } ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ { Name: api.BuiltinPropertyOverrideExtension, Arguments: args, }, } } } propertyOverrideServiceDefaultsAddOutlierDetectionSingle := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/outlier_detection/success_rate_minimum_hosts", "Value": 1234, }, }, }) propertyOverrideServiceDefaultsAddOutlierDetectionMultiple := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/outlier_detection", "Value": map[string]interface{}{ "success_rate_minimum_hosts": 1234, "failure_percentage_request_volume": 2345, }, }, }, }) propertyOverrideServiceDefaultsRemoveOutlierDetection := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "remove", "Path": "/outlier_detection", }, }, }) propertyOverrideServiceDefaultsAddKeepalive := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/upstream_connection_options/tcp_keepalive/keepalive_probes", "Value": 5, }, }, }) propertyOverrideServiceDefaultsAddRoundRobinLbConfig := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/round_robin_lb_config", "Value": map[string]interface{}{}, }, }, }) propertyOverrideServiceDefaultsClusterLoadAssignmentOutboundAdd := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeClusterLoadAssignment, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/policy/overprovisioning_factor", "Value": 123, }, }, }) propertyOverrideServiceDefaultsClusterLoadAssignmentInboundAdd := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeClusterLoadAssignment, "TrafficDirection": extensioncommon.TrafficDirectionInbound, }, "Op": "add", "Path": "/policy/overprovisioning_factor", "Value": 123, }, }, }) propertyOverrideServiceDefaultsListenerInboundAdd := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionInbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats", }, }, }) propertyOverrideServiceDefaultsListenerOutboundAdd := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats", }, }, }) propertyOverrideServiceDefaultsListenerOutboundDoesntApplyToInbound := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionInbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats.inbound.only", }, { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats.outbound.only", }, }, }) // Reverse order of above patches, to prove order is inconsequential propertyOverrideServiceDefaultsListenerInboundDoesntApplyToOutbound := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats.outbound.only", }, { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionInbound, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats.inbound.only", }, }, }) propertyOverridePatchSpecificUpstreamService := makePropOverrideNsFunc( map[string]interface{}{ "Patches": []map[string]interface{}{ { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeListener, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, "Services": []propertyoverride.ServiceName{ {CompoundServiceName: api.CompoundServiceName{Name: "db"}}, }, }, "Op": "add", "Path": "/stat_prefix", "Value": "custom.stats.outbound.only", }, { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeRoute, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, "Services": []propertyoverride.ServiceName{ {CompoundServiceName: api.CompoundServiceName{Name: "db"}}, }, }, "Op": "add", "Path": "/most_specific_header_mutations_wins", "Value": true, }, { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeCluster, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, "Services": []propertyoverride.ServiceName{ {CompoundServiceName: api.CompoundServiceName{Name: "db"}}, }, }, "Op": "add", "Path": "/outlier_detection/success_rate_minimum_hosts", "Value": 1234, }, { "ResourceFilter": map[string]interface{}{ "ResourceType": propertyoverride.ResourceTypeClusterLoadAssignment, "TrafficDirection": extensioncommon.TrafficDirectionOutbound, "Services": []propertyoverride.ServiceName{ {CompoundServiceName: api.CompoundServiceName{Name: "db"}}, }, }, "Op": "add", "Path": "/policy/overprovisioning_factor", "Value": 1234, }, }, }) tests := []struct { name string create func(t testinf.T) *proxycfg.ConfigSnapshot }{ { name: "propertyoverride-add-outlier-detection", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsAddOutlierDetectionSingle, nil) }, }, { name: "propertyoverride-add-outlier-detection-multiple", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsAddOutlierDetectionMultiple, nil) }, }, { name: "propertyoverride-add-keepalive", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsAddKeepalive, nil) }, }, { name: "propertyoverride-add-round-robin-lb-config", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsAddRoundRobinLbConfig, nil) }, }, { name: "propertyoverride-remove-outlier-detection", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsRemoveOutlierDetection, nil) }, }, { name: "propertyoverride-cluster-load-assignment-outbound-add", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsClusterLoadAssignmentOutboundAdd, nil) }, }, { name: "propertyoverride-cluster-load-assignment-inbound-add", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsClusterLoadAssignmentInboundAdd, nil) }, }, { name: "propertyoverride-listener-outbound-add", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsListenerOutboundAdd, nil) }, }, { name: "propertyoverride-listener-inbound-add", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsListenerInboundAdd, nil) }, }, { name: "propertyoverride-outbound-doesnt-apply-to-inbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsListenerOutboundDoesntApplyToInbound, nil) }, }, { name: "propertyoverride-inbound-doesnt-apply-to-outbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, propertyOverrideServiceDefaultsListenerInboundDoesntApplyToOutbound, nil) }, }, { name: "propertyoverride-patch-specific-upstream-service-splitter", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "splitter-with-resolver-redirect-multidc", false, propertyOverridePatchSpecificUpstreamService, nil) }, }, { name: "propertyoverride-patch-specific-upstream-service-failover", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "failover", false, propertyOverridePatchSpecificUpstreamService, nil) }, }, { name: "lambda-connect-proxy", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nil, nil, makeLambdaServiceDefaults(false)) }, }, { name: "lambda-connect-proxy-tproxy", create: func(t testinf.T) *proxycfg.ConfigSnapshot { extra := makeLambdaServiceDefaults(false) extra.Name = "google" return proxycfg.TestConfigSnapshotTransparentProxyHTTPUpstream(t, nil, extra) }, }, // Make sure that if the upstream type is different from ExtensionConfiguration.Kind is, that the resources are not patched. { name: "lambda-connect-proxy-with-terminating-gateway-upstream", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "register-to-terminating-gateway", false, nil, nil, makeLambdaServiceDefaults(false)) }, }, { name: "lambda-connect-proxy-opposite-meta", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nil, nil, makeLambdaServiceDefaults(true)) }, }, { name: "lambda-terminating-gateway", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotTerminatingGatewayWithLambdaService(t) }, }, { name: "lambda-terminating-gateway-with-service-resolvers", create: proxycfg.TestConfigSnapshotTerminatingGatewayWithLambdaServiceAndServiceResolvers, }, { name: "lua-outbound-doesnt-apply-to-local-upstreams-with-envoy-constraint-violation", create: func(t testinf.T) *proxycfg.ConfigSnapshot { // upstreams need to be http in order for lua to be applied to listeners. return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false, "< 1.0.0", ">= 1.0.0"), nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "geo-cache", Protocol: "http", }) }, }, { name: "lua-outbound-doesnt-apply-to-local-upstreams-with-consul-constraint-violation", create: func(t testinf.T) *proxycfg.ConfigSnapshot { // upstreams need to be http in order for lua to be applied to listeners. return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false, ">= 1.0.0", "< 1.0.0"), nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "geo-cache", Protocol: "http", }) }, }, { name: "lua-outbound-applies-to-local-upstreams", create: func(t testinf.T) *proxycfg.ConfigSnapshot { // upstreams need to be http in order for lua to be applied to listeners. return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false, ">= 1.0.0", ">= 1.0.0"), nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "geo-cache", Protocol: "http", }) }, }, { // We expect an inbound public listener lua filter here because the extension targets inbound. // The only difference between goldens for this and lua-inbound-applies-to-inbound // should be that db has HTTP filters rather than TCP. name: "lua-inbound-doesnt-apply-to-local-upstreams", create: func(t testinf.T) *proxycfg.ConfigSnapshot { // db is made an HTTP upstream so that the extension _could_ apply, but does not because // the direction for the extension is inbound. return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(true, "", ""), nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", }) }, }, { name: "lua-inbound-applies-to-inbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(true, "", ""), nil) }, }, { // We expect _no_ lua filters here, because the extension targets outbound, but there are // no upstream HTTP services. We also should not see public listener, which is HTTP, patched. name: "lua-outbound-doesnt-apply-to-inbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false, "", ""), nil) }, }, { name: "lua-outbound-applies-to-local-upstreams-tproxy", create: func(t testinf.T) *proxycfg.ConfigSnapshot { // upstreams need to be http in order for lua to be applied to listeners. return proxycfg.TestConfigSnapshotTransparentProxyDestinationHTTP(t, makeLuaNsFunc(false, "", "")) }, }, { name: "lua-connect-proxy-with-terminating-gateway-upstream", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "register-to-terminating-gateway", false, nil, nil, makeLambdaServiceDefaults(false)) }, }, { name: "lambda-and-lua-connect-proxy", create: func(t testinf.T) *proxycfg.ConfigSnapshot { nsFunc := func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "http" ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ { Name: api.BuiltinLuaExtension, Arguments: map[string]interface{}{ "ProxyType": "connect-proxy", "Listener": "inbound", "Script": ` function envoy_on_request(request_handle) request_handle:headers():add("test", "test") end`, }, }, } } return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nsFunc, nil, makeLambdaServiceDefaults(true)) }, }, { name: "wasm-http-local-file", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "http" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("http", "inbound", "local") }, nil) }, }, { name: "wasm-http-remote-file", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "http" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("http", "inbound", "remote") }, nil) }, }, { name: "wasm-tcp-local-file", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "tcp" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("tcp", "inbound", "local") }, nil) }, }, { name: "wasm-tcp-remote-file", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "tcp" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("tcp", "inbound", "remote") }, nil) }, }, { name: "wasm-tcp-local-file-outbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "tcp" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("tcp", "outbound", "local") }, nil) }, }, { name: "wasm-tcp-remote-file-outbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config["protocol"] = "tcp" ns.Proxy.EnvoyExtensions = makeWasmEnvoyExtension("tcp", "outbound", "remote") }, nil) }, }, { // Insert an HTTP ext_authz filter at the start of the filter chain with the default gRPC config options. name: "ext-authz-http-local-grpc-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "http"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension( "grpc", "dest=local", ) }, nil) }, }, { // Insert an ext_authz HTTP filter after all the header_to_metadata filters, with the default HTTP config options. name: "ext-authz-http-local-http-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "http"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension( "http", "dest=local", "target-uri=localhost:9191", "insert=AfterLastMatch:envoy.filters.http.header_to_metadata", ) }, nil) }, }, { // Insert an ext_authz HTTP filter before the router filter, specifying all gRPC config options. name: "ext-authz-http-upstream-grpc-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "http"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension( "grpc", "required=true", "dest=upstream", "insert=BeforeFirstMatch:envoy.filters.http.router", "config-type=full", ) }, nil) }, }, { // Insert an ext_authz HTTP filter after intentions, specifying all HTTP config options. name: "ext-authz-http-upstream-http-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "http"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension( "http", "required=true", "dest=upstream", "insert=AfterLastMatch:envoy.filters.http.rbac", "config-type=full", ) }, nil) }, }, { // Insert an ext_authz TCP filter at the start of the filter chain, with the default gRPC config options. name: "ext-authz-tcp-local-grpc-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "tcp"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension("grpc") }, nil) }, }, { // Insert an ext_authz TCP filter after intentions, specifying all gRPC config options. name: "ext-authz-tcp-upstream-grpc-service", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "tcp"} ns.Proxy.EnvoyExtensions = makeExtAuthzEnvoyExtension( "grpc", "required=true", "dest=upstream", "insert=AfterLastMatch:envoy.filters.network.rbac", "config-type=full", ) }, nil) }, }, { // Insert an OpenTelemetry access logging extension name: "otel-access-logging-http", create: func(t testinf.T) *proxycfg.ConfigSnapshot { return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { ns.Proxy.Config = map[string]any{"protocol": "http"} ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ { Name: "builtin/otel-access-logging", Arguments: map[string]any{ "Config": map[string]any{ "LogName": "otel-logger", "GrpcService": map[string]any{ "Target": map[string]any{ "Service": map[string]any{ "Name": "db", }, }, }, "BufferFlushInterval": time.Millisecond, "BufferSizeBytes": 4096, "FilterStateObjectsToLog": []string{"obj-1", "obj-2"}, "RetryPolicy": map[string]any{ "RetryBackOff": map[string]any{ "BaseInterval": time.Second, "MaxInterval": 10 * time.Second, }, "NumRetries": 5, }, // Note: only test with one entry in Attributes and ResourceAttributes because // they are maps and multiple entries results in the output order being // non-deterministic. "Attributes": map[string]any{ "name": "Bugs Bunny", }, "ResourceAttributes": map[string]any{ "type": "compute", }, }, }, Required: true, }, } }, nil) }, }, } latestEnvoyVersion := xdscommon.EnvoyVersions[0] for _, envoyVersion := range xdscommon.EnvoyVersions { parsedEnvoyVersion, _ := goversion.NewVersion(envoyVersion) sf, err := xdscommon.DetermineSupportedProxyFeaturesFromString(envoyVersion) require.NoError(t, err) t.Run("envoy-"+envoyVersion, func(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Sanity check default with no overrides first snap := tt.create(t) // We need to replace the TLS certs with deterministic ones to make golden // files workable. Note we don't update these otherwise they'd change // golden files for every test case and so not be any use! testcommon.SetupTLSRootsAndLeaf(t, snap) g := NewResourceGenerator(testutil.Logger(t), nil, false) g.ProxyFeatures = sf res, err := g.AllResourcesFromSnapshot(snap) require.NoError(t, err) indexedResources := xdscommon.IndexResources(g.Logger, res) cfgs := extensionruntime.GetRuntimeConfigurations(snap) for _, extensions := range cfgs { for _, ext := range extensions { indexedResources, err = validateAndApplyEnvoyExtension(hclog.NewNullLogger(), snap, indexedResources, ext, parsedEnvoyVersion, consulVersion) require.NoError(t, err) } } entities := []struct { name string key string sorter func([]proto.Message) func(int, int) bool }{ { name: "clusters", key: xdscommon.ClusterType, sorter: func(msgs []proto.Message) func(int, int) bool { return func(i, j int) bool { return msgs[i].(*envoy_cluster_v3.Cluster).Name < msgs[j].(*envoy_cluster_v3.Cluster).Name } }, }, { name: "listeners", key: xdscommon.ListenerType, sorter: func(msgs []proto.Message) func(int, int) bool { return func(i, j int) bool { return msgs[i].(*envoy_listener_v3.Listener).Name < msgs[j].(*envoy_listener_v3.Listener).Name } }, }, { name: "routes", key: xdscommon.RouteType, sorter: func(msgs []proto.Message) func(int, int) bool { return func(i, j int) bool { return msgs[i].(*envoy_route_v3.RouteConfiguration).Name < msgs[j].(*envoy_route_v3.RouteConfiguration).Name } }, }, { name: "endpoints", key: xdscommon.EndpointType, sorter: func(msgs []proto.Message) func(int, int) bool { return func(i, j int) bool { return msgs[i].(*envoy_endpoint_v3.ClusterLoadAssignment).ClusterName < msgs[j].(*envoy_endpoint_v3.ClusterLoadAssignment).ClusterName } }, }, } for _, entity := range entities { var msgs []proto.Message for _, e := range indexedResources.Index[entity.key] { msgs = append(msgs, e) } sort.Slice(msgs, entity.sorter(msgs)) r, err := response.CreateResponse(entity.key, "00000001", "00000001", msgs) require.NoError(t, err) t.Run(entity.name, func(t *testing.T) { gotJSON := protoToJSON(t, r) require.JSONEq(t, goldenEnvoy(t, filepath.Join("builtin_extension", entity.name, tt.name), envoyVersion, latestEnvoyVersion, gotJSON), gotJSON) }) } }) } }) } }