From 2740d12d44248e953de15588e57694f97b04ce9a Mon Sep 17 00:00:00 2001 From: Chris Thain <32781396+cthain@users.noreply.github.com> Date: Fri, 26 May 2023 11:10:31 -0700 Subject: [PATCH] ENT->OSS merge for Consolidate `ListEnvoyExtender` into `BasicEnvoyExtender` (#17491) --- .../builtin/aws-lambda/aws_lambda.go | 2 + .../builtin/http/localratelimit/ratelimit.go | 14 +- agent/envoyextensions/builtin/lua/lua.go | 14 +- agent/envoyextensions/builtin/wasm/wasm.go | 14 +- .../extensioncommon/basic_envoy_extender.go | 372 +++++++++--------- .../basic_extension_adapter.go | 52 +++ .../extensioncommon/envoy_extender_test.go | 14 - .../extensioncommon/list_envoy_extender.go | 219 ----------- envoyextensions/extensioncommon/resources.go | 258 +++++++++++- .../extensioncommon/resources_test.go | 175 ++++++++ troubleshoot/validate/validate.go | 2 + 11 files changed, 676 insertions(+), 460 deletions(-) create mode 100644 envoyextensions/extensioncommon/basic_extension_adapter.go delete mode 100644 envoyextensions/extensioncommon/list_envoy_extender.go create mode 100644 envoyextensions/extensioncommon/resources_test.go diff --git a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go index 8281e1f016..cc7577f3fe 100644 --- a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go +++ b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go @@ -28,6 +28,8 @@ import ( var _ extensioncommon.BasicExtension = (*awsLambda)(nil) type awsLambda struct { + extensioncommon.BasicExtensionAdapter + ARN string PayloadPassthrough bool InvocationMode string diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go index 64be93e085..686a2390a8 100644 --- a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go +++ b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go @@ -8,10 +8,8 @@ import ( "fmt" "time" - envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 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_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_ratelimit "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" @@ -26,6 +24,8 @@ import ( ) type ratelimit struct { + extensioncommon.BasicExtensionAdapter + ProxyType string // Token bucket of the rate limit @@ -100,16 +100,6 @@ func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool { return string(config.Kind) == p.ProxyType } -// PatchRoute does nothing. -func (p ratelimit) PatchRoute(_ *extensioncommon.RuntimeConfig, route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { - return route, false, nil -} - -// PatchCluster does nothing. -func (p ratelimit) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { - return c, false, nil -} - // PatchFilter inserts a http local rate_limit filter at the head of // envoy.filters.network.http_connection_manager filters func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { diff --git a/agent/envoyextensions/builtin/lua/lua.go b/agent/envoyextensions/builtin/lua/lua.go index b2f2857b0b..32092c5ce8 100644 --- a/agent/envoyextensions/builtin/lua/lua.go +++ b/agent/envoyextensions/builtin/lua/lua.go @@ -7,9 +7,7 @@ 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_lua_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/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" @@ -22,6 +20,8 @@ import ( var _ extensioncommon.BasicExtension = (*lua)(nil) type lua struct { + extensioncommon.BasicExtensionAdapter + ProxyType string Listener string Script string @@ -71,16 +71,6 @@ func (l *lua) matchesListenerDirection(isInboundListener bool) bool { return (!isInboundListener && l.Listener == "outbound") || (isInboundListener && l.Listener == "inbound") } -// PatchRoute does nothing. -func (l *lua) PatchRoute(_ *extensioncommon.RuntimeConfig, route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { - return route, false, nil -} - -// PatchCluster does nothing. -func (l *lua) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { - return c, false, nil -} - // PatchFilter inserts a lua filter directly prior to envoy.filters.http.router. func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { // Make sure filter matches extension config. diff --git a/agent/envoyextensions/builtin/wasm/wasm.go b/agent/envoyextensions/builtin/wasm/wasm.go index f3812ebb45..ec97454f8e 100644 --- a/agent/envoyextensions/builtin/wasm/wasm.go +++ b/agent/envoyextensions/builtin/wasm/wasm.go @@ -7,9 +7,7 @@ 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" @@ -20,6 +18,8 @@ import ( // wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins. type wasm struct { + extensioncommon.BasicExtensionAdapter + name string wasmConfig *wasmConfig } @@ -77,16 +77,6 @@ func (w wasm) matchesConfigDirection(isInboundListener bool) bool { return isInboundListener && w.wasmConfig.ListenerType == "inbound" } -// 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, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { diff --git a/envoyextensions/extensioncommon/basic_envoy_extender.go b/envoyextensions/extensioncommon/basic_envoy_extender.go index 851f5a3a4c..7805fe47b0 100644 --- a/envoyextensions/extensioncommon/basic_envoy_extender.go +++ b/envoyextensions/extensioncommon/basic_envoy_extender.go @@ -5,37 +5,73 @@ package extensioncommon import ( "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_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" - envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/hashicorp/go-multierror" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/envoyextensions/xdscommon" ) +// ClusterMap is a map of clusters indexed by name. +type ClusterMap map[string]*envoy_cluster_v3.Cluster + +// ListenerMap is a map of listeners indexed by name. +type ListenerMap map[string]*envoy_listener_v3.Listener + +// RouteMap is a map of routes indexed by name. +type RouteMap map[string]*envoy_route_v3.RouteConfiguration + // BasicExtension is the interface that each user of BasicEnvoyExtender must implement. It // is responsible for modifying the xDS structures based on only the state of // the extension. type BasicExtension interface { - // CanApply determines if the extension can mutate resources for the given xdscommon.ExtensionConfiguration. + // CanApply determines if the extension can mutate resources for the given runtime configuration. CanApply(*RuntimeConfig) bool // PatchRoute patches a route to include the custom Envoy configuration // required to integrate with the built in extension template. + // See also PatchRoutes. PatchRoute(*RuntimeConfig, *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) + // PatchRoutes patches routes to include the custom Envoy configuration + // required to integrate with the built in extension template. + // This allows extensions to operate on a collection of routes. + // For extensions that implement both PatchRoute and PatchRoutes, + // PatchRoutes is always called first with the entire collection of routes. + // Then PatchRoute is called for each individual route. + PatchRoutes(*RuntimeConfig, RouteMap) (RouteMap, error) + // PatchCluster patches a cluster to include the custom Envoy configuration // required to integrate with the built in extension template. + // See also PatchClusters. PatchCluster(*RuntimeConfig, *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) + // PatchClusters patches clusters to include the custom Envoy configuration + // required to integrate with the built in extension template. + // This allows extensions to operate on a collection of clusters. + // For extensions that implement both PatchCluster and PatchClusters, + // PatchClusters is always called first with the entire collection of clusters. + // Then PatchClusters is called for each individual cluster. + PatchClusters(*RuntimeConfig, ClusterMap) (ClusterMap, error) + // PatchFilter patches an Envoy filter to include the custom Envoy // configuration required to integrate with the built in extension template. + // See also PatchFilters. PatchFilter(cfg *RuntimeConfig, f *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) + + // PatchFilters patches Envoy filters to include the custom Envoy + // configuration required to integrate with the built in extension template. + // This allows extensions to operate on a collection of filters. + // For extensions that implement both PatchFilter and PatchFilters, + // PatchFilters is always called first with the entire collection of filters. + // Then PatchFilter is called for each individual filter. + PatchFilters(cfg *RuntimeConfig, f []*envoy_listener_v3.Filter, isInboundListener bool) ([]*envoy_listener_v3.Filter, error) + + // Validate determines if the runtime configuration provided is valid for the extension. + Validate(*RuntimeConfig) error } var _ EnvoyExtender = (*BasicEnvoyExtender)(nil) @@ -46,8 +82,8 @@ type BasicEnvoyExtender struct { Extension BasicExtension } -func (b *BasicEnvoyExtender) Validate(_ *RuntimeConfig) error { - return nil +func (b *BasicEnvoyExtender) Validate(config *RuntimeConfig) error { + return b.Extension.Validate(config) } func (b *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { @@ -60,6 +96,7 @@ func (b *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, confi } switch config.Kind { + // Currently we only support extensions for terminating gateways and connect proxies. case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: default: return resources, nil @@ -69,6 +106,10 @@ func (b *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, confi return resources, nil } + clusters := make(ClusterMap) + routes := make(RouteMap) + listeners := make(ListenerMap) + for _, indexType := range []string{ xdscommon.ListenerType, xdscommon.RouteType, @@ -77,239 +118,190 @@ func (b *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, confi for nameOrSNI, msg := range resources.Index[indexType] { switch resource := msg.(type) { case *envoy_cluster_v3.Cluster: - newCluster, patched, err := b.Extension.PatchCluster(config, resource) - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster: %w", err)) - continue - } - if patched { - resources.Index[xdscommon.ClusterType][nameOrSNI] = newCluster - } - + clusters[nameOrSNI] = resource case *envoy_listener_v3.Listener: - newListener, patched, err := b.patchListener(config, resource) - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener: %w", err)) - continue - } - if patched { - resources.Index[xdscommon.ListenerType][nameOrSNI] = newListener - } - + listeners[nameOrSNI] = resource case *envoy_route_v3.RouteConfiguration: - newRoute, patched, err := b.Extension.PatchRoute(config, resource) - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route: %w", err)) - continue - } - if patched { - resources.Index[xdscommon.RouteType][nameOrSNI] = newRoute - } + routes[nameOrSNI] = resource default: resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported type was skipped: %T", resource)) } } } + if patchedClusters, err := b.patchClusters(config, clusters); err == nil { + for k, v := range patchedClusters { + resources.Index[xdscommon.ClusterType][k] = v + } + } else { + resultErr = multierror.Append(resultErr, err) + } + + if patchedListeners, err := b.patchListeners(config, listeners); err == nil { + for k, v := range patchedListeners { + resources.Index[xdscommon.ListenerType][k] = v + } + } else { + resultErr = multierror.Append(resultErr, err) + } + + if patchedRoutes, err := b.patchRoutes(config, routes); err == nil { + for k, v := range patchedRoutes { + resources.Index[xdscommon.RouteType][k] = v + } + } else { + resultErr = multierror.Append(resultErr, err) + } + return resources, resultErr } -func (b *BasicEnvoyExtender) patchListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchClusters(config *RuntimeConfig, clusters ClusterMap) (ClusterMap, error) { + var resultErr error + + patchedClusters, err := b.Extension.PatchClusters(config, clusters) + if err != nil { + return clusters, fmt.Errorf("error patching clusters: %w", err) + } + for nameOrSNI, cluster := range clusters { + patchedCluster, patched, err := b.Extension.PatchCluster(config, cluster) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster %q: %w", nameOrSNI, err)) + } + if patched { + patchedClusters[nameOrSNI] = patchedCluster + } else { + patchedClusters[nameOrSNI] = cluster + } + } + return patchedClusters, resultErr +} + +func (b *BasicEnvoyExtender) patchRoutes(config *RuntimeConfig, routes RouteMap) (RouteMap, error) { + var resultErr error + + patchedRoutes, err := b.Extension.PatchRoutes(config, routes) + if err != nil { + return routes, fmt.Errorf("error patching routes: %w", err) + } + for nameOrSNI, route := range patchedRoutes { + patchedRoute, patched, err := b.Extension.PatchRoute(config, route) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route %q: %w", nameOrSNI, err)) + } + if patched { + patchedRoutes[nameOrSNI] = patchedRoute + } else { + patchedRoutes[nameOrSNI] = route + } + } + return patchedRoutes, resultErr +} + +func (b *BasicEnvoyExtender) patchListeners(config *RuntimeConfig, m ListenerMap) (ListenerMap, error) { switch config.Kind { case api.ServiceKindTerminatingGateway: - return b.patchTerminatingGatewayListener(config, l) + return b.patchTerminatingGatewayListeners(config, m) case api.ServiceKindConnectProxy: - return b.patchConnectProxyListener(config, l) + return b.patchConnectProxyListeners(config, m) } - return l, false, nil + return m, nil } -func (b *BasicEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { var resultErr error - patched := false - - for _, filterChain := range l.FilterChains { - var filters []*envoy_listener_v3.Filter - - for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) - - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) - filters = append(filters, filter) - continue - } - if ok { - filters = append(filters, newFilter) - patched = true + for _, listener := range l { + for idx, filterChain := range listener.FilterChains { + if patchedFilterChain, err := b.patchFilterChain(config, filterChain, IsInboundPublicListener(listener)); err == nil { + listener.FilterChains[idx] = patchedFilterChain } else { - filters = append(filters, filter) + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching tgw filter chain: %w", err)) } } - filterChain.Filters = filters } - return l, patched, resultErr + return l, resultErr } -func (b *BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { var resultErr error - patched := false - if IsOutboundTProxyListener(l) { - return b.patchTProxyListener(config, l) - } - - for _, filterChain := range l.FilterChains { - var filters []*envoy_listener_v3.Filter - - for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) - filters = append(filters, filter) - continue - } - - if ok { - filters = append(filters, newFilter) - patched = true + for nameOrSNI, listener := range l { + if IsOutboundTProxyListener(listener) { + patchedListener, err := b.patchTProxyListener(config, listener) + if err == nil { + l[nameOrSNI] = patchedListener } else { - filters = append(filters, filter) + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching TProxy listener %q: %w", nameOrSNI, err)) + } + } else { + + patchedListener, err := b.patchConnectProxyListener(config, listener) + if err == nil { + l[nameOrSNI] = patchedListener + } else { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching connect proxy listener %q: %w", nameOrSNI, err)) } } - filterChain.Filters = filters } - return l, patched, resultErr + return l, resultErr } -func (b *BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (*envoy_listener_v3.Listener, error) { + var resultErr error + + inbound := IsInboundPublicListener(l) + + for idx, filterChain := range l.FilterChains { + if patchedFilterChain, err := b.patchFilterChain(config, filterChain, inbound); err == nil { + l.FilterChains[idx] = patchedFilterChain + } else { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filter chain: %w", err)) + } + } + return l, resultErr +} + +func (b *BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (*envoy_listener_v3.Listener, error) { var resultErr error - patched := false vip := config.Upstreams[config.ServiceName].VIP + inbound := IsInboundPublicListener(l) - for _, filterChain := range l.FilterChains { - var filters []*envoy_listener_v3.Filter - + for idx, filterChain := range l.FilterChains { match := filterChainTProxyMatch(vip, filterChain) if !match { continue } - for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) - if err != nil { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) - filters = append(filters, filter) - continue - } - - if ok { - filters = append(filters, newFilter) - patched = true - } else { - filters = append(filters, filter) - } - } - filterChain.Filters = filters - } - - return l, patched, resultErr -} - -func filterChainTProxyMatch(vip string, filterChain *envoy_listener_v3.FilterChain) bool { - for _, prefixRange := range filterChain.FilterChainMatch.PrefixRanges { - // Since we always set the address prefix as the full VIP (rather than a prefix), we can just check if they are - // equal to find the matching filter chain. - if vip == prefixRange.AddressPrefix { - return true + if patchedFilterChain, err := b.patchFilterChain(config, filterChain, inbound); err == nil { + l.FilterChains[idx] = patchedFilterChain + } else { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filter chain for %q: %w", vip, err)) } } - return false + return l, resultErr } -func FilterClusterNames(filter *envoy_listener_v3.Filter) map[string]struct{} { - clusterNames := make(map[string]struct{}) - if filter == nil { - return clusterNames +func (b *BasicEnvoyExtender) patchFilterChain(config *RuntimeConfig, filterChain *envoy_listener_v3.FilterChain, isInboundListener bool) (*envoy_listener_v3.FilterChain, error) { + var resultErr error + patchedFilters, err := b.Extension.PatchFilters(config, filterChain.Filters, isInboundListener) + if err != nil { + return filterChain, fmt.Errorf("error patching filters: %w", err) } - - if config := envoy_resource_v3.GetHTTPConnectionManager(filter); config != nil { - // If it's using RDS, the cluster names will be in the route, rather than in the http filter's route config, so - // we don't return any cluster names in this case. They can be gathered from the route. - if config.GetRds() != nil { - return clusterNames + for idx, filter := range patchedFilters { + patchedFilter, patched, err := b.Extension.PatchFilter(config, filter, isInboundListener) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filter: %w", err)) } - - cfg := config.GetRouteConfig() - - clusterNames = RouteClusterNames(cfg) - } - - if config := GetTCPProxy(filter); config != nil { - clusterNames[config.GetCluster()] = struct{}{} - } - - return clusterNames -} - -func RouteClusterNames(route *envoy_route_v3.RouteConfiguration) map[string]struct{} { - if route == nil { - return nil - } - - clusterNames := make(map[string]struct{}) - - for _, virtualHost := range route.VirtualHosts { - for _, route := range virtualHost.Routes { - r := route.GetRoute() - if r == nil { - continue - } - if c := r.GetCluster(); c != "" { - clusterNames[r.GetCluster()] = struct{}{} - } - - if wc := r.GetWeightedClusters(); wc != nil { - for _, c := range wc.GetClusters() { - if c.Name != "" { - clusterNames[c.Name] = struct{}{} - } - } - } + if patched { + patchedFilters[idx] = patchedFilter + } else { + patchedFilters[idx] = filter } } - return clusterNames -} - -func GetTCPProxy(filter *envoy_listener_v3.Filter) *envoy_tcp_proxy_v3.TcpProxy { - if typedConfig := filter.GetTypedConfig(); typedConfig != nil { - config := &envoy_tcp_proxy_v3.TcpProxy{} - if err := anypb.UnmarshalTo(typedConfig, config, proto.UnmarshalOptions{}); err == nil { - return config - } - } - - return nil -} - -func getSNI(chain *envoy_listener_v3.FilterChain) string { - var sni string - - if chain == nil { - return sni - } - - if chain.FilterChainMatch == nil { - return sni - } - - if len(chain.FilterChainMatch.ServerNames) == 0 { - return sni - } - - return chain.FilterChainMatch.ServerNames[0] + filterChain.Filters = patchedFilters + return filterChain, err } diff --git a/envoyextensions/extensioncommon/basic_extension_adapter.go b/envoyextensions/extensioncommon/basic_extension_adapter.go new file mode 100644 index 0000000000..b89d6ed100 --- /dev/null +++ b/envoyextensions/extensioncommon/basic_extension_adapter.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package extensioncommon + +import ( + 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" +) + +// BasicExtensionAdapter is an adapter that provides default implementations for all of the EnvoyExtension +// interface functions. Extension implementations can extend the adapter and implement only the functions +// they are interested in. At a minimum, extensions must override the adapter's CanApply and Validate +// functions. +type BasicExtensionAdapter struct{} + +// CanApply provides a default implementation of the CanApply interface that always returns false. +func (BasicExtensionAdapter) CanApply(_ *RuntimeConfig) bool { return false } + +// PatchCluster provides a default implementation of the PatchCluster interface that does nothing. +func (BasicExtensionAdapter) PatchCluster(_ *RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { + return c, false, nil +} + +// PatchClusters provides a default implementation of the PatchClusters interface that does nothing. +func (BasicExtensionAdapter) PatchClusters(_ *RuntimeConfig, c ClusterMap) (ClusterMap, error) { + return c, nil +} + +// PatchFilter provides a default implementation of the PatchFilter interface that does nothing. +func (BasicExtensionAdapter) PatchFilter(_ *RuntimeConfig, f *envoy_listener_v3.Filter, _ bool) (*envoy_listener_v3.Filter, bool, error) { + return f, false, nil +} + +// PatchFilters provides a default implementation of the PatchFilters interface that does nothing. +func (BasicExtensionAdapter) PatchFilters(_ *RuntimeConfig, f []*envoy_listener_v3.Filter, _ bool) ([]*envoy_listener_v3.Filter, error) { + return f, nil +} + +// PatchRoute provides a default implementation of the PatchRoute interface that does nothing. +func (BasicExtensionAdapter) PatchRoute(_ *RuntimeConfig, r *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { + return r, false, nil +} + +// PatchRoutes provides a default implementation of the PatchRoutes interface that does nothing. +func (BasicExtensionAdapter) PatchRoutes(_ *RuntimeConfig, r RouteMap) (RouteMap, error) { + return r, nil +} + +// Validate provides a default implementation of the Validate interface that always returns nil. +func (BasicExtensionAdapter) Validate(_ *RuntimeConfig) error { return nil } diff --git a/envoyextensions/extensioncommon/envoy_extender_test.go b/envoyextensions/extensioncommon/envoy_extender_test.go index 6a5c941166..48e338ed89 100644 --- a/envoyextensions/extensioncommon/envoy_extender_test.go +++ b/envoyextensions/extensioncommon/envoy_extender_test.go @@ -44,20 +44,6 @@ func TestUpstreamConfigSourceLimitations(t *testing.T) { ok: false, errMsg: fmt.Sprintf("%q extension applied as local config but is sourced from an upstream of the local service", api.BuiltinLuaExtension), }, - "list extender upstream config": { - extender: &ListEnvoyExtender{}, - config: &RuntimeConfig{ - Kind: api.ServiceKindConnectProxy, - ServiceName: api.CompoundServiceName{Name: "api"}, - Upstreams: map[api.CompoundServiceName]*UpstreamData{}, - IsSourcedFromUpstream: true, - EnvoyExtension: api.EnvoyExtension{ - Name: api.BuiltinLuaExtension, - }, - }, - ok: false, - errMsg: fmt.Sprintf("%q extension applied as local config but is sourced from an upstream of the local service", api.BuiltinLuaExtension), - }, } for n, tc := range cases { diff --git a/envoyextensions/extensioncommon/list_envoy_extender.go b/envoyextensions/extensioncommon/list_envoy_extender.go deleted file mode 100644 index c326d18570..0000000000 --- a/envoyextensions/extensioncommon/list_envoy_extender.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package extensioncommon - -import ( - "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" - "github.com/hashicorp/go-multierror" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/envoyextensions/xdscommon" -) - -type ClusterMap map[string]*envoy_cluster_v3.Cluster -type ListenerMap map[string]*envoy_listener_v3.Listener -type RouteMap map[string]*envoy_route_v3.RouteConfiguration - -// ListExtension is the interface that each user of ListEnvoyExtender must implement. It -// is responsible for modifying the xDS structures based on only the state of the extension. -type ListExtension interface { - // CanApply determines if the extension can mutate resources for the given runtime configuration. - CanApply(*RuntimeConfig) bool - - // PatchRoutes patches routes to include the custom Envoy configuration - // required to integrate with the built in extension template. - PatchRoutes(*RuntimeConfig, RouteMap) (RouteMap, error) - - // PatchClusters patches clusters to include the custom Envoy configuration - // required to integrate with the built in extension template. - PatchClusters(*RuntimeConfig, ClusterMap) (ClusterMap, error) - - // PatchFilters patches Envoy filters to include the custom Envoy - // configuration required to integrate with the built in extension template. - PatchFilters(*RuntimeConfig, []*envoy_listener_v3.Filter) ([]*envoy_listener_v3.Filter, error) -} - -var _ EnvoyExtender = (*ListEnvoyExtender)(nil) - -// ListEnvoyExtender provides convenience functions for iterating and applying modifications -// to lists of Envoy resources. -type ListEnvoyExtender struct { - Extension ListExtension -} - -func (*ListEnvoyExtender) Validate(config *RuntimeConfig) error { - return nil -} - -func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { - var resultErr error - - // We don't support patching the local proxy with an upstream's config except in special - // cases supported by UpstreamEnvoyExtender. - if config.IsSourcedFromUpstream { - return nil, fmt.Errorf("%q extension applied as local config but is sourced from an upstream of the local service", config.EnvoyExtension.Name) - } - - switch config.Kind { - case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: - default: - return resources, nil - } - - if !e.Extension.CanApply(config) { - return resources, nil - } - - clusters := make(ClusterMap) - routes := make(RouteMap) - listeners := make(ListenerMap) - - for _, indexType := range []string{ - xdscommon.ListenerType, - xdscommon.RouteType, - xdscommon.ClusterType, - } { - for nameOrSNI, msg := range resources.Index[indexType] { - switch resource := msg.(type) { - case *envoy_cluster_v3.Cluster: - clusters[nameOrSNI] = resource - - case *envoy_listener_v3.Listener: - listeners[nameOrSNI] = resource - - case *envoy_route_v3.RouteConfiguration: - routes[nameOrSNI] = resource - - default: - resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported type was skipped: %T", resource)) - } - } - } - - patchedClusters, err := e.Extension.PatchClusters(config, clusters) - if err == nil { - for nameOrSNI, cluster := range patchedClusters { - resources.Index[xdscommon.ClusterType][nameOrSNI] = cluster - } - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching clusters: %w", err)) - } - - patchedListeners, err := e.patchListeners(config, listeners) - if err == nil { - for nameOrSNI, listener := range patchedListeners { - resources.Index[xdscommon.ListenerType][nameOrSNI] = listener - } - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listeners: %w", err)) - } - - patchedRoutes, err := e.Extension.PatchRoutes(config, routes) - if err == nil { - for nameOrSNI, route := range patchedRoutes { - resources.Index[xdscommon.RouteType][nameOrSNI] = route - } - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching routes: %w", err)) - } - - return resources, resultErr -} - -func (e ListEnvoyExtender) patchListeners(config *RuntimeConfig, m ListenerMap) (ListenerMap, error) { - switch config.Kind { - case api.ServiceKindTerminatingGateway: - return e.patchTerminatingGatewayListeners(config, m) - case api.ServiceKindConnectProxy: - return e.patchConnectProxyListeners(config, m) - } - return m, nil -} - -func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { - var resultErr error - for _, listener := range l { - for _, filterChain := range listener.FilterChains { - sni := getSNI(filterChain) - - if sni == "" { - continue - } - - patchedFilters, err := e.Extension.PatchFilters(config, filterChain.Filters) - if err == nil { - filterChain.Filters = patchedFilters - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filters for %q: %w", sni, err)) - continue - } - } - } - - return l, resultErr -} - -func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { - var resultErr error - - for nameOrSNI, listener := range l { - if IsOutboundTProxyListener(listener) { - patchedListener, err := e.patchTProxyListener(config, listener) - if err == nil { - l[nameOrSNI] = patchedListener - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching TProxy listener %q: %w", nameOrSNI, err)) - } - continue - } - - patchedListener, err := e.patchConnectProxyListener(config, listener) - if err == nil { - l[nameOrSNI] = patchedListener - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching connect proxy listener %q: %w", nameOrSNI, err)) - } - } - - return l, resultErr -} - -func (e ListEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (*envoy_listener_v3.Listener, error) { - var resultErr error - - for _, filterChain := range l.FilterChains { - patchedFilters, err := e.Extension.PatchFilters(config, filterChain.Filters) - if err == nil { - filterChain.Filters = patchedFilters - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filters: %w", err)) - } - } - return l, resultErr -} - -func (e ListEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (*envoy_listener_v3.Listener, error) { - var resultErr error - - vip := config.Upstreams[config.ServiceName].VIP - - for _, filterChain := range l.FilterChains { - match := filterChainTProxyMatch(vip, filterChain) - if !match { - continue - } - - patchedFilters, err := e.Extension.PatchFilters(config, filterChain.Filters) - if err == nil { - filterChain.Filters = patchedFilters - } else { - resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filters: %w", err)) - } - } - - return l, resultErr -} diff --git a/envoyextensions/extensioncommon/resources.go b/envoyextensions/extensioncommon/resources.go index 2624bc3515..35fa7c1bc0 100644 --- a/envoyextensions/extensioncommon/resources.go +++ b/envoyextensions/extensioncommon/resources.go @@ -4,16 +4,21 @@ package extensioncommon import ( + "errors" + "fmt" + "strings" + envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 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_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_tcp_proxy_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/hashicorp/consul/envoyextensions/xdscommon" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" - "strings" ) // MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context. @@ -100,3 +105,254 @@ func IsInboundPublicListener(l *envoy_listener_v3.Listener) bool { func IsOutboundTProxyListener(l *envoy_listener_v3.Listener) bool { return GetListenerEnvoyID(l) == xdscommon.OutboundListenerName } + +func filterChainTProxyMatch(vip string, filterChain *envoy_listener_v3.FilterChain) bool { + for _, prefixRange := range filterChain.FilterChainMatch.PrefixRanges { + // Since we always set the address prefix as the full VIP (rather than a prefix), we can just check if they are + // equal to find the matching filter chain. + if vip == prefixRange.AddressPrefix { + return true + } + } + + return false +} + +func FilterClusterNames(filter *envoy_listener_v3.Filter) map[string]struct{} { + clusterNames := make(map[string]struct{}) + if filter == nil { + return clusterNames + } + + if config := envoy_resource_v3.GetHTTPConnectionManager(filter); config != nil { + // If it's using RDS, the cluster names will be in the route, rather than in the http filter's route config, so + // we don't return any cluster names in this case. They can be gathered from the route. + if config.GetRds() != nil { + return clusterNames + } + + cfg := config.GetRouteConfig() + + clusterNames = RouteClusterNames(cfg) + } + + if config := GetTCPProxy(filter); config != nil { + clusterNames[config.GetCluster()] = struct{}{} + } + + return clusterNames +} + +func RouteClusterNames(route *envoy_route_v3.RouteConfiguration) map[string]struct{} { + if route == nil { + return nil + } + + clusterNames := make(map[string]struct{}) + + for _, virtualHost := range route.VirtualHosts { + for _, route := range virtualHost.Routes { + r := route.GetRoute() + if r == nil { + continue + } + if c := r.GetCluster(); c != "" { + clusterNames[r.GetCluster()] = struct{}{} + } + + if wc := r.GetWeightedClusters(); wc != nil { + for _, c := range wc.GetClusters() { + if c.Name != "" { + clusterNames[c.Name] = struct{}{} + } + } + } + } + } + return clusterNames +} + +func GetTCPProxy(filter *envoy_listener_v3.Filter) *envoy_tcp_proxy_v3.TcpProxy { + if typedConfig := filter.GetTypedConfig(); typedConfig != nil { + config := &envoy_tcp_proxy_v3.TcpProxy{} + if err := anypb.UnmarshalTo(typedConfig, config, proto.UnmarshalOptions{}); err == nil { + return config + } + } + + return nil +} + +func getSNI(chain *envoy_listener_v3.FilterChain) string { + var sni string + + if chain == nil { + return sni + } + + if chain.FilterChainMatch == nil { + return sni + } + + if len(chain.FilterChainMatch.ServerNames) == 0 { + return sni + } + + return chain.FilterChainMatch.ServerNames[0] +} + +// GetHTTPConnectionManager returns the Envoy HttpConnectionManager filter from the list of network filters. +// It also returns the index within the list of filters where the connection manager was found in case the caller +// needs this information. +// It returns a non-nil error if the HttpConnectionManager is not found. +func GetHTTPConnectionManager(filters ...*envoy_listener_v3.Filter) (*envoy_http_v3.HttpConnectionManager, int, error) { + for idx, filter := range filters { + if filter.Name == "envoy.filters.network.http_connection_manager" { + if httpConnMgr := envoy_resource_v3.GetHTTPConnectionManager(filter); httpConnMgr != nil { + return httpConnMgr, idx, nil + } + } + } + return nil, 0, errors.New("failed to get HTTP connection manager") +} + +// InsertLocation indicates where to insert an Envoy resource within a list of resources. +type InsertLocation string + +const ( + // InsertFirst inserts the resource as the first entry in the list. + InsertFirst InsertLocation = "First" + // InsertLast inserts the resource as the last entry in the list. + InsertLast InsertLocation = "Last" + // InsertBeforeFirstMatch inserts the resource before the first resource with a matching name. + InsertBeforeFirstMatch InsertLocation = "BeforeFirstMatch" + // InsertAfterFirstMatch inserts the resource after the first resource with a matching name. + InsertAfterFirstMatch InsertLocation = "AfterFirstMatch" + // InsertBeforeLastMatch inserts the resource before the last resource with a matching name. + InsertBeforeLastMatch InsertLocation = "BeforeLastMatch" + // InsertAfterLastMatch inserts the resource after the last resource with a matching name. + InsertAfterLastMatch InsertLocation = "AfterLastMatch" +) + +// InsertOptions controls how and where to insert Envoy resources. +type InsertOptions struct { + // Location defines where to insert the resource within the list. + Location InsertLocation + // FilterName indicates the name of the resource to insert relative to. + FilterName string +} + +// InsertHTTPFilter inserts the given HTTP filter into the HttpConnectionManager's filter chain in the location +// determined by the insert options. This list of filters must include the HttpConnectionManager network +// filter or the operation will fail. +// +// It returns the modified list of filters including the updated HttpConnectionManager. +// If a matching location is not found to insert the filter, a non-nil error is returned. +func InsertHTTPFilter(filters []*envoy_listener_v3.Filter, filter *envoy_http_v3.HttpFilter, opts InsertOptions) ([]*envoy_listener_v3.Filter, error) { + httpConnMgr, idx, err := GetHTTPConnectionManager(filters...) + if err != nil { + return filters, err + } + + namedFilters := make([]namedFilter, 0, len(httpConnMgr.HttpFilters)+1) + for _, f := range httpConnMgr.HttpFilters { + namedFilters = append(namedFilters, f) + } + insertIdx, err := locateInsertIndex(opts, namedFilters) + if err != nil { + return filters, fmt.Errorf("failed to insert %q filter: %w", filter.Name, err) + } + + currIdx := 0 + newHttpFilters := make([]*envoy_http_v3.HttpFilter, len(httpConnMgr.HttpFilters)+1) + for idx, httpFilter := range httpConnMgr.HttpFilters { + if idx == insertIdx { + newHttpFilters[currIdx] = filter + currIdx++ + } + newHttpFilters[currIdx] = httpFilter + currIdx++ + } + if currIdx == insertIdx { + newHttpFilters[currIdx] = filter + } + + httpConnMgr.HttpFilters = newHttpFilters + newHttpConMan, err := MakeFilter("envoy.filters.network.http_connection_manager", httpConnMgr) + if err != nil { + return filters, errors.New("failed to insert new HTTP connection manager filter") + } + filters[idx] = newHttpConMan + + return filters, nil +} + +// InsertNetworkFilter inserts the given network filter into the filter chain in the location +// determined by the insert options. +// +// It returns the modified list of filters including the new filter. +// If a matching location is not found to insert the filter, a non-nil error is returned. +func InsertNetworkFilter(filters []*envoy_listener_v3.Filter, filter *envoy_listener_v3.Filter, opts InsertOptions) ([]*envoy_listener_v3.Filter, error) { + namedFilters := make([]namedFilter, 0, len(filters)+1) + for _, f := range filters { + namedFilters = append(namedFilters, f) + } + insertIdx, err := locateInsertIndex(opts, namedFilters) + if err != nil { + return filters, fmt.Errorf("failed to insert %q filter: %w", filter.Name, err) + } + + currIdx := 0 + newFilters := make([]*envoy_listener_v3.Filter, len(filters)+1) + for idx, f := range filters { + if idx == insertIdx { + newFilters[currIdx] = filter + currIdx++ + } + newFilters[currIdx] = f + currIdx++ + } + if currIdx == insertIdx { + newFilters[currIdx] = filter + } + + return newFilters, nil +} + +// namedFilter is a convenience interface for locating Envoy filters based on name. +type namedFilter interface { + GetName() string +} + +// locateInsertIndex returns the index where a filter should be inserted based on the given +// insert options. +func locateInsertIndex(opts InsertOptions, filters []namedFilter) (int, error) { + idx := 0 + if opts.Location == InsertFirst { + return idx, nil + } + if opts.Location == InsertLast { + return len(filters), nil + } + + matched := false + for currIdx, filter := range filters { + if filter.GetName() == opts.FilterName { + matched = true + switch opts.Location { + case InsertBeforeFirstMatch: + return currIdx, nil + case InsertAfterFirstMatch: + return currIdx + 1, nil + case InsertBeforeLastMatch: + idx = currIdx + case InsertAfterLastMatch: + idx = currIdx + 1 + } + } + } + if matched { + return idx, nil + } + return idx, fmt.Errorf("failed to find insert location %q for %q", opts.Location, opts.FilterName) +} diff --git a/envoyextensions/extensioncommon/resources_test.go b/envoyextensions/extensioncommon/resources_test.go new file mode 100644 index 0000000000..659da8770e --- /dev/null +++ b/envoyextensions/extensioncommon/resources_test.go @@ -0,0 +1,175 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package extensioncommon + +import ( + "testing" + + 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" + "github.com/stretchr/testify/require" +) + +func TestInsertHTTPFilter(t *testing.T) { + cases := map[string]struct { + inputFilters []*envoy_http_v3.HttpFilter + insertOptions InsertOptions + filterName string + expectedFilters []*envoy_http_v3.HttpFilter + errStr string + }{ + "insert first": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertFirst}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "test.filter", "a", "b", "b", "b", "c"), + }, + "insert last": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertLast}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "b", "b", "b", "c", "test.filter"), + }, + "insert before first match": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertBeforeFirstMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "test.filter", "b", "b", "b", "c"), + }, + "insert after first match": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertAfterFirstMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "b", "test.filter", "b", "b", "c"), + }, + "insert before last match": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertBeforeLastMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "b", "b", "test.filter", "b", "c"), + }, + "insert after last match": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertAfterLastMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "b", "b", "b", "test.filter", "c"), + }, + "insert last after last match": { + inputFilters: makeHttpFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertAfterLastMatch, FilterName: "c"}, + filterName: "test.filter", + expectedFilters: makeHttpFilters(t, "a", "b", "b", "b", "c", "test.filter"), + }, + } + + t.Parallel() + for name, c := range cases { + c := c + + t.Run(name, func(t *testing.T) { + filters := []*envoy_listener_v3.Filter{makeHttpConMgr(t, c.inputFilters)} + newFilter := &envoy_http_v3.HttpFilter{Name: c.filterName} + obsFilters, err := InsertHTTPFilter(filters, newFilter, c.insertOptions) + if c.errStr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), c.errStr) + } else { + require.NoError(t, err) + httpConMgr, idx, err := GetHTTPConnectionManager(obsFilters...) + require.NoError(t, err) + require.NotNil(t, httpConMgr) + require.Equal(t, 0, idx) + require.ElementsMatch(t, c.expectedFilters, httpConMgr.HttpFilters) + } + }) + } +} + +func TestInsertFilter(t *testing.T) { + cases := map[string]struct { + inputFilters []*envoy_listener_v3.Filter + filterName string + insertOptions InsertOptions + expectedFilters []*envoy_listener_v3.Filter + errStr string + }{ + "insert first": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertFirst}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "test.filter", "a", "b", "b", "b", "c"), + }, + "insert last": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertLast}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "a", "b", "b", "b", "c", "test.filter"), + }, + "insert before first match": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertBeforeFirstMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "a", "test.filter", "b", "b", "b", "c"), + }, + "insert after first match": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertAfterFirstMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "a", "b", "test.filter", "b", "b", "c"), + }, + "insert before last match": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertBeforeLastMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "a", "b", "b", "test.filter", "b", "c"), + }, + "insert after last match": { + inputFilters: makeFilters(t, "a", "b", "b", "b", "c"), + insertOptions: InsertOptions{Location: InsertAfterLastMatch, FilterName: "b"}, + filterName: "test.filter", + expectedFilters: makeFilters(t, "a", "b", "b", "b", "test.filter", "c"), + }, + } + + t.Parallel() + for name, c := range cases { + c := c + + t.Run(name, func(t *testing.T) { + filter := &envoy_listener_v3.Filter{Name: c.filterName} + obsFilters, err := InsertNetworkFilter(c.inputFilters, filter, c.insertOptions) + if c.errStr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), c.errStr) + } else { + require.NoError(t, err) + require.ElementsMatch(t, c.expectedFilters, obsFilters) + } + }) + } +} + +func makeHttpConMgr(t *testing.T, filters []*envoy_http_v3.HttpFilter) *envoy_listener_v3.Filter { + t.Helper() + httpConMgr := &envoy_http_v3.HttpConnectionManager{HttpFilters: filters} + filter, err := MakeFilter("envoy.filters.network.http_connection_manager", httpConMgr) + require.NoError(t, err) + return filter +} + +func makeHttpFilters(t *testing.T, names ...string) []*envoy_http_v3.HttpFilter { + var filters []*envoy_http_v3.HttpFilter + for _, name := range names { + filters = append(filters, &envoy_http_v3.HttpFilter{Name: name}) + } + return filters +} + +func makeFilters(t *testing.T, names ...string) []*envoy_listener_v3.Filter { + var filters []*envoy_listener_v3.Filter + for _, name := range names { + filters = append(filters, &envoy_listener_v3.Filter{Name: name}) + } + return filters +} diff --git a/troubleshoot/validate/validate.go b/troubleshoot/validate/validate.go index 47c05cc1fb..2177eb304b 100644 --- a/troubleshoot/validate/validate.go +++ b/troubleshoot/validate/validate.go @@ -24,6 +24,8 @@ import ( // Validate contains input information about which proxy resources to validate and output information about resources it // has validated. type Validate struct { + extensioncommon.BasicExtensionAdapter + // envoyID is an argument to the Validate plugin and identifies which listener to begin the validation with. envoyID string