You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/envoyextensions/extensioncommon/basic_envoy_extender.go

352 lines
14 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// 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
}