From f9126b6c3a0aa0bc7e730df9ea8a5df8aaccaf4e Mon Sep 17 00:00:00 2001 From: Chris Thain <32781396+cthain@users.noreply.github.com> Date: Thu, 6 Apr 2023 14:12:07 -0700 Subject: [PATCH] Wasm Envoy HTTP extension (#16877) --- .changelog/16877.txt | 3 + .../builtin/aws-lambda/aws_lambda.go | 6 +- .../builtin/aws-lambda/aws_lambda_test.go | 2 +- .../builtin/http/localratelimit/copied.go | 61 -- .../builtin/http/localratelimit/ratelimit.go | 4 +- agent/envoyextensions/builtin/lua/copied.go | 61 -- agent/envoyextensions/builtin/lua/lua.go | 4 +- agent/envoyextensions/builtin/wasm/structs.go | 435 ++++++++++++ agent/envoyextensions/builtin/wasm/wasm.go | 143 ++++ .../envoyextensions/builtin/wasm/wasm_test.go | 639 ++++++++++++++++++ .../envoyextensions/registered_extensions.go | 2 + agent/xds/delta_envoy_extender_oss_test.go | 60 ++ agent/xds/extensionruntime/runtime_config.go | 5 +- .../runtime_config_oss_test.go | 22 +- .../wasm-http-local-file.latest.golden | 145 ++++ .../wasm-http-remote-file.latest.golden | 145 ++++ .../wasm-http-local-file.latest.golden | 75 ++ .../wasm-http-remote-file.latest.golden | 75 ++ .../wasm-http-local-file.latest.golden | 271 ++++++++ .../wasm-http-remote-file.latest.golden | 276 ++++++++ .../routes/wasm-http-local-file.latest.golden | 5 + .../wasm-http-remote-file.latest.golden | 5 + api/config_entry.go | 1 + .../extensioncommon/resources.go | 22 +- .../extensioncommon/runtime_config.go | 20 +- .../envoy/case-mesh-to-lambda/verify.bats | 4 +- .../connect/envoy/case-wasm/capture.sh | 4 + .../connect/envoy/case-wasm/data/dummy.wasm | 3 + .../connect/envoy/case-wasm/service_s1.hcl | 16 + .../connect/envoy/case-wasm/service_s2.hcl | 5 + .../connect/envoy/case-wasm/setup.sh | 33 + .../connect/envoy/case-wasm/vars.sh | 3 + .../connect/envoy/case-wasm/verify.bats | 25 + test/integration/connect/envoy/helpers.bash | 24 +- 34 files changed, 2440 insertions(+), 164 deletions(-) create mode 100644 .changelog/16877.txt delete mode 100644 agent/envoyextensions/builtin/http/localratelimit/copied.go delete mode 100644 agent/envoyextensions/builtin/lua/copied.go create mode 100644 agent/envoyextensions/builtin/wasm/structs.go create mode 100644 agent/envoyextensions/builtin/wasm/wasm.go create mode 100644 agent/envoyextensions/builtin/wasm/wasm_test.go create mode 100644 agent/xds/testdata/builtin_extension/clusters/wasm-http-local-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/clusters/wasm-http-remote-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/endpoints/wasm-http-local-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/endpoints/wasm-http-remote-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/listeners/wasm-http-local-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/listeners/wasm-http-remote-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/routes/wasm-http-local-file.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/routes/wasm-http-remote-file.latest.golden rename agent/envoyextensions/builtin/aws-lambda/copied.go => envoyextensions/extensioncommon/resources.go (66%) create mode 100644 test/integration/connect/envoy/case-wasm/capture.sh create mode 100644 test/integration/connect/envoy/case-wasm/data/dummy.wasm create mode 100644 test/integration/connect/envoy/case-wasm/service_s1.hcl create mode 100644 test/integration/connect/envoy/case-wasm/service_s2.hcl create mode 100644 test/integration/connect/envoy/case-wasm/setup.sh create mode 100644 test/integration/connect/envoy/case-wasm/vars.sh create mode 100644 test/integration/connect/envoy/case-wasm/verify.bats diff --git a/.changelog/16877.txt b/.changelog/16877.txt new file mode 100644 index 000000000..b27c04ca4 --- /dev/null +++ b/.changelog/16877.txt @@ -0,0 +1,3 @@ +```release-note:feature +xds: Add a built-in Envoy extension that inserts Wasm HTTP filters. +``` diff --git a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go index bfc47fffd..9579baf93 100644 --- a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go +++ b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go @@ -95,7 +95,7 @@ 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) { - transportSocket, err := makeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{ + transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{ Sni: "*.amazonaws.com", }) @@ -169,7 +169,7 @@ func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_ return filter, false, errors.New("error unmarshalling filter") } - lambdaHttpFilter, err := makeEnvoyHTTPFilter( + lambdaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter( "envoy.filters.http.aws_lambda", &envoy_lambda_v3.Config{ Arn: a.ARN, @@ -204,7 +204,7 @@ func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_ config.StripPortMode = &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{ StripAnyHostPort: true, } - newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config) + newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config) if err != nil { return filter, false, errors.New("error making new filter") } diff --git a/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go b/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go index b5563fb33..1b8bb222e 100644 --- a/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go +++ b/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go @@ -152,7 +152,7 @@ func TestPatchCluster(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - transportSocket, err := makeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{ + transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{ Sni: "*.amazonaws.com", }) require.NoError(t, err) diff --git a/agent/envoyextensions/builtin/http/localratelimit/copied.go b/agent/envoyextensions/builtin/http/localratelimit/copied.go deleted file mode 100644 index ea18f1eda..000000000 --- a/agent/envoyextensions/builtin/http/localratelimit/copied.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package localratelimit - -import ( - envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" -) - -// This is copied from xds and not put into the shared package because I'm not -// convinced it should be shared. - -func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) { - if tlsContext == nil { - return nil, nil - } - return makeTransportSocket("tls", tlsContext) -} - -func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) { - any, err := anypb.New(config) - if err != nil { - return nil, err - } - return &envoy_core_v3.TransportSocket{ - Name: name, - ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{ - TypedConfig: any, - }, - }, nil -} - -func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_http_v3.HttpFilter{ - Name: name, - ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any}, - }, nil -} - -func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_listener_v3.Filter{ - Name: name, - ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any}, - }, nil -} diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go index c5940b6de..0fd289ec3 100644 --- a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go +++ b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go @@ -162,7 +162,7 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l } } - ratelimitHttpFilter, err := makeEnvoyHTTPFilter( + ratelimitHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter( "envoy.filters.http.local_ratelimit", &envoy_ratelimit.LocalRateLimit{ TokenBucket: &tokenBucket, @@ -184,7 +184,7 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l changedFilters = append(changedFilters, config.HttpFilters...) config.HttpFilters = changedFilters - newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config) + newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config) if err != nil { return filter, false, errors.New("error making new filter") } diff --git a/agent/envoyextensions/builtin/lua/copied.go b/agent/envoyextensions/builtin/lua/copied.go deleted file mode 100644 index 162d0fa91..000000000 --- a/agent/envoyextensions/builtin/lua/copied.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package lua - -import ( - envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" -) - -// This is copied from xds and not put into the shared package because I'm not -// convinced it should be shared. - -func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) { - if tlsContext == nil { - return nil, nil - } - return makeTransportSocket("tls", tlsContext) -} - -func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) { - any, err := anypb.New(config) - if err != nil { - return nil, err - } - return &envoy_core_v3.TransportSocket{ - Name: name, - ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{ - TypedConfig: any, - }, - }, nil -} - -func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_http_v3.HttpFilter{ - Name: name, - ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any}, - }, nil -} - -func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) { - any, err := anypb.New(cfg) - if err != nil { - return nil, err - } - - return &envoy_listener_v3.Filter{ - Name: name, - ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any}, - }, nil -} diff --git a/agent/envoyextensions/builtin/lua/lua.go b/agent/envoyextensions/builtin/lua/lua.go index f331be42c..6e5eeabca 100644 --- a/agent/envoyextensions/builtin/lua/lua.go +++ b/agent/envoyextensions/builtin/lua/lua.go @@ -94,7 +94,7 @@ func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listen if config == nil { return filter, false, errors.New("error unmarshalling filter") } - luaHttpFilter, err := makeEnvoyHTTPFilter( + luaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter( "envoy.filters.http.lua", &envoy_lua_v3.Lua{ InlineCode: l.Script, @@ -124,7 +124,7 @@ func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listen config.HttpFilters = changedFilters } - newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config) + newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config) if err != nil { return filter, false, errors.New("error making new filter") } diff --git a/agent/envoyextensions/builtin/wasm/structs.go b/agent/envoyextensions/builtin/wasm/structs.go new file mode 100644 index 000000000..f6cbdb3c6 --- /dev/null +++ b/agent/envoyextensions/builtin/wasm/structs.go @@ -0,0 +1,435 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package wasm + +import ( + "fmt" + "net/url" + "time" + + envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/extensioncommon" + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// wasmConfig defines the configuration for a Wasm Envoy extension. +type wasmConfig struct { + // Protocol is the type of Wasm filter to apply, "tcp" or "http". + Protocol string + // ProxyType identifies the type of Envoy proxy that this extension applies to. + // The extension will only be configured for proxies that match this type and + // will be ignored for all other proxy types. + ProxyType api.ServiceKind + // ListenerType identifies the listener type the filter will be applied to. + ListenerType string + // PluginConfig holds the configuration for the Wasm plugin. + PluginConfig pluginConfig +} + +// pluginConfig defines a Wasm plugin configuration. +type pluginConfig struct { + // Name is the unique name for the filter in a VM. For use in identifying the + // filter if multiple filters are handled by the same VmID and RootID. + // Also used for logging/debugging. + Name string + // RootID is a unique ID for a set of filters in a VM which will share a + // RootContext and Contexts if applicable (e.g. a Wasm HttpFilter and a Wasm AccessLog). + // All filters with the same RootID and VmID will share Context(s). + RootID string + // VmConfig is the configuration for starting or finding the Wasm VM that the + // filter will run in. + VmConfig vmConfig + // Configuration holds the configuration that will be encoded as bytes and passed to + // the plugin on startup (proxy_on_configure). + Configuration string + // CapabilityRestrictionConfiguration controls the Wasm ABI capabilities available + // to the filter. + CapabilityRestrictionConfiguration capabilityRestrictionConfiguration + + // failOpen controls the behavior when a runtime error occurs during filter + // processing. + // + // If set to false runtime errors will result in a failed request. + // For TCP filters, the connection will be closed. For HTTP filters a 503 + // status is returned. + // + // If set to true, a runtime error will result in the filter being bypassed. + failOpen bool +} + +// vmConfig defines a Wasm VM configuration. +type vmConfig struct { + // VmID is an ID which will be used along with a hash of the Wasm code to + // determine which VM will be used for the plugin. All plugins which use + // the same VmID and code will use the same VM. May be left blank. + VmID string + // Runtime is the Wasm runtime type, one of: v8, wasmtime, wamr, or wavm. + Runtime string + // Code references the Wasm code that will run in the filter. + Code dataSource + // Configuration holds the configuration that will be encoded as bytes and + // passed to the plugin during VM startup (proxy_on_vm_start). + Configuration string + // EnvironmentVariables specifies environment variables to be injected to + // this VM which will be available through WASI’s environ_get and + // environ_get_sizes system calls. + EnvironmentVariables environmentVariables +} + +// dataSource defines a local or remote location where Wasm code will be loaded from. +type dataSource struct { + // Local supports loading files from a local volume. + Local localDataSource + // Remote supports loading files from a remote server. + Remote remoteDataSource +} + +// environmentVariables defines the environment variables that will be made available +// to the Wasm filter. +type environmentVariables struct { + // HostEnvKeys holds the keys of Envoy’s environment variables exposed to this VM. + // If a key exists in Envoy’s environment variables, then that key-value pair will + // be injected into the Wasm VM. If a key does not exist, it will be ignored. + HostEnvKeys []string + // KeyValues is a list of key-value pairs to be injected to this VM in the form of "KEY=VALUE". + KeyValues map[string]string +} + +// localDataSource defines a file from a local file system. +type localDataSource struct { + // Filename is the path to the Wasm file on the local file system. + Filename string +} + +// remoteDataSource defines a file from a remote file server. +type remoteDataSource struct { + // HttpURI + HttpURI httpURI + // SHA256 of the remote file. Used to validate the remote file. + SHA256 string + // RetryPolicy determines how retries are handled. + RetryPolicy retryPolicy +} + +// httpURI defines a remote file using an HTTP URI. +type httpURI struct { + // Service is the upstream service the Wasm plugin will be fetched from. + Service api.CompoundServiceName + // URI is the location of the Wasm file on the upstream service. + URI string + // Timeout sets the maximum duration that a response can take. + Timeout string + + timeout time.Duration +} + +// retryPolicy defines how to handle retries when fetching remote files. +type retryPolicy struct { + // RetryBackOff holds parameters that control retry backoff strategy. + RetryBackOff retryBackoff + // NumRetries specifies the allowed number of retries. + NumRetries int +} + +// retryBackoff holds parameters that control retry backoff strategy. +type retryBackoff struct { + // BaseInterval is the base interval to be used for the next back off + // computation. It should be greater than zero and less than or equal + // to MaxInterval. + BaseInterval string + // MaxInterval is the maximum interval between retries. + MaxInterval string + + baseInterval time.Duration + maxInterval time.Duration +} + +// capabilityRestrictionConfiguration controls Wasm capabilities available to modules. +type capabilityRestrictionConfiguration struct { + // AllowedCapabilities specifies the Wasm capabilities which will be allowed. + // Capabilities are mapped by name. The value which each capability maps to is + // currently ignored and should be left empty. + AllowedCapabilities map[string]any +} + +// newWasmConfig creates a filterConfig from the given args. +// It starts with the default wasm configuration and merges in the config +// from the given args. +func newWasmConfig(args map[string]any) (*wasmConfig, error) { + cfg := &wasmConfig{} + if err := mapstructure.Decode(args, cfg); err != nil { + return cfg, err + } + cfg.normalize() + return cfg, nil +} + +func (p *pluginConfig) asyncDataSource(rtCfg *extensioncommon.RuntimeConfig) (*envoy_core_v3.AsyncDataSource, error) { + + // Local data source + if filename := p.VmConfig.Code.Local.Filename; filename != "" { + return &envoy_core_v3.AsyncDataSource{ + Specifier: &envoy_core_v3.AsyncDataSource_Local{ + Local: &envoy_core_v3.DataSource{ + Specifier: &envoy_core_v3.DataSource_Filename{ + Filename: filename, + }, + }, + }, + }, nil + } + + // Remote data source + // For a remote file, ensure there is an upstream cluster for the host specified in the URL. + // Envoy requires an explicit cluster in order to perform the DNS lookup required to actually + // fetch the data from the upstream source. + remote := &p.VmConfig.Code.Remote + clusterSNI := "" + for service, upstream := range rtCfg.LocalUpstreams { + if service == remote.HttpURI.Service { + for sni := range upstream.SNI { + clusterSNI = sni + break + } + } + } + if clusterSNI == "" { + return nil, fmt.Errorf("no upstream found for remote service %q", remote.HttpURI.Service.Name) + } + + d := time.Second + if remote.HttpURI.timeout > 0 { + d = remote.HttpURI.timeout + } + timeout := &durationpb.Duration{Seconds: int64(d.Seconds())} + + return &envoy_core_v3.AsyncDataSource{ + Specifier: &envoy_core_v3.AsyncDataSource_Remote{ + Remote: &envoy_core_v3.RemoteDataSource{ + Sha256: remote.SHA256, + HttpUri: &envoy_core_v3.HttpUri{ + Uri: remote.HttpURI.URI, + HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{ + Cluster: clusterSNI, + }, + Timeout: timeout, + }, + RetryPolicy: p.retryPolicy(), + }, + }, + }, nil +} + +func (p *pluginConfig) capConfig() *envoy_wasm_v3.CapabilityRestrictionConfig { + if len(p.CapabilityRestrictionConfiguration.AllowedCapabilities) == 0 { + return nil + } + + allowedCaps := make(map[string]*envoy_wasm_v3.SanitizationConfig) + for key := range p.CapabilityRestrictionConfiguration.AllowedCapabilities { + allowedCaps[key] = &envoy_wasm_v3.SanitizationConfig{} + } + + return &envoy_wasm_v3.CapabilityRestrictionConfig{ + AllowedCapabilities: allowedCaps, + } +} + +func (p *pluginConfig) envoyPluginConfig(rtCfg *extensioncommon.RuntimeConfig) (*envoy_wasm_v3.PluginConfig, error) { + var err error + var pluginCfgData, vmCfgData *anypb.Any + + if p.Configuration != "" { + pluginCfgData, err = anypb.New(wrapperspb.String(p.Configuration)) + if err != nil { + return nil, fmt.Errorf("failed to encode Wasm plugin configuration: %w", err) + } + } + + if p.VmConfig.Configuration != "" { + vmCfgData, err = anypb.New(wrapperspb.String(p.VmConfig.Configuration)) + if err != nil { + return nil, fmt.Errorf("failed to encode Wasm VM configuration: %w", err) + } + } + + code, err := p.asyncDataSource(rtCfg) + if err != nil { + return nil, fmt.Errorf("failed to encode async data source configuration: %w", err) + } + + var envVars *envoy_wasm_v3.EnvironmentVariables + if len(p.VmConfig.EnvironmentVariables.HostEnvKeys) > 0 || + len(p.VmConfig.EnvironmentVariables.KeyValues) > 0 { + envVars = &envoy_wasm_v3.EnvironmentVariables{ + HostEnvKeys: p.VmConfig.EnvironmentVariables.HostEnvKeys, + KeyValues: p.VmConfig.EnvironmentVariables.KeyValues, + } + } + + return &envoy_wasm_v3.PluginConfig{ + Name: p.Name, + RootId: p.RootID, + Vm: &envoy_wasm_v3.PluginConfig_VmConfig{ + VmConfig: &envoy_wasm_v3.VmConfig{ + VmId: p.VmConfig.VmID, + Runtime: fmt.Sprintf("envoy.wasm.runtime.%s", p.VmConfig.Runtime), + Code: code, + Configuration: vmCfgData, + EnvironmentVariables: envVars, + }, + }, + Configuration: pluginCfgData, + CapabilityRestrictionConfig: p.capConfig(), + FailOpen: p.failOpen, + }, nil +} + +func (p *pluginConfig) retryPolicy() *envoy_core_v3.RetryPolicy { + remote := &p.VmConfig.Code.Remote + if remote.RetryPolicy.NumRetries <= 0 && + remote.RetryPolicy.RetryBackOff.BaseInterval == "" && + remote.RetryPolicy.RetryBackOff.MaxInterval == "" { + return nil + } + + retryPolicy := &envoy_core_v3.RetryPolicy{} + + if remote.RetryPolicy.NumRetries > 0 { + retryPolicy.NumRetries = wrapperspb.UInt32(uint32(remote.RetryPolicy.NumRetries)) + } + + var baseInterval, maxInterval *durationpb.Duration + if remote.RetryPolicy.RetryBackOff.baseInterval > 0 { + baseInterval = &durationpb.Duration{Seconds: int64(remote.RetryPolicy.RetryBackOff.baseInterval.Seconds())} + } + if remote.RetryPolicy.RetryBackOff.maxInterval > 0 { + maxInterval = &durationpb.Duration{Seconds: int64(remote.RetryPolicy.RetryBackOff.maxInterval.Seconds())} + } + + if baseInterval != nil || maxInterval != nil { + retryPolicy.RetryBackOff = &envoy_core_v3.BackoffStrategy{ + BaseInterval: baseInterval, + MaxInterval: maxInterval, + } + } + + return retryPolicy +} + +func (w *wasmConfig) normalize() { + if w.ProxyType == "" { + w.ProxyType = api.ServiceKindConnectProxy + } + + if w.PluginConfig.VmConfig.Runtime == "" { + w.PluginConfig.VmConfig.Runtime = supportedRuntimes[0] + } + + httpURI := &w.PluginConfig.VmConfig.Code.Remote.HttpURI + httpURI.Service.Namespace = acl.NamespaceOrDefault(httpURI.Service.Namespace) + httpURI.Service.Partition = acl.PartitionOrDefault(httpURI.Service.Partition) + if httpURI.timeout <= 0 { + httpURI.timeout = time.Second + } +} + +// validate ensures the filterConfig is valid or it returns an error. +// This method must be called before using the configuration. +func (w *wasmConfig) validate() error { + var err, resultErr error + if w.Protocol != "tcp" && w.Protocol != "http" { + resultErr = multierror.Append(resultErr, fmt.Errorf(`unsupported Protocol %q, expected "tcp" or "http"`, w.Protocol)) + } + if w.ProxyType != api.ServiceKindConnectProxy { + resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported ProxyType %q, only %q is supported", w.ProxyType, api.ServiceKindConnectProxy)) + } + if w.ListenerType != "inbound" && w.ListenerType != "outbound" { + resultErr = multierror.Append(resultErr, fmt.Errorf(`unsupported ListenerType %q, expected "inbound" or "outbound"`, w.ListenerType)) + } + if err = validateRuntime(w.PluginConfig.VmConfig.Runtime); err != nil { + resultErr = multierror.Append(resultErr, err) + } + + httpURI := &w.PluginConfig.VmConfig.Code.Remote.HttpURI + isLocal := w.PluginConfig.VmConfig.Code.Local.Filename != "" + isRemote := httpURI.Service.Name != "" || httpURI.URI != "" + if isLocal == isRemote { + resultErr = multierror.Append(resultErr, fmt.Errorf("VmConfig.Code must provide exactly one of Local or Remote data source")) + } + + // If the data source is Local then validation is complete. + if isLocal { + return resultErr + } + + // Validate the remote data source fields. + // Both Service and URI are required inputs for remote data sources. + // We could catch this above in the isRemote check; however, we do an explicit check + // here for UX to give the user extra feedback in case they only provide one or the other. + if httpURI.Service.Name == "" || httpURI.URI == "" { + resultErr = multierror.Append(resultErr, fmt.Errorf("both Service and URI are required for Remote data sources")) + } + if w.PluginConfig.VmConfig.Code.Remote.SHA256 == "" { + resultErr = multierror.Append(resultErr, fmt.Errorf("SHA256 checksum is required for Remote data sources")) + } + if _, err := url.Parse(httpURI.URI); err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("invalid HttpURI.URI: %w", err)) + } + if httpURI.Timeout != "" { + httpURI.timeout, err = time.ParseDuration(httpURI.Timeout) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse HttpURI.Timeout %q as a duration: %w", httpURI.Timeout, err)) + } + } + + retryPolicy := &w.PluginConfig.VmConfig.Code.Remote.RetryPolicy + if retryPolicy.NumRetries < 0 { + resultErr = multierror.Append(resultErr, fmt.Errorf("RetryPolicy.NumRetries must be greater than or equal to 0")) + } + + if retryPolicy.RetryBackOff.BaseInterval != "" { + retryPolicy.RetryBackOff.baseInterval, err = time.ParseDuration(retryPolicy.RetryBackOff.BaseInterval) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse RetryBackOff.BaseInterval %q: %w", retryPolicy.RetryBackOff.BaseInterval, err)) + } + } + if retryPolicy.RetryBackOff.MaxInterval != "" { + retryPolicy.RetryBackOff.maxInterval, err = time.ParseDuration(retryPolicy.RetryBackOff.MaxInterval) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse RetryBackOff.MaxInterval %q: %w", retryPolicy.RetryBackOff.MaxInterval, err)) + } + } + + if retryPolicy.RetryBackOff.BaseInterval != "" && retryPolicy.RetryBackOff.baseInterval <= 0 { + resultErr = multierror.Append(resultErr, fmt.Errorf("RetryBackOff.BaseInterval %q must be greater than zero and less than or equal to RetryBackOff.MaxInterval %q", + retryPolicy.RetryBackOff.BaseInterval, + retryPolicy.RetryBackOff.MaxInterval), + ) + } + if retryPolicy.RetryBackOff.MaxInterval != "" && + retryPolicy.RetryBackOff.maxInterval < retryPolicy.RetryBackOff.baseInterval { + resultErr = multierror.Append(resultErr, fmt.Errorf("RetryBackOff.MaxInterval %q must be greater than or equal to RetryBackOff.BaseInterval %q", + retryPolicy.RetryBackOff.MaxInterval, + retryPolicy.RetryBackOff.BaseInterval), + ) + } + return resultErr +} + +func validateRuntime(s string) error { + for _, rt := range supportedRuntimes { + if s == rt { + return nil + } + } + return fmt.Errorf("unsupported runtime %q", s) +} diff --git a/agent/envoyextensions/builtin/wasm/wasm.go b/agent/envoyextensions/builtin/wasm/wasm.go new file mode 100644 index 000000000..33abce137 --- /dev/null +++ b/agent/envoyextensions/builtin/wasm/wasm.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package wasm + +import ( + "errors" + "fmt" + + envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/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" + 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" + + "github.com/hashicorp/consul/api" + "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 { + name string + wasmConfig *wasmConfig +} + +var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"} + +var _ extensioncommon.BasicExtension = (*wasm)(nil) + +func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) { + w, err := construct(ext) + if err != nil { + return nil, err + } + return &extensioncommon.BasicEnvoyExtender{ + Extension: &w, + }, nil +} + +func construct(ext api.EnvoyExtension) (wasm, error) { + w := wasm{name: ext.Name} + + if w.name != api.BuiltinWasmExtension { + return w, fmt.Errorf("expected extension name %q but got %q", api.BuiltinWasmExtension, w.name) + } + + if err := w.fromArguments(ext.Arguments); err != nil { + return w, err + } + + // Configure the failure behavior for the filter. If the plugin is required, + // then filter runtime errors result in a failed request (fail "closed"). + // Otherwise, runtime errors result in the filter being skipped (fail "open"). + w.wasmConfig.PluginConfig.failOpen = !ext.Required + + return w, nil +} + +func (w *wasm) fromArguments(args map[string]any) error { + var err error + w.wasmConfig, err = newWasmConfig(args) + if err != nil { + return fmt.Errorf("error decoding extension arguments: %w", err) + } + return w.wasmConfig.validate() +} + +// 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 on the a local connect-proxy. +// It does not patch extensions for service upstreams. +func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool { + return config.IsLocal() && w.wasmConfig.ListenerType == "inbound" && + config.Kind == w.wasmConfig.ProxyType + +} + +// PatchRoute does nothing for the WASM extension. +func (w wasm) PatchRoute(_ *extensioncommon.RuntimeConfig, r *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { + return r, false, nil +} + +// PatchCluster does nothing for the WASM extension. +func (w wasm) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { + return c, false, nil +} + +// 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) (*envoy_listener_v3.Filter, bool, error) { + 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") + } + + wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg) + if err != nil { + return filter, false, fmt.Errorf("failed to encode Envoy Wasm 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 + } + + 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 + } + 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 newFilter, true, nil +} diff --git a/agent/envoyextensions/builtin/wasm/wasm_test.go b/agent/envoyextensions/builtin/wasm/wasm_test.go new file mode 100644 index 000000000..a7791e3a3 --- /dev/null +++ b/agent/envoyextensions/builtin/wasm/wasm_test.go @@ -0,0 +1,639 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package wasm + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + "testing" + "time" + + envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_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_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" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/extensioncommon" + "github.com/hashicorp/consul/proto/private/prototest" +) + +func TestHttpWasmExtension(t *testing.T) { + t.Parallel() + cases := map[string]struct { + extName string + canApply bool + args func(bool) map[string]any + rtCfg func(bool) *extensioncommon.RuntimeConfig + inputFilters func() []*envoy_http_v3.HttpFilter + expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter + errStr string + debug bool + }{ + "http 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) }, + 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"}, + } + }, + }, + "local file": { + extName: api.BuiltinWasmExtension, + canApply: true, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.Protocol = "http" + cfg.ListenerType = "inbound" + cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm" + return cfg.toMap(t) + }, + rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, + 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"}, + } + }, + }, + "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) + rt.LocalUpstreams = nil + return rt + }, + inputFilters: makeTestHttpFilters, + errStr: "no upstream found for remote service", + }, + } + + 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() + 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 + } + + route, patched, err := w.PatchRoute(c.rtCfg(enterprise), nil) + require.Nil(t, route) + require.False(t, patched) + require.NoError(t, err) + + cluster, patched, err := w.PatchCluster(c.rtCfg(enterprise), nil) + require.Nil(t, cluster) + require.False(t, patched) + require.NoError(t, err) + + inputHttpConMgr := makeHttpConMgr(t, c.inputFilters()) + obsHttpConMgr, patched, err := w.PatchFilter(c.rtCfg(enterprise), inputHttpConMgr) + if c.errStr == "" { + require.NoError(t, err) + require.True(t, patched) + + 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)) + t.Logf("expFilterJSON =\n%s\n\n", protoToJSON(t, expHttpConMgr)) + t.Logf("obsfilterJSON =\n%s\n\n", protoToJSON(t, obsHttpConMgr)) + } + + prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr) + } else { + require.Contains(t, err.Error(), c.errStr) + } + + }) + } + } +} + +func TestWasmConstructor(t *testing.T) { + t.Parallel() + cases := map[string]struct { + name string + args func(bool) map[string]any + errStr string + }{ + "with no arguments": { + name: api.BuiltinWasmExtension, + args: func(_ 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) + cfg.Protocol = "invalid" + return cfg.toMap(t) + }, + errStr: `unsupported Protocol "invalid", expected "tcp" or "http"`, + }, + "invalid proxy type": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.ProxyType = "invalid" + return cfg.toMap(t) + }, + errStr: "unsupported ProxyType", + }, + "invalid listener type": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.ListenerType = "invalid" + return cfg.toMap(t) + }, + errStr: `unsupported ListenerType "invalid", expected "inbound" or "outbound"`, + }, + "invalid runtime": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Runtime = "invalid" + return cfg.toMap(t) + }, + errStr: "unsupported runtime", + }, + "both local and remote files": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(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" + return cfg.toMap(t) + }, + errStr: "VmConfig.Code must provide exactly one of Local or Remote data source", + }, + "service and uri required for remote files": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server" + return cfg.toMap(t) + }, + errStr: "both Service and URI are required for Remote data sources", + }, + "no sha for remote file": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(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) + }, + errStr: "SHA256 checksum is required for Remote data sources", + }, + "invalid url for remote file": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(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) + }, + errStr: `invalid HttpURI.URI: parse "://bogus.url.com/error": missing protocol scheme`, + }, + "decoding error": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + a := makeTestWasmConfig(ent).toMap(t) + setField(a, "PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval", 1000) + return a + }, + errStr: "got unconvertible type", + }, + "invalid http timeout": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout = "invalid" + return cfg.toMap(t) + }, + errStr: `failed to parse HttpURI.Timeout "invalid" as a duration`, + }, + "invalid num retries": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries = -1 + return cfg.toMap(t) + }, + errStr: "RetryPolicy.NumRetries must be greater than or equal to 0", + }, + "invalid base interval": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "0s" + return cfg.toMap(t) + }, + errStr: `RetryBackOff.BaseInterval "0s" must be greater than zero and less than or equal to RetryBackOff.MaxInterval`, + }, + "invalid max interval": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "10s" + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "5s" + return cfg.toMap(t) + }, + errStr: `RetryBackOff.MaxInterval "5s" must be greater than or equal to RetryBackOff.BaseInterval "10s"`, + }, + "invalid base interval duration": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "invalid" + return cfg.toMap(t) + }, + errStr: `failed to parse RetryBackOff.BaseInterval "invalid"`, + }, + "invalid max interval duration": { + name: api.BuiltinWasmExtension, + args: func(ent bool) map[string]any { + cfg := newTestWasmConfig(ent) + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "invalid" + return cfg.toMap(t) + }, + errStr: `failed to parse RetryBackOff.MaxInterval "invalid"`, + }, + "invalid extension name": { + name: "invalid", + args: func(ent bool) map[string]any { return newTestWasmConfig(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) }, + }, + } + 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), + }, + } + + 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) + } + }) + } + } +} + +type testWasmConfig struct { + Required bool + Protocol string + ProxyType string + ListenerType string + PluginConfig struct { + Name string + RootID string + VmConfig struct { + VmID string + Runtime string + Code struct { + Local struct { + Filename string + } + Remote struct { + HttpURI struct { + Service api.CompoundServiceName + URI string + Timeout string + } + SHA256 string + RetryPolicy struct { + RetryBackOff struct { + BaseInterval string + MaxInterval string + } + NumRetries int + } + } + } + Configuration string + EnvironmentVariables struct { + HostEnvKeys []string + KeyValues map[string]string + } + } + Configuration string + CapabilityRestrictionConfiguration struct { + AllowedCapabilities map[string]any + } + } +} + +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 + require.NoError(t, json.Unmarshal(c.toJSON(t), &m)) + return m +} + +func (c testWasmConfig) toJSON(t *testing.T) []byte { + t.Helper() + b, err := json.MarshalIndent(c, "", " ") + require.NoError(t, err) + return b +} + +func (cfg testWasmConfig) toHttpWasmFilter(t *testing.T) *envoy_wasm_v3.PluginConfig { + t.Helper() + var code *envoy_core_v3.AsyncDataSource + if cfg.PluginConfig.VmConfig.Code.Local.Filename != "" { + code = &envoy_core_v3.AsyncDataSource{ + Specifier: &envoy_core_v3.AsyncDataSource_Local{ + Local: &envoy_core_v3.DataSource{ + Specifier: &envoy_core_v3.DataSource_Filename{ + Filename: cfg.PluginConfig.VmConfig.Code.Local.Filename, + }, + }, + }, + } + } else { + cluster, err := url.Parse(cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI) + require.NoError(t, err) + timeout, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout) + require.NoError(t, err) + baseInterval, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval) + require.NoError(t, err) + maxInterval, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval) + require.NoError(t, err) + + code = &envoy_core_v3.AsyncDataSource{ + Specifier: &envoy_core_v3.AsyncDataSource_Remote{ + Remote: &envoy_core_v3.RemoteDataSource{ + Sha256: cfg.PluginConfig.VmConfig.Code.Remote.SHA256, + HttpUri: &envoy_core_v3.HttpUri{ + Uri: cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI, + Timeout: &durationpb.Duration{Seconds: int64(timeout.Seconds())}, + HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{ + Cluster: cluster.Host, + }, + }, + RetryPolicy: &envoy_core_v3.RetryPolicy{ + RetryBackOff: &envoy_core_v3.BackoffStrategy{ + BaseInterval: &durationpb.Duration{Seconds: int64(baseInterval.Seconds())}, + MaxInterval: &durationpb.Duration{Seconds: int64(maxInterval.Seconds())}, + }, + NumRetries: wrapperspb.UInt32(uint32(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries)), + }, + }, + }, + } + } + + var capConfig *envoy_wasm_v3.CapabilityRestrictionConfig + if len(cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities) > 0 { + caps := make(map[string]*envoy_wasm_v3.SanitizationConfig) + for cap := range cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities { + caps[cap] = &envoy_wasm_v3.SanitizationConfig{} + } + capConfig = &envoy_wasm_v3.CapabilityRestrictionConfig{AllowedCapabilities: caps} + } + + var vmConfiguration *anypb.Any + if cfg.PluginConfig.VmConfig.Configuration != "" { + vmConfiguration = makeAny(t, wrapperspb.String(cfg.PluginConfig.VmConfig.Configuration)) + } + + var envVars *envoy_wasm_v3.EnvironmentVariables + if len(cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys) > 0 || + len(cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues) > 0 { + envVars = &envoy_wasm_v3.EnvironmentVariables{ + HostEnvKeys: cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys, + KeyValues: cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues, + } + } + + var pluginConfiguration *anypb.Any + if cfg.PluginConfig.Configuration != "" { + pluginConfiguration = makeAny(t, wrapperspb.String(cfg.PluginConfig.Configuration)) + } + + rt := cfg.PluginConfig.VmConfig.Runtime + if rt == "" { + rt = supportedRuntimes[0] + } + + return &envoy_wasm_v3.PluginConfig{ + Name: cfg.PluginConfig.Name, + RootId: cfg.PluginConfig.RootID, + Vm: &envoy_wasm_v3.PluginConfig_VmConfig{ + VmConfig: &envoy_wasm_v3.VmConfig{ + VmId: cfg.PluginConfig.VmConfig.VmID, + Runtime: fmt.Sprintf("envoy.wasm.runtime.%s", rt), + Code: code, + Configuration: vmConfiguration, + EnvironmentVariables: envVars, + }, + }, + Configuration: pluginConfiguration, + FailOpen: !cfg.Required, + CapabilityRestrictionConfig: capConfig, + } +} + +func makeAny(t *testing.T, m proto.Message) *anypb.Any { + t.Helper() + v, err := anypb.New(m) + require.NoError(t, err) + return v +} + +func makeHttpConMgr(t *testing.T, filters []*envoy_http_v3.HttpFilter) *envoy_listener_v3.Filter { + t.Helper() + return &envoy_listener_v3.Filter{ + Name: "envoy.filters.network.http_connection_manager", + ConfigType: &envoy_listener_v3.Filter_TypedConfig{ + TypedConfig: makeAny(t, &envoy_http_v3.HttpConnectionManager{ + HttpFilters: filters, + }), + }, + } +} + +func makeTestHttpFilters() []*envoy_http_v3.HttpFilter { + return []*envoy_http_v3.HttpFilter{ + {Name: "one"}, + {Name: "two"}, + {Name: "envoy.filters.http.router"}, + {Name: "three"}, + } +} + +func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig { + var ns, ap string + if enterprise { + ns = "ns1" + ap = "ap1" + } + return &extensioncommon.RuntimeConfig{ + Kind: api.ServiceKindConnectProxy, + ServiceName: api.CompoundServiceName{Name: "test-service"}, + LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + { + Name: "test-file-server", + Namespace: acl.NamespaceOrDefault(ns), + Partition: acl.PartitionOrDefault(ap), + }: { + SNI: map[string]struct{}{"test-file-server": {}}, + EnvoyID: "test-file-server", + }, + }, + } +} + +func makeTestWasmConfig(enterprise bool) *testWasmConfig { + cfg := newTestWasmConfig(enterprise) + cfg.Required = false + cfg.Protocol = "http" + cfg.ProxyType = "connect-proxy" + cfg.ListenerType = "inbound" + cfg.PluginConfig.Name = "test-plugin-name" + cfg.PluginConfig.RootID = "test-root-id" + cfg.PluginConfig.VmConfig.VmID = "test-vm-id" + cfg.PluginConfig.VmConfig.Runtime = "wasmtime" + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "test-file-server" + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "https://test-file-server/plugin.wasm" + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout = "5s" + cfg.PluginConfig.VmConfig.Code.Remote.SHA256 = "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545" + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "3s" + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "15s" + cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries = 3 + cfg.PluginConfig.VmConfig.Configuration = "test-vm-configuration" + cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys = []string{"PATH"} + cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues = map[string]string{"TEST_VAR": "TEST_VAL"} + cfg.PluginConfig.Configuration = "test-plugin-configuration" + cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities = map[string]any{"proxy_on_vm_start": true} + return cfg +} + +func newTestWasmConfig(enterprise bool) *testWasmConfig { + cfg := &testWasmConfig{} + if enterprise { + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Namespace = "ns1" + cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Partition = "ap1" + } + return cfg +} + +func protoToJSON(t *testing.T, pb proto.Message) string { + t.Helper() + m := protojson.MarshalOptions{ + Indent: " ", + } + gotJSON, err := m.Marshal(pb) + require.NoError(t, err) + return string(gotJSON) +} + +func setField(m map[string]any, path string, value any) { + upsertField(m, path, value, 0) +} + +func upsertField(m map[string]any, path string, value any, index int) { + keys := strings.Split(path, ".") + key := keys[index] + + if val, ok := m[key]; ok { + // update the value + if index == len(keys)-1 { + m[key] = value + } else { + upsertField(val.(map[string]any), path, value, index+1) + } + } else { + // key does not exist so insert it + if index == len(keys)-1 { + m[key] = value + } else { + newMap := make(map[string]any) + m[key] = newMap + upsertField(newMap, path, value, index+1) + } + } +} diff --git a/agent/envoyextensions/registered_extensions.go b/agent/envoyextensions/registered_extensions.go index 2f44fe068..9e610cc23 100644 --- a/agent/envoyextensions/registered_extensions.go +++ b/agent/envoyextensions/registered_extensions.go @@ -9,6 +9,7 @@ import ( awslambda "github.com/hashicorp/consul/agent/envoyextensions/builtin/aws-lambda" "github.com/hashicorp/consul/agent/envoyextensions/builtin/http/localratelimit" "github.com/hashicorp/consul/agent/envoyextensions/builtin/lua" + "github.com/hashicorp/consul/agent/envoyextensions/builtin/wasm" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/envoyextensions/extensioncommon" "github.com/hashicorp/go-multierror" @@ -20,6 +21,7 @@ var extensionConstructors = map[string]extensionConstructor{ api.BuiltinLuaExtension: lua.Constructor, api.BuiltinAWSLambdaExtension: awslambda.Constructor, api.BuiltinLocalRatelimitExtension: localratelimit.Constructor, + api.BuiltinWasmExtension: wasm.Constructor, } // ConstructExtension attempts to lookup and build an extension from the registry with the diff --git a/agent/xds/delta_envoy_extender_oss_test.go b/agent/xds/delta_envoy_extender_oss_test.go index 8488be8c1..490bf016a 100644 --- a/agent/xds/delta_envoy_extender_oss_test.go +++ b/agent/xds/delta_envoy_extender_oss_test.go @@ -232,6 +232,66 @@ end`, }, nil) }, }, + { + 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 = []structs.EnvoyExtension{ + { + Name: api.BuiltinWasmExtension, + Arguments: map[string]interface{}{ + "Protocol": "http", + "ListenerType": "inbound", + "PluginConfig": map[string]interface{}{ + "VmConfig": map[string]interface{}{ + "Code": map[string]interface{}{ + "Local": map[string]interface{}{ + "Filename": "/path/to/extension.wasm", + }, + }, + }, + "Configuration": `{"foo": "bar"}`, + }, + }, + }, + } + }, 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 = []structs.EnvoyExtension{ + { + Name: api.BuiltinWasmExtension, + Arguments: map[string]interface{}{ + "Protocol": "http", + "ListenerType": "inbound", + "PluginConfig": map[string]interface{}{ + "VmConfig": map[string]interface{}{ + "Code": map[string]interface{}{ + "Remote": map[string]interface{}{ + "HttpURI": map[string]interface{}{ + "Service": map[string]interface{}{ + "Name": "db", + }, + "URI": "https://db/plugin.wasm", + }, + "SHA256": "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545", + }, + }, + }, + "Configuration": `{"foo": "bar"}`, + }, + }, + }, + } + }, nil) + }, + }, } latestEnvoyVersion := xdscommon.EnvoyVersions[0] diff --git a/agent/xds/extensionruntime/runtime_config.go b/agent/xds/extensionruntime/runtime_config.go index 57e60db91..31f2ce642 100644 --- a/agent/xds/extensionruntime/runtime_config.go +++ b/agent/xds/extensionruntime/runtime_config.go @@ -89,8 +89,9 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound EnvoyExtension: ext, ServiceName: localSvc, // Upstreams is nil to signify this extension is not being applied to an upstream service, but rather to the local service. - Upstreams: nil, - Kind: kind, + Upstreams: nil, + LocalUpstreams: upstreamMap, + Kind: kind, } extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg) } diff --git a/agent/xds/extensionruntime/runtime_config_oss_test.go b/agent/xds/extensionruntime/runtime_config_oss_test.go index 95b6a56b2..24c7ed6ad 100644 --- a/agent/xds/extensionruntime/runtime_config_oss_test.go +++ b/agent/xds/extensionruntime/runtime_config_oss_test.go @@ -256,8 +256,17 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { }, }, ServiceName: webService, - Upstreams: nil, Kind: api.ServiceKindConnectProxy, + Upstreams: nil, + LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + dbService: { + SNI: map[string]struct{}{ + "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, + }, + EnvoyID: "db", + OutgoingProxyKind: "connect-proxy", + }, + }, }, { EnvoyExtension: api.EnvoyExtension{ @@ -268,8 +277,17 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { }, }, ServiceName: webService, - Upstreams: nil, Kind: api.ServiceKindConnectProxy, + Upstreams: nil, + LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + dbService: { + SNI: map[string]struct{}{ + "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, + }, + EnvoyID: "db", + OutgoingProxyKind: "connect-proxy", + }, + }, }, }, }, diff --git a/agent/xds/testdata/builtin_extension/clusters/wasm-http-local-file.latest.golden b/agent/xds/testdata/builtin_extension/clusters/wasm-http-local-file.latest.golden new file mode 100644 index 000000000..2e3c9ea20 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/clusters/wasm-http-local-file.latest.golden @@ -0,0 +1,145 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" + }, + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" + } + ] + } + }, + "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connectTimeout": "5s", + "loadAssignment": { + "clusterName": "local_app", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 8080 + } + } + } + } + ] + } + ] + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/clusters/wasm-http-remote-file.latest.golden b/agent/xds/testdata/builtin_extension/clusters/wasm-http-remote-file.latest.golden new file mode 100644 index 000000000..2e3c9ea20 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/clusters/wasm-http-remote-file.latest.golden @@ -0,0 +1,145 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "commonLbConfig": { + "healthyPanicThreshold": { + + } + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db" + } + ] + } + }, + "sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "type": "EDS", + "edsClusterConfig": { + "edsConfig": { + "ads": { + + }, + "resourceApiVersion": "V3" + } + }, + "connectTimeout": "5s", + "circuitBreakers": { + + }, + "outlierDetection": { + + }, + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + }, + "matchSubjectAltNames": [ + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target" + }, + { + "exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target" + } + ] + } + }, + "sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + }, + { + "@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "name": "local_app", + "type": "STATIC", + "connectTimeout": "5s", + "loadAssignment": { + "clusterName": "local_app", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 8080 + } + } + } + } + ] + } + ] + } + } + ], + "typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/endpoints/wasm-http-local-file.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/wasm-http-local-file.latest.golden new file mode 100644 index 000000000..6e4d37bc3 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/endpoints/wasm-http-local-file.latest.golden @@ -0,0 +1,75 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.20.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + } + ], + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/endpoints/wasm-http-remote-file.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/wasm-http-remote-file.latest.golden new file mode 100644 index 000000000..6e4d37bc3 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/endpoints/wasm-http-remote-file.latest.golden @@ -0,0 +1,75 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + }, + { + "@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul", + "endpoints": [ + { + "lbEndpoints": [ + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.10.1.1", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + }, + { + "endpoint": { + "address": { + "socketAddress": { + "address": "10.20.1.2", + "portValue": 8080 + } + } + }, + "healthStatus": "HEALTHY", + "loadBalancingWeight": 1 + } + ] + } + ] + } + ], + "typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/listeners/wasm-http-local-file.latest.golden b/agent/xds/testdata/builtin_extension/listeners/wasm-http-local-file.latest.golden new file mode 100644 index 000000000..a709a6a13 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/listeners/wasm-http-local-file.latest.golden @@ -0,0 +1,271 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "db:127.0.0.1:9191", + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 9191 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.db.default.default.dc1", + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "prepared_query:geo-cache:127.10.10.10:8181", + "address": { + "socketAddress": { + "address": "127.10.10.10", + "portValue": 8181 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.prepared_query_geo-cache", + "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:0.0.0.0:9999", + "address": { + "socketAddress": { + "address": "0.0.0.0", + "portValue": 9999 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "public_listener", + "routeConfig": { + "name": "public_listener", + "virtualHosts": [ + { + "name": "public_listener", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + + } + } + }, + { + "name": "envoy.filters.http.header_to_metadata", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", + "requestRules": [ + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "trust-domain", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\1" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "partition", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\2" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "namespace", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\3" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "datacenter", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\4" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "service", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\5" + } + } + } + ] + } + }, + { + "name": "envoy.filters.http.wasm", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm", + "config": { + "vmConfig": { + "runtime": "envoy.wasm.runtime.v8", + "code": { + "local": { + "filename": "/path/to/extension.wasm" + } + } + }, + "configuration": { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "{\"foo\": \"bar\"}" + }, + "failOpen": true + } + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": { + + } + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + }, + "alpnProtocols": [ + "http/1.1" + ] + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/listeners/wasm-http-remote-file.latest.golden b/agent/xds/testdata/builtin_extension/listeners/wasm-http-remote-file.latest.golden new file mode 100644 index 000000000..e757392cf --- /dev/null +++ b/agent/xds/testdata/builtin_extension/listeners/wasm-http-remote-file.latest.golden @@ -0,0 +1,276 @@ +{ + "versionInfo": "00000001", + "resources": [ + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "db:127.0.0.1:9191", + "address": { + "socketAddress": { + "address": "127.0.0.1", + "portValue": 9191 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.db.default.default.dc1", + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "prepared_query:geo-cache:127.10.10.10:8181", + "address": { + "socketAddress": { + "address": "127.10.10.10", + "portValue": 8181 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.tcp_proxy", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy", + "statPrefix": "upstream.prepared_query_geo-cache", + "cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ], + "trafficDirection": "OUTBOUND" + }, + { + "@type": "type.googleapis.com/envoy.config.listener.v3.Listener", + "name": "public_listener:0.0.0.0:9999", + "address": { + "socketAddress": { + "address": "0.0.0.0", + "portValue": 9999 + } + }, + "filterChains": [ + { + "filters": [ + { + "name": "envoy.filters.network.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "public_listener", + "routeConfig": { + "name": "public_listener", + "virtualHosts": [ + { + "name": "public_listener", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": { + + } + } + }, + { + "name": "envoy.filters.http.header_to_metadata", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", + "requestRules": [ + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "trust-domain", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\1" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "partition", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\2" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "namespace", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\3" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "datacenter", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\4" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "service", + "regexValueRewrite": { + "pattern": { + "googleRe2": { + + }, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\5" + } + } + } + ] + } + }, + { + "name": "envoy.filters.http.wasm", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm", + "config": { + "vmConfig": { + "runtime": "envoy.wasm.runtime.v8", + "code": { + "remote": { + "httpUri": { + "uri": "https://db/plugin.wasm", + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul", + "timeout": "1s" + }, + "sha256": "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545" + } + } + }, + "configuration": { + "@type": "type.googleapis.com/google.protobuf.StringValue", + "value": "{\"foo\": \"bar\"}" + }, + "failOpen": true + } + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": { + + } + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": { + + }, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + }, + "alpnProtocols": [ + "http/1.1" + ] + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/routes/wasm-http-local-file.latest.golden b/agent/xds/testdata/builtin_extension/routes/wasm-http-local-file.latest.golden new file mode 100644 index 000000000..9c050cbe6 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/routes/wasm-http-local-file.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/routes/wasm-http-remote-file.latest.golden b/agent/xds/testdata/builtin_extension/routes/wasm-http-remote-file.latest.golden new file mode 100644 index 000000000..9c050cbe6 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/routes/wasm-http-remote-file.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/api/config_entry.go b/api/config_entry.go index 340ad771f..76e8e27e5 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -41,6 +41,7 @@ const ( BuiltinAWSLambdaExtension string = "builtin/aws/lambda" BuiltinLuaExtension string = "builtin/lua" BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit" + BuiltinWasmExtension string = "builtin/wasm" ) type ConfigEntry interface { diff --git a/agent/envoyextensions/builtin/aws-lambda/copied.go b/envoyextensions/extensioncommon/resources.go similarity index 66% rename from agent/envoyextensions/builtin/aws-lambda/copied.go rename to envoyextensions/extensioncommon/resources.go index b68efaa92..e0f084db0 100644 --- a/agent/envoyextensions/builtin/aws-lambda/copied.go +++ b/envoyextensions/extensioncommon/resources.go @@ -1,29 +1,27 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package awslambda +package extensioncommon import ( envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" ) -// This is copied from xds and not put into the shared package because I'm not -// convinced it should be shared. - -func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) { +// MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context. +func MakeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) { if tlsContext == nil { return nil, nil } - return makeTransportSocket("tls", tlsContext) + return MakeTransportSocket("tls", tlsContext) } -func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) { +// MakeTransportSocket generates an Envoy transport socket from the given proto message. +func MakeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) { any, err := anypb.New(config) if err != nil { return nil, err @@ -36,7 +34,8 @@ func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.Tran }, nil } -func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) { +// MakeEnvoyHTTPFilter generates an Envoy HTTP filter from the given proto message. +func MakeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) { any, err := anypb.New(cfg) if err != nil { return nil, err @@ -48,7 +47,8 @@ func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFil }, nil } -func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) { +// MakeFilter generates an Envoy listener filter from the given proto message. +func MakeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) { any, err := anypb.New(cfg) if err != nil { return nil, err diff --git a/envoyextensions/extensioncommon/runtime_config.go b/envoyextensions/extensioncommon/runtime_config.go index cb1836c0e..d36f389ac 100644 --- a/envoyextensions/extensioncommon/runtime_config.go +++ b/envoyextensions/extensioncommon/runtime_config.go @@ -23,10 +23,9 @@ type UpstreamData struct { VIP string } -// RuntimeConfig is the configuration for an extension attached to a service on the local proxy. Currently, it -// is only created for the local proxy's upstream service if the upstream service has an extension configured. In the -// future it will also include information about the service local to the local proxy as well. It should depend on the -// API client rather than the structs package because the API client is meant to be public. +// RuntimeConfig is the configuration for an extension attached to a service on the local proxy. +// It should depend on the API client rather than the structs package because the API client is +// meant to be public. type RuntimeConfig struct { // EnvoyExtension is the extension that will patch Envoy resources. EnvoyExtension api.EnvoyExtension @@ -39,27 +38,40 @@ type RuntimeConfig struct { // If there are no Upstreams, then EnvoyExtension is being applied to the local service's resources. Upstreams map[api.CompoundServiceName]*UpstreamData + // LocalUpstreams will only be configured if the EnvoyExtension is being applied to the local service. + LocalUpstreams map[api.CompoundServiceName]*UpstreamData + // Kind is mode the local Envoy proxy is running in. For now, only connect proxy and // terminating gateways are supported. Kind api.ServiceKind } +// IsLocal indicates if the extension configuration is for the proxy's local service. +func (ec RuntimeConfig) IsLocal() bool { + return !ec.IsUpstream() +} + +// IsUpstream indicates if the extension configuration is for an upstream service. func (ec RuntimeConfig) IsUpstream() bool { _, ok := ec.Upstreams[ec.ServiceName] return ok } +// MatchesUpstreamServiceSNI indicates if the extension configuration is for an upstream service +// that matches the given SNI. func (ec RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool { u := ec.Upstreams[ec.ServiceName] _, match := u.SNI[sni] return match } +// EnvoyID returns the unique Envoy identifier of the upstream service. func (ec RuntimeConfig) EnvoyID() string { u := ec.Upstreams[ec.ServiceName] return u.EnvoyID } +// OutgoingProxyKind returns the service kind for the outgoing listener of an upstream service. func (ec RuntimeConfig) OutgoingProxyKind() api.ServiceKind { u := ec.Upstreams[ec.ServiceName] return u.OutgoingProxyKind diff --git a/test/integration/connect/envoy/case-mesh-to-lambda/verify.bats b/test/integration/connect/envoy/case-mesh-to-lambda/verify.bats index 6a88e83f3..60363b717 100644 --- a/test/integration/connect/envoy/case-mesh-to-lambda/verify.bats +++ b/test/integration/connect/envoy/case-mesh-to-lambda/verify.bats @@ -7,7 +7,7 @@ load helpers } @test "s1 has lambda http filter for l1" { - assert_lambda_envoy_dynamic_http_filter_exists localhost:19000 l1 $AWS_LAMBDA_ARN + assert_lambda_envoy_dynamic_http_filter_exists localhost:19000 $AWS_LAMBDA_ARN } @test "terminating gateway has lambda cluster for l2" { @@ -15,7 +15,7 @@ load helpers } @test "terminating gateway has lambda http filter for l2" { - assert_lambda_envoy_dynamic_http_filter_exists localhost:20000 l2 $AWS_LAMBDA_ARN + assert_lambda_envoy_dynamic_http_filter_exists localhost:20000 $AWS_LAMBDA_ARN } @test "s1 can call l1 through its sidecar-proxy" { diff --git a/test/integration/connect/envoy/case-wasm/capture.sh b/test/integration/connect/envoy/case-wasm/capture.sh new file mode 100644 index 000000000..1a11f7d5e --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/capture.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:19000 s1 primary || true +snapshot_envoy_admin localhost:19001 s2 primary || true diff --git a/test/integration/connect/envoy/case-wasm/data/dummy.wasm b/test/integration/connect/envoy/case-wasm/data/dummy.wasm new file mode 100644 index 000000000..336c6c728 --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/data/dummy.wasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22ef72a242dd71795540508058cae2915b501e6143494226b33d7416c0a3384a +size 121436 diff --git a/test/integration/connect/envoy/case-wasm/service_s1.hcl b/test/integration/connect/envoy/case-wasm/service_s1.hcl new file mode 100644 index 000000000..0d8957c00 --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/service_s1.hcl @@ -0,0 +1,16 @@ +services { + name = "s1" + port = 8080 + connect { + sidecar_service { + proxy { + upstreams = [ + { + destination_name = "s2" + local_bind_port = 5000 + } + ] + } + } + } +} diff --git a/test/integration/connect/envoy/case-wasm/service_s2.hcl b/test/integration/connect/envoy/case-wasm/service_s2.hcl new file mode 100644 index 000000000..9c23e79c7 --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/service_s2.hcl @@ -0,0 +1,5 @@ +services { + name = "s2" + port = 8181 + connect { sidecar_service {} } +} diff --git a/test/integration/connect/envoy/case-wasm/setup.sh b/test/integration/connect/envoy/case-wasm/setup.sh new file mode 100644 index 000000000..4a3360511 --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/setup.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -eEuo pipefail + +upsert_config_entry primary ' +Kind = "service-defaults" +Name = "s2" +Protocol = "http" +EnvoyExtensions = [ + { + Name = "builtin/wasm" + Arguments = { + Protocol = "http" + ListenerType = "inbound" + PluginConfig = { + VmConfig = { + Code = { + Local = { + Filename = "/workdir/primary/data/dummy.wasm" + } + } + } + Configuration = "plugin configuration" + } + } + } +] +' + +register_services primary + +gen_envoy_bootstrap s1 19000 primary +gen_envoy_bootstrap s2 19001 primary diff --git a/test/integration/connect/envoy/case-wasm/vars.sh b/test/integration/connect/envoy/case-wasm/vars.sh new file mode 100644 index 000000000..433e50c1b --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/vars.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy" diff --git a/test/integration/connect/envoy/case-wasm/verify.bats b/test/integration/connect/envoy/case-wasm/verify.bats new file mode 100644 index 000000000..9a9756ec7 --- /dev/null +++ b/test/integration/connect/envoy/case-wasm/verify.bats @@ -0,0 +1,25 @@ +#!/usr/bin/env bats + +load helpers + +@test "s1 proxy admin is up on :19000" { + retry_default curl -f -s localhost:19000/stats -o /dev/null +} + +@test "s2 proxy admin is up on :19001" { + retry_default curl -f -s localhost:19001/stats -o /dev/null +} + +@test "s1 proxy listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21000 s1 +} + +@test "s2 proxy is configured with a wasm http filter" { + run get_envoy_http_filter localhost:19001 envoy.filters.http.wasm + [ "$status" == 0 ] + + [ "$(echo "$output" | jq -r '.typed_config.config.vm_config.runtime')" == "envoy.wasm.runtime.v8" ] + [ "$(echo "$output" | jq -r '.typed_config.config.vm_config.code.local.filename')" == "/workdir/primary/data/dummy.wasm" ] + [ "$(echo "$output" | jq -r '.typed_config.config.configuration.value')" == "plugin configuration" ] + [ "$(echo "$output" | jq -r '.typed_config.config.fail_open')" == "true" ] +} diff --git a/test/integration/connect/envoy/helpers.bash b/test/integration/connect/envoy/helpers.bash index f364042af..81ee39012 100755 --- a/test/integration/connect/envoy/helpers.bash +++ b/test/integration/connect/envoy/helpers.bash @@ -260,6 +260,14 @@ function get_envoy_network_rbac_once { echo "$output" | jq --raw-output '.configs[2].dynamic_listeners[].active_state.listener.filter_chains[0].filters[] | select(.name == "envoy.filters.network.rbac") | .typed_config' } +function get_envoy_http_filter { + local HOSTPORT=$1 + local FILTER_NAME=$2 + run retry_default curl -s -f $HOSTPORT/config_dump + [ "$status" -eq 0 ] + echo "$output" | jq --raw-output ".configs[2].dynamic_listeners[] | .active_state.listener.filter_chains[].filters[] | select(.name == \"envoy.filters.network.http_connection_manager\") | .typed_config.http_filters[] | select(.name == \"${FILTER_NAME}\")" +} + function get_envoy_listener_filters { local HOSTPORT=$1 run retry_default curl -s -f $HOSTPORT/config_dump @@ -1078,15 +1086,6 @@ function assert_service_has_imported { fi } -function get_lambda_envoy_http_filter { - local HOSTPORT=$1 - local NAME_PREFIX=$2 - run retry_default curl -s -f $HOSTPORT/config_dump - [ "$status" -eq 0 ] - # get the full http filter object so the individual fields can be validated. - echo "$output" | jq --raw-output ".configs[2].dynamic_listeners[] | .active_state.listener.filter_chains[].filters[] | select(.name == \"envoy.filters.network.http_connection_manager\") | .typed_config.http_filters[] | select(.name == \"envoy.filters.http.aws_lambda\") | .typed_config" -} - function register_lambdas { local DC=${1:-primary} # register lambdas to the catalog @@ -1114,13 +1113,12 @@ function assert_lambda_envoy_dynamic_cluster_exists { function assert_lambda_envoy_dynamic_http_filter_exists { local HOSTPORT=$1 - local NAME_PREFIX=$2 - local ARN=$3 + local ARN=$2 - local FILTER=$(get_lambda_envoy_http_filter $HOSTPORT $NAME_PREFIX) + local FILTER=$(get_envoy_http_filter $HOSTPORT 'envoy.filters.http.aws_lambda') [ -n "$FILTER" ] - [ "$(echo $FILTER | jq -r '.arn')" == "$ARN" ] + [ "$(echo $FILTER | jq -r '.typed_config | .arn')" == "$ARN" ] } function varsub {