consul/envoyextensions/extensioncommon/basic_envoy_extender.go

352 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package extensioncommon
import (
"fmt"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"github.com/hashicorp/go-multierror"
"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
// ClusterLoadAssignmentMap is a map of cluster load assignments indexed by name.
type ClusterLoadAssignmentMap map[string]*envoy_endpoint_v3.ClusterLoadAssignment
// 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 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(RoutePayload) (*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(ClusterPayload) (*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)
// PatchClusterLoadAssignment patches a cluster load assignment to include the custom Envoy configuration
// required to integrate with the built in extension template.
PatchClusterLoadAssignment(ClusterLoadAssignmentPayload) (*envoy_endpoint_v3.ClusterLoadAssignment, bool, error)
// PatchListener patches a listener to include the custom Envoy configuration
// required to integrate with the built in extension template.
// See also PatchListeners.
PatchListener(ListenerPayload) (*envoy_listener_v3.Listener, bool, error)
// PatchListeners patches listeners to include the custom Envoy configuration
// required to integrate with the built in extension template.
// This allows extensions to operate on a collection of listeners.
// For extensions that implement both PatchListener and PatchListeners,
// PatchListeners is always called first with the entire collection of listeners.
// Then PatchListeners is called for each individual listener.
PatchListeners(*RuntimeConfig, ListenerMap) (ListenerMap, 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(FilterPayload) (*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)
// BasicEnvoyExtender provides convenience functions for iterating and applying modifications
// to Envoy resources.
type BasicEnvoyExtender struct {
Extension BasicExtension
}
func (b *BasicEnvoyExtender) CanApply(config *RuntimeConfig) bool {
return b.Extension.CanApply(config)
}
func (b *BasicEnvoyExtender) Validate(config *RuntimeConfig) error {
return b.Extension.Validate(config)
}
func (b *BasicEnvoyExtender) 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 {
// Currently we only support extensions for terminating gateways and connect proxies.
case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy:
default:
return resources, nil
}
clusters := make(ClusterMap)
clusterLoadAssignments := make(ClusterLoadAssignmentMap)
routes := make(RouteMap)
listeners := make(ListenerMap)
for _, indexType := range []string{
xdscommon.ListenerType,
xdscommon.RouteType,
xdscommon.ClusterType,
xdscommon.EndpointType,
} {
for nameOrSNI, msg := range resources.Index[indexType] {
switch resource := msg.(type) {
case *envoy_cluster_v3.Cluster:
clusters[nameOrSNI] = resource
case *envoy_endpoint_v3.ClusterLoadAssignment:
clusterLoadAssignments[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))
}
}
}
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 patchedClusterLoadAssignments, err := b.patchClusterLoadAssignments(config, clusterLoadAssignments); err == nil {
for k, v := range patchedClusterLoadAssignments {
resources.Index[xdscommon.EndpointType][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) 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.GetClusterPayload(cluster))
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster %q: %w", nameOrSNI, err))
}
if !patched {
patchedCluster = cluster
}
// We patch cluster load assignments directly above for EDS, but also here for CDS,
// since updates can come from either.
if patchedCluster.LoadAssignment != nil {
patchedClusterLoadAssignment, patched, err := b.Extension.PatchClusterLoadAssignment(config.GetClusterLoadAssignmentPayload(patchedCluster.LoadAssignment))
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching load assignment for cluster %q: %w", nameOrSNI, err))
} else if patched {
patchedCluster.LoadAssignment = patchedClusterLoadAssignment
}
}
patchedClusters[nameOrSNI] = patchedCluster
}
return patchedClusters, resultErr
}
func (b *BasicEnvoyExtender) patchClusterLoadAssignments(config *RuntimeConfig, clusterLoadAssignments ClusterLoadAssignmentMap) (ClusterLoadAssignmentMap, error) {
var resultErr error
for nameOrSNI, clusterLoadAssignment := range clusterLoadAssignments {
patchedClusterLoadAssignment, patched, err := b.Extension.PatchClusterLoadAssignment(config.GetClusterLoadAssignmentPayload(clusterLoadAssignment))
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster load assignment %q: %w", nameOrSNI, err))
}
if patched {
clusterLoadAssignments[nameOrSNI] = patchedClusterLoadAssignment
} else {
clusterLoadAssignments[nameOrSNI] = clusterLoadAssignment
}
}
return clusterLoadAssignments, 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.GetRoutePayload(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, listeners ListenerMap) (ListenerMap, error) {
var resultErr error
patchedListeners, err := b.Extension.PatchListeners(config, listeners)
if err != nil {
return listeners, fmt.Errorf("error patching listeners: %w", err)
}
for nameOrSNI, listener := range listeners {
patchedListener, patched, err := b.Extension.PatchListener(config.GetListenerPayload(listener))
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener %q: %w", nameOrSNI, err))
}
if !patched {
patchedListener = listener
}
if patchedListener, err = b.patchSupportedListenerFilterChains(config, patchedListener, nameOrSNI); err == nil {
patchedListeners[nameOrSNI] = patchedListener
} else {
resultErr = multierror.Append(resultErr, err)
patchedListeners[nameOrSNI] = listener
}
}
return patchedListeners, resultErr
}
func (b *BasicEnvoyExtender) patchSupportedListenerFilterChains(config *RuntimeConfig, l *envoy_listener_v3.Listener, nameOrSNI string) (*envoy_listener_v3.Listener, error) {
switch config.Kind {
case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy:
return b.patchListenerFilterChains(config, l, nameOrSNI)
}
return l, nil
}
func (b *BasicEnvoyExtender) patchListenerFilterChains(config *RuntimeConfig, l *envoy_listener_v3.Listener, nameOrSNI string) (*envoy_listener_v3.Listener, error) {
var resultErr error
// Special case for Permissive mTLS, which adds a filter chain
// containing a TCP Proxy only. We don't care about errors
// applying filters as long as the main filter chain is
// patched successfully.
if IsInboundPublicListener(l) && len(l.FilterChains) > 1 {
var isPatched bool
for idx, filterChain := range l.FilterChains {
patchedFilterChain, err := b.patchFilterChain(config, filterChain, l)
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter chain %q: %w", nameOrSNI, err))
continue
}
l.FilterChains[idx] = patchedFilterChain
isPatched = true
}
if isPatched {
return l, nil
}
return l, resultErr
}
for idx, filterChain := range l.FilterChains {
if patchedFilterChain, err := b.patchFilterChain(config, filterChain, l); err == nil {
l.FilterChains[idx] = patchedFilterChain
} else {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter chain %q: %w", nameOrSNI, err))
}
}
return l, resultErr
}
func (b *BasicEnvoyExtender) patchFilterChain(config *RuntimeConfig, filterChain *envoy_listener_v3.FilterChain, l *envoy_listener_v3.Listener) (*envoy_listener_v3.FilterChain, error) {
var resultErr error
inbound := IsInboundPublicListener(l)
patchedFilters, err := b.Extension.PatchFilters(config, filterChain.Filters, inbound)
if err != nil {
return filterChain, fmt.Errorf("error patching filters: %w", err)
}
for idx, filter := range patchedFilters {
patchedFilter, patched, err := b.Extension.PatchFilter(config.GetFilterPayload(filter, l))
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching filter: %w", err))
}
if patched {
patchedFilters[idx] = patchedFilter
} else {
patchedFilters[idx] = filter
}
}
filterChain.Filters = patchedFilters
return filterChain, err
}