consul/agent/envoyextensions/builtin/property-override/property_override.go

376 lines
14 KiB
Go
Raw Normal View History

[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>
2023-08-11 13:12:13 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package propertyoverride
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/consul/lib/decode"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
)
type propertyOverride struct {
extensioncommon.BasicExtensionAdapter
// Patches are an array of Patch operations to be applied to the target resource(s).
Patches []Patch
// Debug controls error messages when Path matching fails.
// When set to true, all possible fields for the unmatched segment of the Path are returned.
// When set to false, only the first ten possible fields are returned.
Debug bool
// 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
}
// ResourceFilter matches specific Envoy resources to target with a Patch operation.
type ResourceFilter struct {
// ResourceType specifies the Envoy resource type the patch applies to. Valid values are
// `cluster`, `route`, `endpoint`, and `listener`.
// This field is required.
ResourceType ResourceType
// TrafficDirection determines whether the patch will be applied to a service's inbound
// or outbound resources.
// This field is required.
TrafficDirection extensioncommon.TrafficDirection
// Services indicates which upstream services will have corresponding Envoy resources patched.
// This includes directly targeted and discovery chain services. If Services is omitted or
// empty, all resources matching the filter will be targeted (including TProxy, which
// implicitly corresponds to any number of upstreams). Services must be omitted unless
// TrafficDirection is set to outbound.
Services []*ServiceName
}
func matchesResourceFilter[K proto.Message](rf ResourceFilter, resourceType ResourceType, payload extensioncommon.Payload[K]) bool {
if resourceType != rf.ResourceType {
return false
}
if payload.TrafficDirection != rf.TrafficDirection {
return false
}
if len(rf.Services) == 0 {
return true
}
for _, s := range rf.Services {
if payload.ServiceName == nil || s.CompoundServiceName != *payload.ServiceName {
continue
}
return true
}
return false
}
type ServiceName struct {
api.CompoundServiceName `mapstructure:",squash"`
}
// ResourceType is the type of Envoy resource being patched.
type ResourceType string
const (
ResourceTypeCluster ResourceType = "cluster"
ResourceTypeClusterLoadAssignment ResourceType = "cluster-load-assignment"
ResourceTypeListener ResourceType = "listener"
ResourceTypeRoute ResourceType = "route"
)
var ResourceTypes = extensioncommon.StringSet{
string(ResourceTypeCluster): {},
string(ResourceTypeClusterLoadAssignment): {},
string(ResourceTypeRoute): {},
string(ResourceTypeListener): {},
}
// Op is the type of JSON Patch operation being applied.
type Op string
const (
OpAdd Op = "add"
OpRemove Op = "remove"
)
var Ops = extensioncommon.StringSet{string(OpAdd): {}, string(OpRemove): {}}
// validProxyTypes is the set of supported proxy types for this extension.
var validProxyTypes = extensioncommon.StringSet{
// For now, we only support `connect-proxy`.
string(api.ServiceKindConnectProxy): struct{}{},
}
// Patch describes a single patch operation to modify the specific field of matching
// Envoy resources.
//
// The semantics of Patch closely resemble those of JSON Patch (https://jsonpatch.com/,
// https://datatracker.ietf.org/doc/html/rfc6902/).
type Patch struct {
// ResourceFilter determines which Envoy resource(s) will be patched. ResourceFilter and
// its subfields are not part of the JSON Patch specification.
// This field is required.
ResourceFilter ResourceFilter
// Op represents the JSON Patch operation to be applied by the patch. Supported ops are
// `add` and `remove`:
// - add: Replaces a field with the current value at the specified Path. (Note that
// JSON Patch does not inherently support object “merges”, which must be implemented
// using one discrete add per changed field.)
// - remove: Sets the value at the given path to `nil`. As with `add`, if the target
// field does not exist in the corresponding schema, an error is returned; this
// conforms to JSON Patch semantics and is intended to avoid silent failure when a
// field removal is expected.
// This field is required.
Op Op
// Path specifies where the patch will be applied on a target resource. Path does not
// support array member lookups or appending (`-`).
//
// When an unset but schema-valid (i.e. specified in the corresponding Envoy resource
// .proto) intermediate message field is encountered on the Path, that field will be
// set to its non-`nil` empty (default) value and evaluation will continue. This means
// that even if parents of a field for a given Path are unset, a single patch can set
// deeply nested children of that parent. Subsequent patching of these initialized
// parent field(s) may be necessary to satisfy validation or configuration requirements.
// This field is required.
Path string
// Value specifies the value that will be set at the given Path in a target resource in
// an `add` operation.
//
// Value must be a map with scalar values, a scalar value, or an array of scalar values.
// (Note that this along with the Path constraints noted above imply that setting values
// nested within non-scalar arrays is not supported.)
//
// In every case, the target field will be replaced entirely with the specified value;
// this conforms to JSON Patch `add` semantics. If Value is a map, the non-`nil` empty
// value for the target field will be placed at the specified Path, and then the fields
// specified in the Value map will be explicitly set.
// This field is required if the Op is compatible with a Value per JSON Patch semantics
// (e.g. `add`), and must not be set otherwise.
Value any
}
var _ extensioncommon.BasicExtension = (*propertyOverride)(nil)
func (f *ResourceFilter) isEmpty() bool {
if f == nil {
return true
}
if len(f.Services) > 0 {
return false
}
if string(f.TrafficDirection) != "" {
return false
}
if string(f.ResourceType) != "" {
return false
}
return true
}
func (f *ResourceFilter) validate() error {
if f == nil || f.isEmpty() {
return fmt.Errorf("field ResourceFilter is required")
}
if err := ResourceTypes.CheckRequired(string(f.ResourceType), "ResourceType"); err != nil {
return err
}
if err := extensioncommon.TrafficDirections.CheckRequired(string(f.TrafficDirection), "TrafficDirection"); err != nil {
return err
}
if len(f.Services) > 0 && f.TrafficDirection != extensioncommon.TrafficDirectionOutbound {
return fmt.Errorf("patch contains non-empty ResourceFilter.Services but ResourceFilter.TrafficDirection is not %q",
extensioncommon.TrafficDirectionOutbound)
}
for i := range f.Services {
sn := f.Services[i]
sn.normalize()
if err := sn.validate(); err != nil {
return err
}
}
return nil
}
func (sn *ServiceName) normalize() {
extensioncommon.NormalizeServiceName(&sn.CompoundServiceName)
}
func (sn *ServiceName) validate() error {
if sn.Name == "" {
return fmt.Errorf("service name is required")
}
return nil
}
// validate validates the fields of an individual Patch.
func (p *Patch) validate(debug bool) error {
if err := p.ResourceFilter.validate(); err != nil {
return err
}
if err := Ops.CheckRequired(string(p.Op), "Op"); err != nil {
return err
}
if p.Value != nil && p.Op != OpAdd {
return fmt.Errorf("field Value is not supported for %s operation", p.Op)
}
// Attempt to execute the patch by applying it to a dummy empty struct.
var err error
switch p.ResourceFilter.ResourceType {
case ResourceTypeCluster:
_, err = PatchStruct(&envoy_cluster_v3.Cluster{}, *p, debug)
case ResourceTypeClusterLoadAssignment:
_, err = PatchStruct(&envoy_endpoint_v3.ClusterLoadAssignment{}, *p, debug)
case ResourceTypeRoute:
_, err = PatchStruct(&envoy_route_v3.RouteConfiguration{}, *p, debug)
case ResourceTypeListener:
_, err = PatchStruct(&envoy_listener_v3.Listener{}, *p, debug)
default:
return fmt.Errorf("path validation unimplemented for %q", p.ResourceFilter.ResourceType)
}
return err
}
// validate validates the fields of the property-override extension, including all of its
// configured Patches.
func (p *propertyOverride) validate() error {
if len(p.Patches) == 0 {
return fmt.Errorf("at least one patch is required")
}
var resultErr error
for i, patch := range p.Patches {
if err := patch.validate(p.Debug); err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("invalid Patches[%d]: %w", i, err))
}
}
if p.ProxyType == "" {
p.ProxyType = api.ServiceKindConnectProxy
}
if err := validProxyTypes.CheckRequired(string(p.ProxyType), "ProxyType"); err != nil {
resultErr = multierror.Append(resultErr, err)
}
return resultErr
}
// Constructor follows a specific function signature required for the extension registration.
// It constructs a BasicEnvoyExtender with a patch Extension from the arguments provided by ext.
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
var p propertyOverride
if name := ext.Name; name != api.BuiltinPropertyOverrideExtension {
return nil, fmt.Errorf("expected extension name %q but got %q", api.BuiltinPropertyOverrideExtension, name)
}
// This avoids issues with decoding nested slices, which are error-prone
// due to slice<->map coercion by mapstructure. See HookWeakDecodeFromSlice
// and WeaklyTypedInput docs for more details.
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
decode.HookWeakDecodeFromSlice,
decode.HookTranslateKeys,
),
WeaklyTypedInput: true,
Result: &p,
})
if err != nil {
return nil, fmt.Errorf("error configuring decoder: %v", err)
}
if err := d.Decode(ext.Arguments); err != nil {
return nil, fmt.Errorf("error decoding extension arguments: %v", err)
}
if err := p.validate(); err != nil {
return nil, err
}
return &extensioncommon.BasicEnvoyExtender{
Extension: &p,
}, nil
}
// CanApply returns true if the ProxyType of the extension config matches the kind of the local proxy indicated by the
// RuntimeConfig.
func (p *propertyOverride) CanApply(config *extensioncommon.RuntimeConfig) bool {
return config.Kind == p.ProxyType
}
// PatchRoute patches the provided Envoy Route with any applicable `route` ResourceType patches.
func (p *propertyOverride) PatchRoute(payload extensioncommon.RoutePayload) (*envoy_route_v3.RouteConfiguration, bool, error) {
return patchResourceType[*envoy_route_v3.RouteConfiguration](p, ResourceTypeRoute, payload, &defaultStructPatcher[*envoy_route_v3.RouteConfiguration]{})
}
// PatchCluster patches the provided Envoy Cluster with any applicable `cluster` ResourceType patches.
func (p *propertyOverride) PatchCluster(payload extensioncommon.ClusterPayload) (*envoy_cluster_v3.Cluster, bool, error) {
return patchResourceType[*envoy_cluster_v3.Cluster](p, ResourceTypeCluster, payload, &defaultStructPatcher[*envoy_cluster_v3.Cluster]{})
}
// PatchClusterLoadAssignment patches the provided Envoy ClusterLoadAssignment with any applicable `cluster-load-assignment` ResourceType patches.
func (p *propertyOverride) PatchClusterLoadAssignment(payload extensioncommon.ClusterLoadAssignmentPayload) (*envoy_endpoint_v3.ClusterLoadAssignment, bool, error) {
return patchResourceType[*envoy_endpoint_v3.ClusterLoadAssignment](p, ResourceTypeClusterLoadAssignment, payload, &defaultStructPatcher[*envoy_endpoint_v3.ClusterLoadAssignment]{})
}
// PatchListener patches the provided Envoy Listener with any applicable `listener` ResourceType patches.
func (p *propertyOverride) PatchListener(payload extensioncommon.ListenerPayload) (*envoy_listener_v3.Listener, bool, error) {
return patchResourceType[*envoy_listener_v3.Listener](p, ResourceTypeListener, payload, &defaultStructPatcher[*envoy_listener_v3.Listener]{})
}
// patchResourceType applies Patches matching the given ResourceType to the target K.
// This helper simplifies implementation of the above per-type patch methods defined by BasicExtension.
func patchResourceType[K proto.Message](p *propertyOverride, resourceType ResourceType, payload extensioncommon.Payload[K], patcher structPatcher[K]) (K, bool, error) {
resultPatched := false
var resultErr error
k := payload.Message
for _, patch := range p.Patches {
if !matchesResourceFilter(patch.ResourceFilter, resourceType, payload) {
continue
}
newK, err := patcher.applyPatch(k, patch, p.Debug)
if err != nil {
resultErr = multierror.Append(resultErr, err)
} else {
k = newK
resultPatched = true
}
}
return k, resultPatched && resultErr == nil, resultErr
}
// structPatcher allows us to mock applyPatch in tests.
type structPatcher[K proto.Message] interface {
applyPatch(k K, patch Patch, debug bool) (result K, e error)
}
type defaultStructPatcher[K proto.Message] struct {
}
func (patcher *defaultStructPatcher[K]) applyPatch(k K, patch Patch, debug bool) (result K, e error) {
return PatchStruct(k, patch, debug)
}