From b1232be9e811c70b16b0a5eb280aeedf7a8299b7 Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Mon, 29 Jun 2026 11:33:45 +0200 Subject: [PATCH 1/6] feat: support default mergeType for BackendTrafficPolicy via EnvoyProxy A route-level BackendTrafficPolicy that leaves mergeType unset currently replaces its parent policy rather than merging into it. That means anyone who wants merging has to remember to set mergeType on every policy. This adds a backendTrafficPolicy section to the EnvoyProxy API with a defaultMergeType field. When a route-level policy doesn't set its own mergeType, it picks up this default and merges into the parent instead of replacing it. An explicit mergeType still wins, an optional excludeLabel lets a policy opt out, and policies in the control-plane namespace are left untouched. defaultMergeType only accepts StrategicMerge or JSONMerge. Fixes #9334 Signed-off-by: Maksim Kuchkovskiy --- api/v1alpha1/envoyproxy_types.go | 18 + api/v1alpha1/zz_generated.deepcopy.go | 30 + .../gateway.envoyproxy.io_envoyproxies.yaml | 18 + .../gateway.envoyproxy.io_envoyproxies.yaml | 18 + internal/gatewayapi/backendtrafficpolicy.go | 56 +- .../backendtrafficpolicy_mergedefault_test.go | 185 ++++++ ...default-mergetype-control-plane-ns.in.yaml | 85 +++ ...efault-mergetype-control-plane-ns.out.yaml | 258 +++++++++ ...cy-default-mergetype-exclude-label.in.yaml | 83 +++ ...y-default-mergetype-exclude-label.out.yaml | 277 +++++++++ ...icy-default-mergetype-multi-parent.in.yaml | 119 ++++ ...cy-default-mergetype-multi-parent.out.yaml | 544 ++++++++++++++++++ ...endtrafficpolicy-default-mergetype.in.yaml | 80 +++ ...ndtrafficpolicy-default-mergetype.out.yaml | 284 +++++++++ ...fficpolicy-default-mergetype-envoyproxy.md | 1 + site/content/en/latest/api/extension_types.md | 17 + .../backend-traffic-policy.md | 31 +- test/cel-validation/envoyproxy_test.go | 34 ++ test/helm/gateway-crds-helm/all.out.yaml | 18 + test/helm/gateway-crds-helm/e2e.out.yaml | 18 + .../envoy-gateway-crds.out.yaml | 18 + 21 files changed, 2186 insertions(+), 6 deletions(-) create mode 100644 internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml create mode 100644 release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index a4ef2ee57a..93b03c31c9 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -216,6 +216,24 @@ type EnvoyProxySpec struct { // +kubebuilder:validation:Enum=Replace;StrategicMerge;JSONMerge // +optional MergeType *MergeType `json:"mergeType,omitempty"` + + // BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + // attached to Gateways that use this EnvoyProxy. + // +optional + BackendTrafficPolicy *BackendTrafficPolicyDefaults `json:"backendTrafficPolicy,omitempty"` +} + +// BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. +type BackendTrafficPolicyDefaults struct { + // DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + // so a route-level policy merges into its parent instead of replacing it. + // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge + // +optional + DefaultMergeType *MergeType `json:"defaultMergeType,omitempty"` + + // ExcludeLabel, when present on a BackendTrafficPolicy, opts that policy out of DefaultMergeType. + // +optional + ExcludeLabel *string `json:"excludeLabel,omitempty"` } // EnvoyProxyGeoIP defines shared GeoIP provider settings for EnvoyProxy. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ed5a44424d..34d6787939 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -770,6 +770,31 @@ func (in *BackendTrafficPolicy) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendTrafficPolicyDefaults) DeepCopyInto(out *BackendTrafficPolicyDefaults) { + *out = *in + if in.DefaultMergeType != nil { + in, out := &in.DefaultMergeType, &out.DefaultMergeType + *out = new(MergeType) + **out = **in + } + if in.ExcludeLabel != nil { + in, out := &in.ExcludeLabel, &out.ExcludeLabel + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicyDefaults. +func (in *BackendTrafficPolicyDefaults) DeepCopy() *BackendTrafficPolicyDefaults { + if in == nil { + return nil + } + out := new(BackendTrafficPolicyDefaults) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendTrafficPolicyList) DeepCopyInto(out *BackendTrafficPolicyList) { *out = *in @@ -3281,6 +3306,11 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { *out = new(MergeType) **out = **in } + if in.BackendTrafficPolicy != nil { + in, out := &in.BackendTrafficPolicy, &out.BackendTrafficPolicy + *out = new(BackendTrafficPolicyDefaults) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyProxySpec. diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index 93db35107b..a98f043653 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -236,6 +236,24 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' + backendTrafficPolicy: + description: |- + BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + attached to Gateways that use this EnvoyProxy. + properties: + defaultMergeType: + description: |- + DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + excludeLabel: + description: ExcludeLabel, when present on a BackendTrafficPolicy, + opts that policy out of DefaultMergeType. + type: string + type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index eafaa9486a..e1b1a0ecf3 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -235,6 +235,24 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' + backendTrafficPolicy: + description: |- + BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + attached to Gateways that use this EnvoyProxy. + properties: + defaultMergeType: + description: |- + DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + excludeLabel: + description: ExcludeLabel, when present on a BackendTrafficPolicy, + opts that policy out of DefaultMergeType. + type: string + type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 049cd15b7b..ab840c6622 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -496,7 +496,7 @@ func (t *Translator) processBackendTrafficPolicyForRoute( return } - if policy.Spec.MergeType == nil { + if policy.Spec.MergeType == nil && !t.anyGatewayMergeDefault(policy, parentRefCtxs) { // Set conditions for translation error if it got any if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, nil, nil); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, @@ -512,6 +512,10 @@ func (t *Translator) processBackendTrafficPolicyForRoute( gwNN := utils.NamespacedName(listener.gateway.Gateway) ancestorRef := getAncestorRefForPolicy(gwNN, &listener.Name) + // Resolve the effective mergeType for this gateway: the policy's own value, or + // the default from the gateway's EnvoyProxy unless the policy is excluded. + mergeType := t.effectiveMergeType(policy, listener.gateway.envoyProxy) + // Find Gateway listener level policy listenerMapKey := NamespacedNameWithSection{ NamespacedName: gwNN, @@ -524,8 +528,8 @@ func (t *Translator) processBackendTrafficPolicyForRoute( NamespacedName: gwNN, } gwPolicy := gatewayPolicyMap[gwMapKey] - if gwPolicy == nil && listenerPolicy == nil { - // not found, fall back to the current policy + if mergeType == nil || (gwPolicy == nil && listenerPolicy == nil) { + // No merge for this gateway: apply the policy standalone. if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, @@ -543,9 +547,17 @@ func (t *Translator) processBackendTrafficPolicyForRoute( if listenerPolicy != nil { parentPolicy = listenerPolicy } + + // Carry the effective mergeType so a defaulted policy (no explicit mergeType) + // merges like an explicit one. policy is a deep copy; nothing is persisted. + mergePolicy := policy + if policy.Spec.MergeType == nil { + mergePolicy = policy.DeepCopy() + mergePolicy.Spec.MergeType = mergeType + } // merge with parent policy if err := t.translateBackendTrafficPolicyForRouteWithMerge( - policy, parentPolicy, currTarget, gwNN, &listener.Name, + mergePolicy, parentPolicy, currTarget, gwNN, &listener.Name, targetedRoute, xdsIR, ); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, @@ -1036,6 +1048,42 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, } } +// effectiveMergeType returns the mergeType to use when merging a route-level policy into the +// parent policy on a given gateway: the policy's own value if set, otherwise the default from +// the gateway's EnvoyProxy, unless the policy is in the control-plane namespace or carries the +// exclude label. +func (t *Translator) effectiveMergeType(policy *egv1a1.BackendTrafficPolicy, ep *egv1a1.EnvoyProxy) *egv1a1.MergeType { + if policy.Spec.MergeType != nil { + return policy.Spec.MergeType + } + if ep == nil || ep.Spec.BackendTrafficPolicy == nil || ep.Spec.BackendTrafficPolicy.DefaultMergeType == nil { + return nil + } + if policy.Namespace == t.ControllerNamespace { + return nil + } + d := ep.Spec.BackendTrafficPolicy + if label := ptr.Deref(d.ExcludeLabel, ""); label != "" { + if _, ok := policy.Labels[label]; ok { + return nil + } + } + return d.DefaultMergeType +} + +// anyGatewayMergeDefault reports whether any of the route's parent gateways supplies a default +// mergeType for the policy via its EnvoyProxy. +func (t *Translator) anyGatewayMergeDefault(policy *egv1a1.BackendTrafficPolicy, parentRefCtxs []*RouteParentContext) bool { + for _, p := range parentRefCtxs { + for _, l := range p.listeners { + if t.effectiveMergeType(policy, l.gateway.envoyProxy) != nil { + return true + } + } + } + return false +} + // mergeBackendTrafficPolicy merges route policy into gateway policy. func (t *Translator) mergeBackendTrafficPolicy(routePolicy, gwPolicy *egv1a1.BackendTrafficPolicy) (*egv1a1.BackendTrafficPolicy, error) { if routePolicy.Spec.MergeType == nil || gwPolicy == nil { diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go new file mode 100644 index 0000000000..11d437ece3 --- /dev/null +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -0,0 +1,185 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package gatewayapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestEffectiveMergeType(t *testing.T) { + strategic := egv1a1.StrategicMerge + jsonMerge := egv1a1.JSONMerge + + ep := func(mt *egv1a1.MergeType, label string) *egv1a1.EnvoyProxy { + d := &egv1a1.BackendTrafficPolicyDefaults{DefaultMergeType: mt} + if label != "" { + d.ExcludeLabel = new(label) + } + return &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{BackendTrafficPolicy: d}} + } + btp := func(ns string, labels map[string]string, mt *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { + return &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: ns, Labels: labels}, + Spec: egv1a1.BackendTrafficPolicySpec{MergeType: mt}, + } + } + + tr := &Translator{ControllerNamespace: "eg"} + + tests := []struct { + name string + pol *egv1a1.BackendTrafficPolicy + ep *egv1a1.EnvoyProxy + want *egv1a1.MergeType + }{ + {"explicit value wins over default", btp("app", nil, &jsonMerge), ep(&strategic, ""), &jsonMerge}, + {"default applied when unset", btp("app", nil, nil), ep(&strategic, ""), &strategic}, + {"no envoyproxy stays nil", btp("app", nil, nil), nil, nil}, + {"no default in envoyproxy stays nil", btp("app", nil, nil), ep(nil, ""), nil}, + {"control-plane namespace excluded", btp("eg", nil, nil), ep(&strategic, ""), nil}, + {"exclude label opts out", btp("app", map[string]string{"skip": "x"}, nil), ep(&strategic, "skip"), nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tr.effectiveMergeType(tt.pol, tt.ep) + if tt.want == nil { + assert.Nil(t, got) + return + } + assert.NotNil(t, got) + assert.Equal(t, *tt.want, *got) + }) + } +} + +// TestEffectiveMergeType_AdditionalBranches covers the effectiveMergeType branches that +// TestEffectiveMergeType does not: an EnvoyProxy without a BackendTrafficPolicy section, and an +// exclude label that is configured but not present on the policy (so the default still applies). +func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { + strategic := egv1a1.StrategicMerge + + tr := &Translator{ControllerNamespace: "eg"} + + tests := []struct { + name string + pol *egv1a1.BackendTrafficPolicy + ep *egv1a1.EnvoyProxy + want *egv1a1.MergeType + }{ + { + // ep != nil but ep.Spec.BackendTrafficPolicy == nil -> nil. + name: "envoyproxy without backendTrafficPolicy stays nil", + pol: &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, + }, + ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}}, + want: nil, + }, + { + // exclude label configured but absent on the policy -> falls through to the default. + name: "exclude label configured but not on policy applies default", + pol: &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app", Labels: map[string]string{"other": "x"}}, + }, + ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + DefaultMergeType: &strategic, + ExcludeLabel: new("skip"), + }, + }}, + want: &strategic, + }, + { + // nil labels map with an exclude label configured -> default still applies. + name: "nil labels with exclude label configured applies default", + pol: &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, + }, + ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + DefaultMergeType: &strategic, + ExcludeLabel: new("skip"), + }, + }}, + want: &strategic, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tr.effectiveMergeType(tt.pol, tt.ep) + if tt.want == nil { + assert.Nil(t, got) + return + } + assert.NotNil(t, got) + assert.Equal(t, *tt.want, *got) + }) + } +} + +// TestAnyGatewayMergeDefault covers anyGatewayMergeDefault, which reports whether any of a route's +// parent gateways supplies a default mergeType for the policy via its EnvoyProxy. +func TestAnyGatewayMergeDefault(t *testing.T) { + strategic := egv1a1.StrategicMerge + + epWithDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{DefaultMergeType: &strategic}, + }} + epNoDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}} + + // parentWith builds a parent context whose listeners reference gateways carrying the given + // EnvoyProxies (one listener per EnvoyProxy). + parentWith := func(eps ...*egv1a1.EnvoyProxy) *RouteParentContext { + p := &RouteParentContext{} + for _, ep := range eps { + p.listeners = append(p.listeners, &ListenerContext{ + gateway: &GatewayContext{envoyProxy: ep}, + }) + } + return p + } + + policy := &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, + } + + tr := &Translator{ControllerNamespace: "eg"} + + tests := []struct { + name string + parents []*RouteParentContext + want bool + }{ + {"no parents", nil, false}, + {"parent with no listeners", []*RouteParentContext{parentWith()}, false}, + {"single parent supplies default", []*RouteParentContext{parentWith(epWithDefault)}, true}, + {"single parent no default", []*RouteParentContext{parentWith(epNoDefault)}, false}, + {"mixed listeners on one parent", []*RouteParentContext{parentWith(epNoDefault, epWithDefault)}, true}, + { + "multiple parents one supplies default", + []*RouteParentContext{parentWith(epNoDefault), parentWith(epWithDefault)}, + true, + }, + { + "multiple parents none supply default", + []*RouteParentContext{parentWith(epNoDefault), parentWith(nil)}, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tr.anyGatewayMergeDefault(policy, tt.parents)) + }) + } +} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml new file mode 100644 index 0000000000..36fa213df8 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml @@ -0,0 +1,85 @@ +namespaces: + - apiVersion: v1 + kind: Namespace + metadata: + name: envoy-gateway-system +envoyProxyForGatewayClass: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: envoy-gateway-system + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + tcp: + connectTimeout: 15s + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + httpUpgrade: + - type: websocket + # Route-level policy lives in the control-plane (controller) namespace, so the EnvoyProxy + # defaultMergeType does not apply and the policy replaces the gateway-level policy. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway-system + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + connection: + bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml new file mode 100644 index 0000000000..0babf0f8b0 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml @@ -0,0 +1,258 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-route + namespace: envoy-gateway-system + spec: + connection: + bufferLimit: 100M + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being overridden by other backendTrafficPolicies + for these routes: [envoy-gateway-system/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: envoy-gateway-system + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: 'Failed to process route rule 0 backendRef 0: service envoy-gateway-system/service-1 + not found.' + reason: BackendNotFound + status: "False" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + logging: {} + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - directResponse: + statusCode: 500 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: envoy-gateway-system + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: envoy-gateway-system + name: httproute/envoy-gateway-system/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + timeout: + tcp: + connectTimeout: 10s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml new file mode 100644 index 0000000000..a87572c121 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml @@ -0,0 +1,83 @@ +envoyProxyForGatewayClass: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + excludeLabel: gateway.envoyproxy.io/skip-merge-default +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + tcp: + connectTimeout: 15s + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + httpUpgrade: + - type: websocket + # Route-level policy carries the EnvoyProxy excludeLabel, so it opts out of the + # defaultMergeType and replaces (rather than merges into) the gateway-level policy. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + labels: + gateway.envoyproxy.io/skip-merge-default: "true" + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + connection: + bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml new file mode 100644 index 0000000000..62ea6ec8fc --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml @@ -0,0 +1,277 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + labels: + gateway.envoyproxy.io/skip-merge-default: "true" + name: policy-for-route + namespace: default + spec: + connection: + bufferLimit: 100M + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being overridden by other backendTrafficPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + excludeLabel: gateway.envoyproxy.io/skip-merge-default + logging: {} + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + timeout: + tcp: + connectTimeout: 10s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml new file mode 100644 index 0000000000..e4c3015719 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml @@ -0,0 +1,119 @@ +envoyProxiesForGateways: + # Attached to gateway-1 only: supplies a default mergeType. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway + name: ep-merge-default + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: ep-merge-default + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + # gateway-2 has no EnvoyProxy default, so the same route policy applies standalone here. + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + tcp: + connectTimeout: 15s + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + httpUpgrade: + - type: websocket + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + timeout: + tcp: + connectTimeout: 25s + httpUpgrade: + - type: websocket + # Route-level policy with no mergeType: merges into gateway-1's policy (which has the default), + # but applies standalone on gateway-2 (which has no default). + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + connection: + bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml new file mode 100644 index 0000000000..1f7b919a17 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml @@ -0,0 +1,544 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-route + namespace: default + spec: + connection: + bufferLimit: 100M + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Merged with policy envoy-gateway/policy-for-gateway-1 + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway-1 + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway-2 + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + timeout: + tcp: + connectTimeout: 25s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being overridden by other backendTrafficPolicies + for these routes: [default/httproute-1]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +envoyProxiesForGateways: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: ep-merge-default + namespace: envoy-gateway + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + logging: {} + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: + parametersRef: + group: gateway.envoyproxy.io + kind: EnvoyProxy + name: ep-merge-default + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + - name: gateway-2 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: ep-merge-default + namespace: envoy-gateway + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + logging: {} + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: EnvoyProxy has been accepted. + reason: Accepted + status: "True" + type: Accepted + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system + envoy-gateway/gateway-2: + proxy: + listeners: + - name: envoy-gateway/gateway-2/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-2 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + httpUpgrade: + - type: websocket + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 10s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 + envoy-gateway/gateway-2: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-2-4a0e4eb9 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-2 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + timeout: + tcp: + connectTimeout: 10s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml new file mode 100644 index 0000000000..f4dd15ad2c --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml @@ -0,0 +1,80 @@ +envoyProxyForGatewayClass: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + tcp: + connectTimeout: 15s + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + httpUpgrade: + - type: websocket + # Route-level policy with no mergeType: the EnvoyProxy defaultMergeType makes it merge + # into the gateway-level policy instead of replacing it. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + connection: + bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml new file mode 100644 index 0000000000..5379470bb5 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml @@ -0,0 +1,284 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-route + namespace: default + spec: + connection: + bufferLimit: 100M + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + timeout: + tcp: + connectTimeout: 10s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Merged with policy envoy-gateway/policy-for-gateway + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + logging: {} + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + httpUpgrade: + - type: websocket + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + tcp: + connectTimeout: 10s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md b/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md new file mode 100644 index 0000000000..7c08c9bba6 --- /dev/null +++ b/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md @@ -0,0 +1 @@ +Added `backendTrafficPolicy.defaultMergeType` to `EnvoyProxy`, so a route-level `BackendTrafficPolicy` that doesn't set `mergeType` merges into its parent policy instead of replacing it. An optional `excludeLabel` lets individual policies opt out. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 741ff2f568..8eac2518b1 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -559,6 +559,21 @@ between the Envoy Proxy listener and the backend service. | `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#policystatus)_ | true | | status defines the current status of BackendTrafficPolicy. | +#### BackendTrafficPolicyDefaults + + + +BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `defaultMergeType` | _[MergeType](#mergetype)_ | false | | DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | +| `excludeLabel` | _string_ | false | | ExcludeLabel, when present on a BackendTrafficPolicy, opts that policy out of DefaultMergeType. | + + #### BackendTrafficPolicySpec @@ -2241,6 +2256,7 @@ _Appears in:_ | `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). | | `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. | +| `backendTrafficPolicy` | _[BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources
attached to Gateways that use this EnvoyProxy. | #### EnvoyProxyStatus @@ -4162,6 +4178,7 @@ _Underlying type:_ _string_ MergeType defines the type of merge operation _Appears in:_ +- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) - [EnvoyProxySpec](#envoyproxyspec) - [KubernetesPatchSpec](#kubernetespatchspec) diff --git a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md index daa6fa1abf..0f51907478 100644 --- a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md +++ b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md @@ -147,7 +147,7 @@ spec: In this example, `alpha-policy` would take precedence due to its earlier creation time, so the HTTPRoute would use `maxConnections: 30`. -When the `mergeType` field is unset, no merging occurs and only the most specific configuration takes effect. However, policies can be configured to merge with parent policies using the `mergeType` field (see [Policy Merging](#policy-merging) section below). +When the `mergeType` field is unset, no merging occurs and only the most specific configuration takes effect, unless a default is configured on the EnvoyProxy (see [Defaulting mergeType via EnvoyProxy](#defaulting-mergetype-via-envoyproxy)). Policies can also opt into merging explicitly using the `mergeType` field (see [Policy Merging](#policy-merging) section below). ## Policy Merging @@ -214,10 +214,37 @@ spec: In this example, the route-level policy merges with the gateway-level policy, resulting in both rate limits being enforced: the global 100 requests/second abuse limit and the route-specific 5 requests/minute limit. +### Defaulting mergeType via EnvoyProxy + +Setting `mergeType` on every route-level policy can be repetitive when an organization wants merging to be the norm. To make merging the default, configure `defaultMergeType` on the [EnvoyProxy](../../api/extension_types#envoyproxy) attached to the GatewayClass or Gateway: + +```yaml +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyProxy +metadata: + name: custom-proxy-config + namespace: envoy-gateway-system +spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + # Optional: a policy carrying this label key opts out of the default and replaces + # its parent instead of merging. The label's value is ignored. + excludeLabel: gateway.envoyproxy.io/skip-merge-default +``` + +With this configuration, a route-level BackendTrafficPolicy that does **not** set `mergeType` merges into its parent (gateway-level or listener-level) policy as if it had set `mergeType: StrategicMerge`. The default follows the same precedence as other EnvoyProxy settings, so it can be set once at the GatewayClass level or overridden per Gateway. + +The defaulting behavior follows these rules: + +- A policy's own `mergeType` always wins; the default only applies when `mergeType` is unset. +- `defaultMergeType` accepts only `StrategicMerge` or `JSONMerge`. `Replace` is not allowed because defaulting to a replace would have no effect. +- A policy that carries the configured `excludeLabel` key opts out of the default and replaces its parent, even when `mergeType` is unset. +- Policies in the Envoy Gateway control-plane namespace are not defaulted. + ### Key Constraints - The `mergeType` field can only be set on policies targeting child resources (like HTTPRoute), not parent resources (like Gateway) -- When `mergeType` is unset, no merging occurs - only the most specific policy takes effect +- When `mergeType` is unset, no merging occurs - only the most specific policy takes effect - unless a [default is configured on the EnvoyProxy](#defaulting-mergetype-via-envoyproxy) - The merged configuration combines both policies, enabling layered protection strategies ## Related Resources diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 20e425157c..b75f1e3705 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2485,6 +2485,40 @@ func TestEnvoyProxyProvider(t *testing.T) { }, wantErrors: []string{"If type is Remote, local field must not be set"}, }, + { + desc: "backendTrafficPolicy defaultMergeType StrategicMerge is valid", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + DefaultMergeType: new(egv1a1.StrategicMerge), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "backendTrafficPolicy defaultMergeType JSONMerge with excludeLabel is valid", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + DefaultMergeType: new(egv1a1.JSONMerge), + ExcludeLabel: new("gateway.envoyproxy.io/skip-merge-default"), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "backendTrafficPolicy defaultMergeType Replace is rejected", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + DefaultMergeType: new(egv1a1.MergeType("Replace")), + }, + } + }, + wantErrors: []string{"Unsupported value: \"Replace\": supported values: \"StrategicMerge\", \"JSONMerge\""}, + }, } for _, tc := range cases { diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 5499e8824e..f398e4c47c 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -33895,6 +33895,24 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' + backendTrafficPolicy: + description: |- + BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + attached to Gateways that use this EnvoyProxy. + properties: + defaultMergeType: + description: |- + DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + excludeLabel: + description: ExcludeLabel, when present on a BackendTrafficPolicy, + opts that policy out of DefaultMergeType. + type: string + type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 194623aac3..1151e904a7 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -9833,6 +9833,24 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' + backendTrafficPolicy: + description: |- + BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + attached to Gateways that use this EnvoyProxy. + properties: + defaultMergeType: + description: |- + DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + excludeLabel: + description: ExcludeLabel, when present on a BackendTrafficPolicy, + opts that policy out of DefaultMergeType. + type: string + type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 5e3951fca6..58b79ed9ce 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -9833,6 +9833,24 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' + backendTrafficPolicy: + description: |- + BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources + attached to Gateways that use this EnvoyProxy. + properties: + defaultMergeType: + description: |- + DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + excludeLabel: + description: ExcludeLabel, when present on a BackendTrafficPolicy, + opts that policy out of DefaultMergeType. + type: string + type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. From 80dcd87d81a85a0c82a60d778ca927db03a4b0dc Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Tue, 30 Jun 2026 11:00:39 +0200 Subject: [PATCH 2/6] refactor: share PolicyDefaults type and address review feedback Rename the EnvoyProxy policy-defaults type from BackendTrafficPolicyDefaults to a reusable PolicyDefaults, so other xPolicies can adopt the same shape. The backendTrafficPolicy field is unchanged; only the type name changes. Also fixes two review findings: - Validate defaultMergeType on the EnvoyGateway default EnvoyProxySpec. That spec is loaded via config (ValidateEnvoyGateway), not CRD admission, so the enum does not apply and a value like Replace could slip through. effectiveMergeType also defensively ignores a non-merge value, so a stray default can never report a "merged" status while actually replacing. - Scope a defaulted merged route policy to its own Gateway's listeners. In MergeGateways mode all Gateways share one IR keyed by section name, so a route attached to two Gateways with different parent policies received the first Gateway's merge on both. applyTrafficFeatureToRoute now filters listeners by the target Gateway. Adds a merged-gateways golden fixture. Signed-off-by: Maksim Kuchkovskiy --- api/v1alpha1/envoyproxy_types.go | 11 +- .../validation/envoygateway_validate.go | 20 + .../validation/envoygateway_validate_test.go | 30 ++ api/v1alpha1/zz_generated.deepcopy.go | 52 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 6 +- .../gateway.envoyproxy.io_envoyproxies.yaml | 6 +- internal/gatewayapi/backendtrafficpolicy.go | 35 +- .../backendtrafficpolicy_mergedefault_test.go | 23 +- ...-default-mergetype-merged-gateways.in.yaml | 102 ++++ ...default-mergetype-merged-gateways.out.yaml | 447 ++++++++++++++++++ site/content/en/latest/api/extension_types.md | 35 +- test/cel-validation/envoyproxy_test.go | 6 +- test/helm/gateway-crds-helm/all.out.yaml | 6 +- test/helm/gateway-crds-helm/e2e.out.yaml | 6 +- .../envoy-gateway-crds.out.yaml | 6 +- 15 files changed, 719 insertions(+), 72 deletions(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 93b03c31c9..661a167d3f 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -220,18 +220,19 @@ type EnvoyProxySpec struct { // BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources // attached to Gateways that use this EnvoyProxy. // +optional - BackendTrafficPolicy *BackendTrafficPolicyDefaults `json:"backendTrafficPolicy,omitempty"` + BackendTrafficPolicy *PolicyDefaults `json:"backendTrafficPolicy,omitempty"` } -// BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. -type BackendTrafficPolicyDefaults struct { - // DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, +// PolicyDefaults defines default settings shared by Envoy Gateway xPolicies (e.g. BackendTrafficPolicy) +// attached to Gateways that use this EnvoyProxy. +type PolicyDefaults struct { + // DefaultMergeType is the mergeType used for a policy that does not set one, // so a route-level policy merges into its parent instead of replacing it. // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge // +optional DefaultMergeType *MergeType `json:"defaultMergeType,omitempty"` - // ExcludeLabel, when present on a BackendTrafficPolicy, opts that policy out of DefaultMergeType. + // ExcludeLabel, when present on a policy, opts that policy out of DefaultMergeType. // +optional ExcludeLabel *string `json:"excludeLabel,omitempty"` } diff --git a/api/v1alpha1/validation/envoygateway_validate.go b/api/v1alpha1/validation/envoygateway_validate.go index 79ec2a85ce..7d93fb4d9a 100644 --- a/api/v1alpha1/validation/envoygateway_validate.go +++ b/api/v1alpha1/validation/envoygateway_validate.go @@ -71,9 +71,29 @@ func ValidateEnvoyGateway(eg *egv1a1.EnvoyGateway) error { return fmt.Errorf("disableLua and enableLua must not have the same value") } + if err := validateEnvoyGatewayDefaultEnvoyProxy(eg.EnvoyProxy); err != nil { + return err + } + return nil } +// validateEnvoyGatewayDefaultEnvoyProxy validates fields of the default EnvoyProxySpec that are +// enforced by CRD validation for EnvoyProxy resources but not when the spec is provided inline as +// the EnvoyGateway default, since that path does not go through CRD admission. +func validateEnvoyGatewayDefaultEnvoyProxy(spec *egv1a1.EnvoyProxySpec) error { + if spec == nil || spec.BackendTrafficPolicy == nil || spec.BackendTrafficPolicy.DefaultMergeType == nil { + return nil + } + switch *spec.BackendTrafficPolicy.DefaultMergeType { + case egv1a1.StrategicMerge, egv1a1.JSONMerge: + return nil + default: + return fmt.Errorf("envoyProxy.backendTrafficPolicy.defaultMergeType must be one of StrategicMerge or JSONMerge, got %q", + *spec.BackendTrafficPolicy.DefaultMergeType) + } +} + // WarnEnvoyGateway returns deprecation warnings for the provided EnvoyGateway configuration. func WarnEnvoyGateway(eg *egv1a1.EnvoyGateway) []string { if eg == nil || eg.ExtensionAPIs == nil { diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index de79017ce8..8f2f254c14 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1054,6 +1054,36 @@ func TestValidateEnvoyGateway(t *testing.T) { }, expect: true, }, + { + name: "default EnvoyProxy with valid backendTrafficPolicy.defaultMergeType", + eg: &egv1a1.EnvoyGateway{ + EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ + Gateway: egv1a1.DefaultGateway(), + Provider: egv1a1.DefaultEnvoyGatewayProvider(), + EnvoyProxy: &egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ + DefaultMergeType: new(egv1a1.StrategicMerge), + }, + }, + }, + }, + expect: true, + }, + { + name: "default EnvoyProxy with invalid backendTrafficPolicy.defaultMergeType", + eg: &egv1a1.EnvoyGateway{ + EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ + Gateway: egv1a1.DefaultGateway(), + Provider: egv1a1.DefaultEnvoyGatewayProvider(), + EnvoyProxy: &egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ + DefaultMergeType: new(egv1a1.Replace), + }, + }, + }, + }, + expect: false, + }, } for _, tc := range testCases { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 34d6787939..d6fc7202b9 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -770,31 +770,6 @@ func (in *BackendTrafficPolicy) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendTrafficPolicyDefaults) DeepCopyInto(out *BackendTrafficPolicyDefaults) { - *out = *in - if in.DefaultMergeType != nil { - in, out := &in.DefaultMergeType, &out.DefaultMergeType - *out = new(MergeType) - **out = **in - } - if in.ExcludeLabel != nil { - in, out := &in.ExcludeLabel, &out.ExcludeLabel - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicyDefaults. -func (in *BackendTrafficPolicyDefaults) DeepCopy() *BackendTrafficPolicyDefaults { - if in == nil { - return nil - } - out := new(BackendTrafficPolicyDefaults) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendTrafficPolicyList) DeepCopyInto(out *BackendTrafficPolicyList) { *out = *in @@ -3308,7 +3283,7 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { } if in.BackendTrafficPolicy != nil { in, out := &in.BackendTrafficPolicy, &out.BackendTrafficPolicy - *out = new(BackendTrafficPolicyDefaults) + *out = new(PolicyDefaults) (*in).DeepCopyInto(*out) } } @@ -6515,6 +6490,31 @@ func (in *PerRetryPolicy) DeepCopy() *PerRetryPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PolicyDefaults) DeepCopyInto(out *PolicyDefaults) { + *out = *in + if in.DefaultMergeType != nil { + in, out := &in.DefaultMergeType, &out.DefaultMergeType + *out = new(MergeType) + **out = **in + } + if in.ExcludeLabel != nil { + in, out := &in.ExcludeLabel, &out.ExcludeLabel + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyDefaults. +func (in *PolicyDefaults) DeepCopy() *PolicyDefaults { + if in == nil { + return nil + } + out := new(PolicyDefaults) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyTargetReferences) DeepCopyInto(out *PolicyTargetReferences) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index a98f043653..593353da8f 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -243,15 +243,15 @@ spec: properties: defaultMergeType: description: |- - DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + DefaultMergeType is the mergeType used for a policy that does not set one, so a route-level policy merges into its parent instead of replacing it. enum: - StrategicMerge - JSONMerge type: string excludeLabel: - description: ExcludeLabel, when present on a BackendTrafficPolicy, - opts that policy out of DefaultMergeType. + description: ExcludeLabel, when present on a policy, opts that + policy out of DefaultMergeType. type: string type: object bootstrap: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index e1b1a0ecf3..f4f378a7f9 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -242,15 +242,15 @@ spec: properties: defaultMergeType: description: |- - DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + DefaultMergeType is the mergeType used for a policy that does not set one, so a route-level policy merges into its parent instead of replacing it. enum: - StrategicMerge - JSONMerge type: string excludeLabel: - description: ExcludeLabel, when present on a BackendTrafficPolicy, - opts that policy out of DefaultMergeType. + description: ExcludeLabel, when present on a policy, opts that + policy out of DefaultMergeType. type: string type: object bootstrap: diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index ab840c6622..a04dca153f 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -850,7 +850,7 @@ func (t *Translator) translateBackendTrafficPolicyForRoute( // Skip if not the gateway wanted continue } - t.applyTrafficFeatureToRoute(route, tf, errs, policy, target, x, policyTargetListener) + t.applyTrafficFeatureToRoute(route, tf, errs, policy, target, x, policyTargetGatewayNN, policyTargetListener) } return errs @@ -911,16 +911,29 @@ func (t *Translator) translateBackendTrafficPolicyForRouteWithMerge( // should not happen. return nil } - t.applyTrafficFeatureToRoute(route, tf, errs, mergedPolicy, target, x, policyTargetListener) + t.applyTrafficFeatureToRoute(route, tf, errs, mergedPolicy, target, x, &policyTargetGatewayNN, policyTargetListener) return nil } +// listenerBelongsToGateway reports whether an IR listener (named "//
...") +// belongs to the given Gateway. In MergeGateways mode multiple Gateways share a single IR, so a +// route policy merged with a Gateway-specific parent must be scoped to that Gateway's listeners; +// otherwise the section name alone (e.g. "http") would also match sibling Gateways' listeners and +// the first applied policy would win for all of them. A nil gwNN means "no Gateway scoping". +func listenerBelongsToGateway(listenerName string, gwNN *types.NamespacedName) bool { + if gwNN == nil { + return true + } + return strings.HasPrefix(listenerName, gwNN.Namespace+"/"+gwNN.Name+"/") +} + func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, tf *ir.TrafficFeatures, errs error, policy *egv1a1.BackendTrafficPolicy, target policyTargetReferenceWithSectionName, x *ir.Xds, + policyTargetGatewayNN *types.NamespacedName, policyTargetListener *gwapiv1.SectionName, ) { routeStatName := "" @@ -930,6 +943,11 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, prefix := irRoutePrefix(route) for _, tcp := range x.TCP { + // In MergeGateways mode multiple Gateways share one IR, so scope to the target Gateway's + // listeners; otherwise the section name alone would match sibling Gateways' listeners. + if !listenerBelongsToGateway(tcp.Name, policyTargetGatewayNN) { + continue + } // if listenerName is not nil, only apply to the specific listener if policyTargetListener != nil && string(*policyTargetListener) != tcp.Metadata.SectionName { // Skip if not the listener wanted @@ -958,6 +976,9 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, } for _, udp := range x.UDP { + if !listenerBelongsToGateway(udp.Name, policyTargetGatewayNN) { + continue + } // if listenerName is not nil, only apply to the specific listener if policyTargetListener != nil && string(*policyTargetListener) != udp.Metadata.SectionName { // Skip if not the listener wanted @@ -980,6 +1001,9 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, routesWithDirectResponse := sets.New[string]() for _, http := range x.HTTP { + if !listenerBelongsToGateway(http.Name, policyTargetGatewayNN) { + continue + } // if listenerName is not nil, only apply to the specific listener if policyTargetListener != nil && string(*policyTargetListener) != http.Metadata.SectionName { // Skip if not the listener wanted @@ -1068,6 +1092,13 @@ func (t *Translator) effectiveMergeType(policy *egv1a1.BackendTrafficPolicy, ep return nil } } + // Defense in depth: the CRD enum restricts DefaultMergeType to StrategicMerge/JSONMerge, but the + // EnvoyGateway default EnvoyProxySpec is not subject to CRD validation. Ignore anything that is + // not a real merge so a stray value (e.g. Replace) can never produce a "merged" status while + // actually replacing the parent. + if *d.DefaultMergeType != egv1a1.StrategicMerge && *d.DefaultMergeType != egv1a1.JSONMerge { + return nil + } return d.DefaultMergeType } diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go index 11d437ece3..f9ba2279e6 100644 --- a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -19,7 +19,7 @@ func TestEffectiveMergeType(t *testing.T) { jsonMerge := egv1a1.JSONMerge ep := func(mt *egv1a1.MergeType, label string) *egv1a1.EnvoyProxy { - d := &egv1a1.BackendTrafficPolicyDefaults{DefaultMergeType: mt} + d := &egv1a1.PolicyDefaults{DefaultMergeType: mt} if label != "" { d.ExcludeLabel = new(label) } @@ -91,7 +91,7 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: "app", Labels: map[string]string{"other": "x"}}, }, ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ DefaultMergeType: &strategic, ExcludeLabel: new("skip"), }, @@ -105,13 +105,28 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, }, ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ DefaultMergeType: &strategic, ExcludeLabel: new("skip"), }, }}, want: &strategic, }, + { + // Defense in depth: a non-merge value (e.g. Replace, which can slip through the + // unvalidated EnvoyGateway default spec) is ignored instead of producing a "merged" + // status while actually replacing. + name: "replace default is ignored", + pol: &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, + }, + ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ + DefaultMergeType: new(egv1a1.Replace), + }, + }}, + want: nil, + }, } for _, tt := range tests { @@ -133,7 +148,7 @@ func TestAnyGatewayMergeDefault(t *testing.T) { strategic := egv1a1.StrategicMerge epWithDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{DefaultMergeType: &strategic}, + BackendTrafficPolicy: &egv1a1.PolicyDefaults{DefaultMergeType: &strategic}, }} epNoDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml new file mode 100644 index 0000000000..b81bd6472c --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml @@ -0,0 +1,102 @@ +envoyProxyForGatewayClass: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + mergeGateways: true + backendTrafficPolicy: + defaultMergeType: StrategicMerge +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 8080 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + # Distinct parent policy per Gateway: gateway-1 sets maxConnections 1111, gateway-2 sets 2222. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway-1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + circuitBreaker: + maxConnections: 1111 + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway-2 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + circuitBreaker: + maxConnections: 2222 + # Route-level policy with no mergeType: should merge into each Gateway's own parent, so the route + # on gateway-1 gets maxConnections 1111 and the route on gateway-2 gets 2222. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + connection: + bufferLimit: 100Mi diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml new file mode 100644 index 0000000000..4c1c3350cb --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml @@ -0,0 +1,447 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-route + namespace: default + spec: + connection: + bufferLimit: 100Mi + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Merged with policy envoy-gateway/policy-for-gateway-1 + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Merged with policy envoy-gateway/policy-for-gateway-2 + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway-1 + namespace: envoy-gateway + spec: + circuitBreaker: + maxConnections: 1111 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway-2 + namespace: envoy-gateway + spec: + circuitBreaker: + maxConnections: 2222 + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 8080 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + - name: gateway-2 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + name: test + namespace: envoy-gateway-system + spec: + backendTrafficPolicy: + defaultMergeType: StrategicMerge + logging: {} + mergeGateways: true + status: {} + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + - name: envoy-gateway/gateway-2/http + ports: + - containerPort: 8080 + name: http-8080 + protocol: HTTP + servicePort: 8080 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway-class + namespace: envoy-gateway-system +xdsIR: + envoy-gateway-class: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-class-3b1df594 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway-class + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-class-3b1df594 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway-class + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 104857600 + circuitBreaker: + maxConnections: 1111 + - address: 0.0.0.0 + externalPort: 8080 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-2 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-2/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 8080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 104857600 + circuitBreaker: + maxConnections: 2222 + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 8eac2518b1..faa295a66e 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -559,21 +559,6 @@ between the Envoy Proxy listener and the backend service. | `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#policystatus)_ | true | | status defines the current status of BackendTrafficPolicy. | -#### BackendTrafficPolicyDefaults - - - -BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. - -_Appears in:_ -- [EnvoyProxySpec](#envoyproxyspec) - -| Field | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `defaultMergeType` | _[MergeType](#mergetype)_ | false | | DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | -| `excludeLabel` | _string_ | false | | ExcludeLabel, when present on a BackendTrafficPolicy, opts that policy out of DefaultMergeType. | - - #### BackendTrafficPolicySpec @@ -2256,7 +2241,7 @@ _Appears in:_ | `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). | | `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. | -| `backendTrafficPolicy` | _[BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources
attached to Gateways that use this EnvoyProxy. | +| `backendTrafficPolicy` | _[PolicyDefaults](#policydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources
attached to Gateways that use this EnvoyProxy. | #### EnvoyProxyStatus @@ -4178,10 +4163,10 @@ _Underlying type:_ _string_ MergeType defines the type of merge operation _Appears in:_ -- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) - [EnvoyProxySpec](#envoyproxyspec) - [KubernetesPatchSpec](#kubernetespatchspec) +- [PolicyDefaults](#policydefaults) - [SecurityPolicySpec](#securitypolicyspec) | Value | Description | @@ -4583,6 +4568,22 @@ _Appears in:_ | `backOff` | _[BackOffPolicy](#backoffpolicy)_ | false | | Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential
back-off algorithm for retries. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries | +#### PolicyDefaults + + + +PolicyDefaults defines default settings shared by Envoy Gateway xPolicies (e.g. BackendTrafficPolicy) +attached to Gateways that use this EnvoyProxy. + +_Appears in:_ +- [EnvoyProxySpec](#envoyproxyspec) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `defaultMergeType` | _[MergeType](#mergetype)_ | false | | DefaultMergeType is the mergeType used for a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | +| `excludeLabel` | _string_ | false | | ExcludeLabel, when present on a policy, opts that policy out of DefaultMergeType. | + + #### PolicyTargetReferences diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index b75f1e3705..44c042f5bf 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2489,7 +2489,7 @@ func TestEnvoyProxyProvider(t *testing.T) { desc: "backendTrafficPolicy defaultMergeType StrategicMerge is valid", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ DefaultMergeType: new(egv1a1.StrategicMerge), }, } @@ -2500,7 +2500,7 @@ func TestEnvoyProxyProvider(t *testing.T) { desc: "backendTrafficPolicy defaultMergeType JSONMerge with excludeLabel is valid", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ DefaultMergeType: new(egv1a1.JSONMerge), ExcludeLabel: new("gateway.envoyproxy.io/skip-merge-default"), }, @@ -2512,7 +2512,7 @@ func TestEnvoyProxyProvider(t *testing.T) { desc: "backendTrafficPolicy defaultMergeType Replace is rejected", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{ + BackendTrafficPolicy: &egv1a1.PolicyDefaults{ DefaultMergeType: new(egv1a1.MergeType("Replace")), }, } diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index f398e4c47c..17aad73ce7 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -33902,15 +33902,15 @@ spec: properties: defaultMergeType: description: |- - DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + DefaultMergeType is the mergeType used for a policy that does not set one, so a route-level policy merges into its parent instead of replacing it. enum: - StrategicMerge - JSONMerge type: string excludeLabel: - description: ExcludeLabel, when present on a BackendTrafficPolicy, - opts that policy out of DefaultMergeType. + description: ExcludeLabel, when present on a policy, opts that + policy out of DefaultMergeType. type: string type: object bootstrap: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 1151e904a7..30de462c6d 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -9840,15 +9840,15 @@ spec: properties: defaultMergeType: description: |- - DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + DefaultMergeType is the mergeType used for a policy that does not set one, so a route-level policy merges into its parent instead of replacing it. enum: - StrategicMerge - JSONMerge type: string excludeLabel: - description: ExcludeLabel, when present on a BackendTrafficPolicy, - opts that policy out of DefaultMergeType. + description: ExcludeLabel, when present on a policy, opts that + policy out of DefaultMergeType. type: string type: object bootstrap: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 58b79ed9ce..e01cdf4b75 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -9840,15 +9840,15 @@ spec: properties: defaultMergeType: description: |- - DefaultMergeType is the mergeType used for a BackendTrafficPolicy that does not set one, + DefaultMergeType is the mergeType used for a policy that does not set one, so a route-level policy merges into its parent instead of replacing it. enum: - StrategicMerge - JSONMerge type: string excludeLabel: - description: ExcludeLabel, when present on a BackendTrafficPolicy, - opts that policy out of DefaultMergeType. + description: ExcludeLabel, when present on a policy, opts that + policy out of DefaultMergeType. type: string type: object bootstrap: From f71b7665fd5985fbb0527f551f1c6d7222eb8128 Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Tue, 30 Jun 2026 12:37:50 +0200 Subject: [PATCH 3/6] test: cover MergeGateways scoping for TCP and UDP listeners Add a unit test for applyTrafficFeatureToRoute exercising the TCP and UDP listener skip branches when a route policy targets a different Gateway under the shared MergeGateways IR. The HTTP path is already covered by the merged-gateways golden fixture; this brings the new scoping code to full patch coverage. Signed-off-by: Maksim Kuchkovskiy --- .../backendtrafficpolicy_mergedefault_test.go | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go index f9ba2279e6..0fb9f7cd33 100644 --- a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -10,8 +10,11 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" ) func TestEffectiveMergeType(t *testing.T) { @@ -198,3 +201,42 @@ func TestAnyGatewayMergeDefault(t *testing.T) { }) } } + +// TestApplyTrafficFeatureToRoute_MergeGatewayScoping covers the MergeGateways scoping in +// applyTrafficFeatureToRoute for the TCP and UDP listener loops: a listener belonging to a +// different Gateway must be skipped, so a defaulted merged policy does not bleed across Gateways +// that share one IR. The HTTP path is covered by the merged-gateways golden fixture. +func TestApplyTrafficFeatureToRoute_MergeGatewayScoping(t *testing.T) { + tr := &Translator{} + gwNN := &types.NamespacedName{Namespace: "envoy-gateway", Name: "gw"} + policy := &egv1a1.BackendTrafficPolicy{} + target := policyTargetReferenceWithSectionName{} + + t.Run("tcp listener of another gateway is skipped", func(t *testing.T) { + route := &TCPRouteContext{TCPRoute: &gwapiv1a2.TCPRoute{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "tcproute-1"}, + }} + sibling := &ir.TCPRoute{Destination: &ir.RouteDestination{Name: irRoutePrefix(route) + "rule/0"}} + x := &ir.Xds{TCP: []*ir.TCPListener{{ + CoreListenerDetails: ir.CoreListenerDetails{Name: "envoy-gateway/other-gw/tcp"}, + Routes: []*ir.TCPRoute{sibling}, + }}} + tr.applyTrafficFeatureToRoute(route, &ir.TrafficFeatures{CircuitBreaker: &ir.CircuitBreaker{}}, + nil, policy, target, x, gwNN, nil) + assert.Nil(t, sibling.CircuitBreaker, "route on a sibling Gateway's listener must be skipped") + }) + + t.Run("udp listener of another gateway is skipped", func(t *testing.T) { + route := &UDPRouteContext{UDPRoute: &gwapiv1a2.UDPRoute{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "udproute-1"}, + }} + sibling := &ir.UDPRoute{Destination: &ir.RouteDestination{Name: irRoutePrefix(route) + "rule/0"}} + x := &ir.Xds{UDP: []*ir.UDPListener{{ + CoreListenerDetails: ir.CoreListenerDetails{Name: "envoy-gateway/other-gw/udp"}, + Route: sibling, + }}} + tr.applyTrafficFeatureToRoute(route, &ir.TrafficFeatures{LoadBalancer: &ir.LoadBalancer{}}, + nil, policy, target, x, gwNN, nil) + assert.Nil(t, sibling.LoadBalancer, "route on a sibling Gateway's listener must be skipped") + }) +} From 0e3e12dfa0ed97ae8ebd52067846f3ed326b4490 Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Thu, 9 Jul 2026 12:38:56 +0200 Subject: [PATCH 4/6] refactor: nest policy defaults under policyDefaults with per-policy MergeSettings Signed-off-by: Maksim Kuchkovskiy --- api/v1alpha1/envoyproxy_types.go | 29 +++++++--- .../validation/envoygateway_validate.go | 9 +-- .../validation/envoygateway_validate_test.go | 16 +++-- api/v1alpha1/zz_generated.deepcopy.go | 58 +++++++++++++++---- .../gateway.envoyproxy.io_envoyproxies.yaml | 41 +++++++------ .../gateway.envoyproxy.io_envoyproxies.yaml | 41 +++++++------ internal/gatewayapi/backendtrafficpolicy.go | 13 +++-- .../backendtrafficpolicy_mergedefault_test.go | 40 +++++++------ ...default-mergetype-control-plane-ns.in.yaml | 7 ++- ...efault-mergetype-control-plane-ns.out.yaml | 5 +- ...cy-default-mergetype-exclude-label.in.yaml | 11 ++-- ...y-default-mergetype-exclude-label.out.yaml | 7 ++- ...-default-mergetype-merged-gateways.in.yaml | 5 +- ...default-mergetype-merged-gateways.out.yaml | 5 +- ...icy-default-mergetype-multi-parent.in.yaml | 5 +- ...cy-default-mergetype-multi-parent.out.yaml | 10 ++-- ...endtrafficpolicy-default-mergetype.in.yaml | 7 ++- ...ndtrafficpolicy-default-mergetype.out.yaml | 5 +- site/content/en/latest/api/extension_types.md | 41 +++++++++++-- .../backend-traffic-policy.md | 17 +++--- test/cel-validation/envoyproxy_test.go | 26 +++++---- test/helm/gateway-crds-helm/all.out.yaml | 41 +++++++------ test/helm/gateway-crds-helm/e2e.out.yaml | 41 +++++++------ .../envoy-gateway-crds.out.yaml | 41 +++++++------ 24 files changed, 326 insertions(+), 195 deletions(-) diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 661a167d3f..90b2456ff5 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -217,24 +217,35 @@ type EnvoyProxySpec struct { // +optional MergeType *MergeType `json:"mergeType,omitempty"` - // BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - // attached to Gateways that use this EnvoyProxy. + // PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + // Gateways that use this EnvoyProxy. // +optional - BackendTrafficPolicy *PolicyDefaults `json:"backendTrafficPolicy,omitempty"` + PolicyDefaults *PolicyDefaults `json:"policyDefaults,omitempty"` } -// PolicyDefaults defines default settings shared by Envoy Gateway xPolicies (e.g. BackendTrafficPolicy) -// attached to Gateways that use this EnvoyProxy. +// PolicyDefaults defines defaults applied to Envoy Gateway policies, keyed by policy kind. type PolicyDefaults struct { - // DefaultMergeType is the mergeType used for a policy that does not set one, + // BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources. + // +optional + BackendTrafficPolicy *BackendTrafficPolicyDefaults `json:"backendTrafficPolicy,omitempty"` +} + +// BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. +type BackendTrafficPolicyDefaults struct { + MergeSettings `json:",inline"` +} + +// MergeSettings defines how an Envoy Gateway policy that does not set a mergeType is merged by default. +type MergeSettings struct { + // MergeType is the mergeType applied to a policy that does not set one, // so a route-level policy merges into its parent instead of replacing it. // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge // +optional - DefaultMergeType *MergeType `json:"defaultMergeType,omitempty"` + MergeType *MergeType `json:"mergeType,omitempty"` - // ExcludeLabel, when present on a policy, opts that policy out of DefaultMergeType. + // MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. // +optional - ExcludeLabel *string `json:"excludeLabel,omitempty"` + MergeExcludeLabel *string `json:"mergeExcludeLabel,omitempty"` } // EnvoyProxyGeoIP defines shared GeoIP provider settings for EnvoyProxy. diff --git a/api/v1alpha1/validation/envoygateway_validate.go b/api/v1alpha1/validation/envoygateway_validate.go index 7d93fb4d9a..40db7aef9c 100644 --- a/api/v1alpha1/validation/envoygateway_validate.go +++ b/api/v1alpha1/validation/envoygateway_validate.go @@ -82,15 +82,16 @@ func ValidateEnvoyGateway(eg *egv1a1.EnvoyGateway) error { // enforced by CRD validation for EnvoyProxy resources but not when the spec is provided inline as // the EnvoyGateway default, since that path does not go through CRD admission. func validateEnvoyGatewayDefaultEnvoyProxy(spec *egv1a1.EnvoyProxySpec) error { - if spec == nil || spec.BackendTrafficPolicy == nil || spec.BackendTrafficPolicy.DefaultMergeType == nil { + if spec == nil || spec.PolicyDefaults == nil || spec.PolicyDefaults.BackendTrafficPolicy == nil || + spec.PolicyDefaults.BackendTrafficPolicy.MergeType == nil { return nil } - switch *spec.BackendTrafficPolicy.DefaultMergeType { + switch *spec.PolicyDefaults.BackendTrafficPolicy.MergeType { case egv1a1.StrategicMerge, egv1a1.JSONMerge: return nil default: - return fmt.Errorf("envoyProxy.backendTrafficPolicy.defaultMergeType must be one of StrategicMerge or JSONMerge, got %q", - *spec.BackendTrafficPolicy.DefaultMergeType) + return fmt.Errorf("envoyProxy.policyDefaults.backendTrafficPolicy.mergeType must be one of StrategicMerge or JSONMerge, got %q", + *spec.PolicyDefaults.BackendTrafficPolicy.MergeType) } } diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index 8f2f254c14..9df74f6e00 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1055,14 +1055,16 @@ func TestValidateEnvoyGateway(t *testing.T) { expect: true, }, { - name: "default EnvoyProxy with valid backendTrafficPolicy.defaultMergeType", + name: "default EnvoyProxy with valid backendTrafficPolicy.mergeType", eg: &egv1a1.EnvoyGateway{ EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ Gateway: egv1a1.DefaultGateway(), Provider: egv1a1.DefaultEnvoyGatewayProvider(), EnvoyProxy: &egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.StrategicMerge), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.StrategicMerge), + }}, }, }, }, @@ -1070,14 +1072,16 @@ func TestValidateEnvoyGateway(t *testing.T) { expect: true, }, { - name: "default EnvoyProxy with invalid backendTrafficPolicy.defaultMergeType", + name: "default EnvoyProxy with invalid backendTrafficPolicy.mergeType", eg: &egv1a1.EnvoyGateway{ EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ Gateway: egv1a1.DefaultGateway(), Provider: egv1a1.DefaultEnvoyGatewayProvider(), EnvoyProxy: &egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.Replace), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.Replace), + }}, }, }, }, diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d6fc7202b9..eea829c850 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -770,6 +770,22 @@ func (in *BackendTrafficPolicy) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendTrafficPolicyDefaults) DeepCopyInto(out *BackendTrafficPolicyDefaults) { + *out = *in + in.MergeSettings.DeepCopyInto(&out.MergeSettings) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicyDefaults. +func (in *BackendTrafficPolicyDefaults) DeepCopy() *BackendTrafficPolicyDefaults { + if in == nil { + return nil + } + out := new(BackendTrafficPolicyDefaults) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendTrafficPolicyList) DeepCopyInto(out *BackendTrafficPolicyList) { *out = *in @@ -3281,8 +3297,8 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { *out = new(MergeType) **out = **in } - if in.BackendTrafficPolicy != nil { - in, out := &in.BackendTrafficPolicy, &out.BackendTrafficPolicy + if in.PolicyDefaults != nil { + in, out := &in.PolicyDefaults, &out.PolicyDefaults *out = new(PolicyDefaults) (*in).DeepCopyInto(*out) } @@ -5946,6 +5962,31 @@ func (in *Lua) DeepCopy() *Lua { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeSettings) DeepCopyInto(out *MergeSettings) { + *out = *in + if in.MergeType != nil { + in, out := &in.MergeType, &out.MergeType + *out = new(MergeType) + **out = **in + } + if in.MergeExcludeLabel != nil { + in, out := &in.MergeExcludeLabel, &out.MergeExcludeLabel + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeSettings. +func (in *MergeSettings) DeepCopy() *MergeSettings { + if in == nil { + return nil + } + out := new(MergeSettings) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MethodMatch) DeepCopyInto(out *MethodMatch) { *out = *in @@ -6493,15 +6534,10 @@ func (in *PerRetryPolicy) DeepCopy() *PerRetryPolicy { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyDefaults) DeepCopyInto(out *PolicyDefaults) { *out = *in - if in.DefaultMergeType != nil { - in, out := &in.DefaultMergeType, &out.DefaultMergeType - *out = new(MergeType) - **out = **in - } - if in.ExcludeLabel != nil { - in, out := &in.ExcludeLabel, &out.ExcludeLabel - *out = new(string) - **out = **in + if in.BackendTrafficPolicy != nil { + in, out := &in.BackendTrafficPolicy, &out.BackendTrafficPolicy + *out = new(BackendTrafficPolicyDefaults) + (*in).DeepCopyInto(*out) } } diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index 593353da8f..adb551e8ea 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -236,24 +236,6 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' - backendTrafficPolicy: - description: |- - BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - attached to Gateways that use this EnvoyProxy. - properties: - defaultMergeType: - description: |- - DefaultMergeType is the mergeType used for a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - excludeLabel: - description: ExcludeLabel, when present on a policy, opts that - policy out of DefaultMergeType. - type: string - type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. @@ -812,6 +794,29 @@ spec: - StrategicMerge - JSONMerge type: string + policyDefaults: + description: |- + PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + Gateways that use this EnvoyProxy. + properties: + backendTrafficPolicy: + description: BackendTrafficPolicy defines defaults applied to + BackendTrafficPolicy resources. + properties: + mergeExcludeLabel: + description: MergeExcludeLabel, when present on a policy, + opts that policy out of the default MergeType. + type: string + mergeType: + description: |- + MergeType is the mergeType applied to a policy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + type: object + type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index f4f378a7f9..6fbfd1d6d0 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -235,24 +235,6 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' - backendTrafficPolicy: - description: |- - BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - attached to Gateways that use this EnvoyProxy. - properties: - defaultMergeType: - description: |- - DefaultMergeType is the mergeType used for a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - excludeLabel: - description: ExcludeLabel, when present on a policy, opts that - policy out of DefaultMergeType. - type: string - type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. @@ -811,6 +793,29 @@ spec: - StrategicMerge - JSONMerge type: string + policyDefaults: + description: |- + PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + Gateways that use this EnvoyProxy. + properties: + backendTrafficPolicy: + description: BackendTrafficPolicy defines defaults applied to + BackendTrafficPolicy resources. + properties: + mergeExcludeLabel: + description: MergeExcludeLabel, when present on a policy, + opts that policy out of the default MergeType. + type: string + mergeType: + description: |- + MergeType is the mergeType applied to a policy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + type: object + type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index a04dca153f..5c495c5cc3 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -1080,26 +1080,27 @@ func (t *Translator) effectiveMergeType(policy *egv1a1.BackendTrafficPolicy, ep if policy.Spec.MergeType != nil { return policy.Spec.MergeType } - if ep == nil || ep.Spec.BackendTrafficPolicy == nil || ep.Spec.BackendTrafficPolicy.DefaultMergeType == nil { + if ep == nil || ep.Spec.PolicyDefaults == nil || ep.Spec.PolicyDefaults.BackendTrafficPolicy == nil || + ep.Spec.PolicyDefaults.BackendTrafficPolicy.MergeType == nil { return nil } if policy.Namespace == t.ControllerNamespace { return nil } - d := ep.Spec.BackendTrafficPolicy - if label := ptr.Deref(d.ExcludeLabel, ""); label != "" { + d := ep.Spec.PolicyDefaults.BackendTrafficPolicy + if label := ptr.Deref(d.MergeExcludeLabel, ""); label != "" { if _, ok := policy.Labels[label]; ok { return nil } } - // Defense in depth: the CRD enum restricts DefaultMergeType to StrategicMerge/JSONMerge, but the + // Defense in depth: the CRD enum restricts MergeType to StrategicMerge/JSONMerge, but the // EnvoyGateway default EnvoyProxySpec is not subject to CRD validation. Ignore anything that is // not a real merge so a stray value (e.g. Replace) can never produce a "merged" status while // actually replacing the parent. - if *d.DefaultMergeType != egv1a1.StrategicMerge && *d.DefaultMergeType != egv1a1.JSONMerge { + if *d.MergeType != egv1a1.StrategicMerge && *d.MergeType != egv1a1.JSONMerge { return nil } - return d.DefaultMergeType + return d.MergeType } // anyGatewayMergeDefault reports whether any of the route's parent gateways supplies a default diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go index 0fb9f7cd33..eb4ea38b8f 100644 --- a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" @@ -22,11 +22,11 @@ func TestEffectiveMergeType(t *testing.T) { jsonMerge := egv1a1.JSONMerge ep := func(mt *egv1a1.MergeType, label string) *egv1a1.EnvoyProxy { - d := &egv1a1.PolicyDefaults{DefaultMergeType: mt} + d := &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{MergeType: mt}} if label != "" { - d.ExcludeLabel = new(label) + d.MergeExcludeLabel = new(label) } - return &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{BackendTrafficPolicy: d}} + return &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{PolicyDefaults: &egv1a1.PolicyDefaults{BackendTrafficPolicy: d}}} } btp := func(ns string, labels map[string]string, mt *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { return &egv1a1.BackendTrafficPolicy{ @@ -79,8 +79,8 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { want *egv1a1.MergeType }{ { - // ep != nil but ep.Spec.BackendTrafficPolicy == nil -> nil. - name: "envoyproxy without backendTrafficPolicy stays nil", + // ep != nil but ep.Spec.PolicyDefaults == nil -> nil. + name: "envoyproxy without policyDefaults stays nil", pol: &egv1a1.BackendTrafficPolicy{ ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, }, @@ -94,9 +94,11 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: "app", Labels: map[string]string{"other": "x"}}, }, ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: &strategic, - ExcludeLabel: new("skip"), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: &strategic, + MergeExcludeLabel: new("skip"), + }}, }, }}, want: &strategic, @@ -108,9 +110,11 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, }, ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: &strategic, - ExcludeLabel: new("skip"), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: &strategic, + MergeExcludeLabel: new("skip"), + }}, }, }}, want: &strategic, @@ -124,8 +128,10 @@ func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, }, ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.Replace), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.Replace), + }}, }, }}, want: nil, @@ -151,7 +157,7 @@ func TestAnyGatewayMergeDefault(t *testing.T) { strategic := egv1a1.StrategicMerge epWithDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{DefaultMergeType: &strategic}, + PolicyDefaults: &egv1a1.PolicyDefaults{BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{MergeType: &strategic}}}, }} epNoDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}} @@ -213,7 +219,7 @@ func TestApplyTrafficFeatureToRoute_MergeGatewayScoping(t *testing.T) { target := policyTargetReferenceWithSectionName{} t.Run("tcp listener of another gateway is skipped", func(t *testing.T) { - route := &TCPRouteContext{TCPRoute: &gwapiv1a2.TCPRoute{ + route := &TCPRouteContext{TCPRoute: &gwapiv1.TCPRoute{ ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "tcproute-1"}, }} sibling := &ir.TCPRoute{Destination: &ir.RouteDestination{Name: irRoutePrefix(route) + "rule/0"}} @@ -227,7 +233,7 @@ func TestApplyTrafficFeatureToRoute_MergeGatewayScoping(t *testing.T) { }) t.Run("udp listener of another gateway is skipped", func(t *testing.T) { - route := &UDPRouteContext{UDPRoute: &gwapiv1a2.UDPRoute{ + route := &UDPRouteContext{UDPRoute: &gwapiv1.UDPRoute{ ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "udproute-1"}, }} sibling := &ir.UDPRoute{Destination: &ir.RouteDestination{Name: irRoutePrefix(route) + "rule/0"}} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml index 36fa213df8..3079913437 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml @@ -10,8 +10,9 @@ envoyProxyForGatewayClass: namespace: envoy-gateway-system name: test spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -67,7 +68,7 @@ backendTrafficPolicies: httpUpgrade: - type: websocket # Route-level policy lives in the control-plane (controller) namespace, so the EnvoyProxy - # defaultMergeType does not apply and the policy replaces the gateway-level policy. + # mergeType does not apply and the policy replaces the gateway-level policy. - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml index 0babf0f8b0..c3eeca1a11 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml @@ -166,9 +166,10 @@ infraIR: name: test namespace: envoy-gateway-system spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge logging: {} + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge status: {} listeners: - name: envoy-gateway/gateway-1/http diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml index a87572c121..f40f78b92f 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml @@ -5,9 +5,10 @@ envoyProxyForGatewayClass: namespace: envoy-gateway-system name: test spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge - excludeLabel: gateway.envoyproxy.io/skip-merge-default + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge + mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -62,8 +63,8 @@ backendTrafficPolicies: maxConnectionDuration: 17s httpUpgrade: - type: websocket - # Route-level policy carries the EnvoyProxy excludeLabel, so it opts out of the - # defaultMergeType and replaces (rather than merges into) the gateway-level policy. + # Route-level policy carries the EnvoyProxy mergeExcludeLabel, so it opts out of the + # mergeType and replaces (rather than merges into) the gateway-level policy. - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml index 62ea6ec8fc..d0591a5aa9 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml @@ -167,10 +167,11 @@ infraIR: name: test namespace: envoy-gateway-system spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge - excludeLabel: gateway.envoyproxy.io/skip-merge-default logging: {} + policyDefaults: + backendTrafficPolicy: + mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default + mergeType: StrategicMerge status: {} listeners: - name: envoy-gateway/gateway-1/http diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml index b81bd6472c..8304735311 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml @@ -6,8 +6,9 @@ envoyProxyForGatewayClass: namespace: envoy-gateway-system spec: mergeGateways: true - backendTrafficPolicy: - defaultMergeType: StrategicMerge + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml index 4c1c3350cb..18a9188883 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml @@ -279,10 +279,11 @@ infraIR: name: test namespace: envoy-gateway-system spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge logging: {} mergeGateways: true + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge status: {} listeners: - name: envoy-gateway/gateway-1/http diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml index e4c3015719..58527f09b1 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml @@ -6,8 +6,9 @@ envoyProxiesForGateways: namespace: envoy-gateway name: ep-merge-default spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml index 1f7b919a17..abd95856dc 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml @@ -147,9 +147,10 @@ envoyProxiesForGateways: name: ep-merge-default namespace: envoy-gateway spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge logging: {} + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge status: ancestors: - ancestorRef: @@ -314,9 +315,10 @@ infraIR: name: ep-merge-default namespace: envoy-gateway spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge logging: {} + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge status: ancestors: - ancestorRef: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml index f4dd15ad2c..cb232a60aa 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml @@ -5,8 +5,9 @@ envoyProxyForGatewayClass: namespace: envoy-gateway-system name: test spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -61,7 +62,7 @@ backendTrafficPolicies: maxConnectionDuration: 17s httpUpgrade: - type: websocket - # Route-level policy with no mergeType: the EnvoyProxy defaultMergeType makes it merge + # Route-level policy with no mergeType: the EnvoyProxy mergeType makes it merge # into the gateway-level policy instead of replacing it. - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml index 5379470bb5..cb6f9cb806 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml @@ -170,9 +170,10 @@ infraIR: name: test namespace: envoy-gateway-system spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge logging: {} + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge status: {} listeners: - name: envoy-gateway/gateway-1/http diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index faa295a66e..b7613b9a65 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -559,6 +559,21 @@ between the Envoy Proxy listener and the backend service. | `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#policystatus)_ | true | | status defines the current status of BackendTrafficPolicy. | +#### BackendTrafficPolicyDefaults + + + +BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. + +_Appears in:_ +- [PolicyDefaults](#policydefaults) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType is the mergeType applied to a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | +| `mergeExcludeLabel` | _string_ | false | | MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. | + + #### BackendTrafficPolicySpec @@ -2241,7 +2256,7 @@ _Appears in:_ | `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). | | `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. | -| `backendTrafficPolicy` | _[PolicyDefaults](#policydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources
attached to Gateways that use this EnvoyProxy. | +| `policyDefaults` | _[PolicyDefaults](#policydefaults)_ | false | | PolicyDefaults defines defaults applied to Envoy Gateway policies attached to
Gateways that use this EnvoyProxy. | #### EnvoyProxyStatus @@ -4156,6 +4171,21 @@ _Appears in:_ | `ValueRef` | LuaValueTypeValueRef defines the "ValueRef" Lua type.
| +#### MergeSettings + + + +MergeSettings defines how an Envoy Gateway policy that does not set a mergeType is merged by default. + +_Appears in:_ +- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType is the mergeType applied to a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | +| `mergeExcludeLabel` | _string_ | false | | MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. | + + #### MergeType _Underlying type:_ _string_ @@ -4163,10 +4193,11 @@ _Underlying type:_ _string_ MergeType defines the type of merge operation _Appears in:_ +- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) - [EnvoyProxySpec](#envoyproxyspec) - [KubernetesPatchSpec](#kubernetespatchspec) -- [PolicyDefaults](#policydefaults) +- [MergeSettings](#mergesettings) - [SecurityPolicySpec](#securitypolicyspec) | Value | Description | @@ -4572,16 +4603,14 @@ _Appears in:_ -PolicyDefaults defines default settings shared by Envoy Gateway xPolicies (e.g. BackendTrafficPolicy) -attached to Gateways that use this EnvoyProxy. +PolicyDefaults defines defaults applied to Envoy Gateway policies, keyed by policy kind. _Appears in:_ - [EnvoyProxySpec](#envoyproxyspec) | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `defaultMergeType` | _[MergeType](#mergetype)_ | false | | DefaultMergeType is the mergeType used for a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | -| `excludeLabel` | _string_ | false | | ExcludeLabel, when present on a policy, opts that policy out of DefaultMergeType. | +| `backendTrafficPolicy` | _[BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources. | #### PolicyTargetReferences diff --git a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md index 0f51907478..bf9520716e 100644 --- a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md +++ b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md @@ -216,7 +216,7 @@ In this example, the route-level policy merges with the gateway-level policy, re ### Defaulting mergeType via EnvoyProxy -Setting `mergeType` on every route-level policy can be repetitive when an organization wants merging to be the norm. To make merging the default, configure `defaultMergeType` on the [EnvoyProxy](../../api/extension_types#envoyproxy) attached to the GatewayClass or Gateway: +Setting `mergeType` on every route-level policy can be repetitive when an organization wants merging to be the norm. To make merging the default, configure it under `policyDefaults.backendTrafficPolicy` on the [EnvoyProxy](../../api/extension_types#envoyproxy) attached to the GatewayClass or Gateway: ```yaml apiVersion: gateway.envoyproxy.io/v1alpha1 @@ -225,11 +225,12 @@ metadata: name: custom-proxy-config namespace: envoy-gateway-system spec: - backendTrafficPolicy: - defaultMergeType: StrategicMerge - # Optional: a policy carrying this label key opts out of the default and replaces - # its parent instead of merging. The label's value is ignored. - excludeLabel: gateway.envoyproxy.io/skip-merge-default + policyDefaults: + backendTrafficPolicy: + mergeType: StrategicMerge + # Optional: a policy carrying this label key opts out of the default and replaces + # its parent instead of merging. The label's value is ignored. + mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default ``` With this configuration, a route-level BackendTrafficPolicy that does **not** set `mergeType` merges into its parent (gateway-level or listener-level) policy as if it had set `mergeType: StrategicMerge`. The default follows the same precedence as other EnvoyProxy settings, so it can be set once at the GatewayClass level or overridden per Gateway. @@ -237,8 +238,8 @@ With this configuration, a route-level BackendTrafficPolicy that does **not** se The defaulting behavior follows these rules: - A policy's own `mergeType` always wins; the default only applies when `mergeType` is unset. -- `defaultMergeType` accepts only `StrategicMerge` or `JSONMerge`. `Replace` is not allowed because defaulting to a replace would have no effect. -- A policy that carries the configured `excludeLabel` key opts out of the default and replaces its parent, even when `mergeType` is unset. +- `mergeType` accepts only `StrategicMerge` or `JSONMerge`. `Replace` is not allowed because defaulting to a replace would have no effect. +- A policy that carries the configured `mergeExcludeLabel` key opts out of the default and replaces its parent, even when `mergeType` is unset. - Policies in the Envoy Gateway control-plane namespace are not defaulted. ### Key Constraints diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 44c042f5bf..052e799c85 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2486,34 +2486,40 @@ func TestEnvoyProxyProvider(t *testing.T) { wantErrors: []string{"If type is Remote, local field must not be set"}, }, { - desc: "backendTrafficPolicy defaultMergeType StrategicMerge is valid", + desc: "policyDefaults backendTrafficPolicy mergeType StrategicMerge is valid", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.StrategicMerge), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.StrategicMerge), + }}, }, } }, wantErrors: []string{}, }, { - desc: "backendTrafficPolicy defaultMergeType JSONMerge with excludeLabel is valid", + desc: "policyDefaults backendTrafficPolicy mergeType JSONMerge with mergeExcludeLabel is valid", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.JSONMerge), - ExcludeLabel: new("gateway.envoyproxy.io/skip-merge-default"), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.JSONMerge), + MergeExcludeLabel: new("gateway.envoyproxy.io/skip-merge-default"), + }}, }, } }, wantErrors: []string{}, }, { - desc: "backendTrafficPolicy defaultMergeType Replace is rejected", + desc: "policyDefaults backendTrafficPolicy mergeType Replace is rejected", mutate: func(envoy *egv1a1.EnvoyProxy) { envoy.Spec = egv1a1.EnvoyProxySpec{ - BackendTrafficPolicy: &egv1a1.PolicyDefaults{ - DefaultMergeType: new(egv1a1.MergeType("Replace")), + PolicyDefaults: &egv1a1.PolicyDefaults{ + BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ + MergeType: new(egv1a1.MergeType("Replace")), + }}, }, } }, diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 17aad73ce7..34172d5957 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -33895,24 +33895,6 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' - backendTrafficPolicy: - description: |- - BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - attached to Gateways that use this EnvoyProxy. - properties: - defaultMergeType: - description: |- - DefaultMergeType is the mergeType used for a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - excludeLabel: - description: ExcludeLabel, when present on a policy, opts that - policy out of DefaultMergeType. - type: string - type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. @@ -34471,6 +34453,29 @@ spec: - StrategicMerge - JSONMerge type: string + policyDefaults: + description: |- + PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + Gateways that use this EnvoyProxy. + properties: + backendTrafficPolicy: + description: BackendTrafficPolicy defines defaults applied to + BackendTrafficPolicy resources. + properties: + mergeExcludeLabel: + description: MergeExcludeLabel, when present on a policy, + opts that policy out of the default MergeType. + type: string + mergeType: + description: |- + MergeType is the mergeType applied to a policy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + type: object + type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 30de462c6d..db6fed1e24 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -9833,24 +9833,6 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' - backendTrafficPolicy: - description: |- - BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - attached to Gateways that use this EnvoyProxy. - properties: - defaultMergeType: - description: |- - DefaultMergeType is the mergeType used for a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - excludeLabel: - description: ExcludeLabel, when present on a policy, opts that - policy out of DefaultMergeType. - type: string - type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. @@ -10409,6 +10391,29 @@ spec: - StrategicMerge - JSONMerge type: string + policyDefaults: + description: |- + PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + Gateways that use this EnvoyProxy. + properties: + backendTrafficPolicy: + description: BackendTrafficPolicy defines defaults applied to + BackendTrafficPolicy resources. + properties: + mergeExcludeLabel: + description: MergeExcludeLabel, when present on a policy, + opts that policy out of the default MergeType. + type: string + mergeType: + description: |- + MergeType is the mergeType applied to a policy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + type: object + type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index e01cdf4b75..f8c0732a51 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -9833,24 +9833,6 @@ spec: <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : !has(self.minVersion) && has(self.maxVersion) ? 3 <= {"1.0":1,"1.1":2,"1.2":3,"1.3":4,"Auto":5}[self.maxVersion] : true' - backendTrafficPolicy: - description: |- - BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources - attached to Gateways that use this EnvoyProxy. - properties: - defaultMergeType: - description: |- - DefaultMergeType is the mergeType used for a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - excludeLabel: - description: ExcludeLabel, when present on a policy, opts that - policy out of DefaultMergeType. - type: string - type: object bootstrap: description: |- Bootstrap defines the Envoy Bootstrap as a YAML string. @@ -10409,6 +10391,29 @@ spec: - StrategicMerge - JSONMerge type: string + policyDefaults: + description: |- + PolicyDefaults defines defaults applied to Envoy Gateway policies attached to + Gateways that use this EnvoyProxy. + properties: + backendTrafficPolicy: + description: BackendTrafficPolicy defines defaults applied to + BackendTrafficPolicy resources. + properties: + mergeExcludeLabel: + description: MergeExcludeLabel, when present on a policy, + opts that policy out of the default MergeType. + type: string + mergeType: + description: |- + MergeType is the mergeType applied to a policy that does not set one, + so a route-level policy merges into its parent instead of replacing it. + enum: + - StrategicMerge + - JSONMerge + type: string + type: object + type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API From ebea924d09a543a40e51216a9cfe2121a9b802d9 Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Fri, 17 Jul 2026 09:10:33 +0200 Subject: [PATCH 5/6] refactor: move merge default to parent policy defaultChildMergeType Replace the EnvoyProxy policyDefaults API with a defaultChildMergeType field on Gateway-targeting BackendTrafficPolicies, following GEP-713. A child policy without its own mergeType merges into the closest parent policy using its defaultChildMergeType; an explicit mergeType (including Replace, allowed by a separate change) always wins. Signed-off-by: Maksim Kuchkovskiy --- api/v1alpha1/backendtrafficpolicy_types.go | 11 + api/v1alpha1/envoyproxy_types.go | 30 -- .../validation/envoygateway_validate.go | 21 -- .../validation/envoygateway_validate_test.go | 34 --- api/v1alpha1/zz_generated.deepcopy.go | 71 +---- ....envoyproxy.io_backendtrafficpolicies.yaml | 16 ++ .../gateway.envoyproxy.io_envoyproxies.yaml | 23 -- ....envoyproxy.io_backendtrafficpolicies.yaml | 16 ++ .../gateway.envoyproxy.io_envoyproxies.yaml | 23 -- internal/gatewayapi/backendtrafficpolicy.go | 79 +++--- .../backendtrafficpolicy_mergedefault_test.go | 204 +++++--------- ...default-mergetype-control-plane-ns.in.yaml | 86 ------ ...efault-mergetype-control-plane-ns.out.yaml | 259 ------------------ ...-default-mergetype-merged-gateways.in.yaml | 5 +- ...default-mergetype-merged-gateways.out.yaml | 5 +- ...icy-default-mergetype-multi-parent.in.yaml | 23 +- ...cy-default-mergetype-multi-parent.out.yaml | 54 +--- ...default-mergetype-replace-opt-out.in.yaml} | 19 +- ...efault-mergetype-replace-opt-out.out.yaml} | 17 +- ...endtrafficpolicy-default-mergetype.in.yaml | 15 +- ...ndtrafficpolicy-default-mergetype.out.yaml | 13 +- ...ndtrafficpolicy-default-child-mergetype.md | 1 + ...fficpolicy-default-mergetype-envoyproxy.md | 1 - site/content/en/latest/api/extension_types.md | 48 +--- .../backend-traffic-policy.md | 39 +-- .../backendtrafficpolicy_test.go | 54 ++++ test/cel-validation/envoyproxy_test.go | 40 --- test/helm/gateway-crds-helm/all.out.yaml | 39 ++- test/helm/gateway-crds-helm/e2e.out.yaml | 39 ++- .../envoy-gateway-crds.out.yaml | 39 ++- 30 files changed, 293 insertions(+), 1031 deletions(-) delete mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml delete mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml rename internal/gatewayapi/testdata/{backendtrafficpolicy-default-mergetype-exclude-label.in.yaml => backendtrafficpolicy-default-mergetype-replace-opt-out.in.yaml} (75%) rename internal/gatewayapi/testdata/{backendtrafficpolicy-default-mergetype-exclude-label.out.yaml => backendtrafficpolicy-default-mergetype-replace-opt-out.out.yaml} (93%) create mode 100644 release-notes/current/new_features/9334-backendtrafficpolicy-default-child-mergetype.md delete mode 100644 release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index 9468331583..d74f76bf83 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -46,6 +46,7 @@ type BackendTrafficPolicy struct { // +kubebuilder:validation:XValidation:rule="!has(self.compression) || !has(self.compressor)", message="either compression or compressor can be set, not both" // +kubebuilder:validation:XValidation:rule="!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0", message="requestBuffer cannot be used together with httpUpgrade" // +kubebuilder:validation:XValidation:rule="!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute']) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])))", message="admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets" +// +kubebuilder:validation:XValidation:rule="!has(self.defaultChildMergeType) || ((!has(self.targetRef) || self.targetRef.kind == 'Gateway') && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == 'Gateway')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind == 'Gateway')))", message="defaultChildMergeType can only be used with Gateway targets" type BackendTrafficPolicySpec struct { PolicyTargetReferences `json:",inline"` ClusterSettings `json:",inline"` @@ -60,6 +61,16 @@ type BackendTrafficPolicySpec struct { // +optional MergeType *MergeType `json:"mergeType,omitempty"` + // DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + // an xRoute under this policy's target) that do not set their own mergeType, so a child + // policy merges into this policy instead of replacing it. A child policy can opt out by + // setting mergeType to Replace. + // This field can only be set on policies targeting a parent resource (Gateway). + // + // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge + // +optional + DefaultChildMergeType *MergeType `json:"defaultChildMergeType,omitempty"` + // RateLimit allows the user to limit the number of incoming requests // to a predefined value based on attributes within the traffic flow. // +optional diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go index 90b2456ff5..a4ef2ee57a 100644 --- a/api/v1alpha1/envoyproxy_types.go +++ b/api/v1alpha1/envoyproxy_types.go @@ -216,36 +216,6 @@ type EnvoyProxySpec struct { // +kubebuilder:validation:Enum=Replace;StrategicMerge;JSONMerge // +optional MergeType *MergeType `json:"mergeType,omitempty"` - - // PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - // Gateways that use this EnvoyProxy. - // +optional - PolicyDefaults *PolicyDefaults `json:"policyDefaults,omitempty"` -} - -// PolicyDefaults defines defaults applied to Envoy Gateway policies, keyed by policy kind. -type PolicyDefaults struct { - // BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources. - // +optional - BackendTrafficPolicy *BackendTrafficPolicyDefaults `json:"backendTrafficPolicy,omitempty"` -} - -// BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. -type BackendTrafficPolicyDefaults struct { - MergeSettings `json:",inline"` -} - -// MergeSettings defines how an Envoy Gateway policy that does not set a mergeType is merged by default. -type MergeSettings struct { - // MergeType is the mergeType applied to a policy that does not set one, - // so a route-level policy merges into its parent instead of replacing it. - // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge - // +optional - MergeType *MergeType `json:"mergeType,omitempty"` - - // MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. - // +optional - MergeExcludeLabel *string `json:"mergeExcludeLabel,omitempty"` } // EnvoyProxyGeoIP defines shared GeoIP provider settings for EnvoyProxy. diff --git a/api/v1alpha1/validation/envoygateway_validate.go b/api/v1alpha1/validation/envoygateway_validate.go index 40db7aef9c..79ec2a85ce 100644 --- a/api/v1alpha1/validation/envoygateway_validate.go +++ b/api/v1alpha1/validation/envoygateway_validate.go @@ -71,30 +71,9 @@ func ValidateEnvoyGateway(eg *egv1a1.EnvoyGateway) error { return fmt.Errorf("disableLua and enableLua must not have the same value") } - if err := validateEnvoyGatewayDefaultEnvoyProxy(eg.EnvoyProxy); err != nil { - return err - } - return nil } -// validateEnvoyGatewayDefaultEnvoyProxy validates fields of the default EnvoyProxySpec that are -// enforced by CRD validation for EnvoyProxy resources but not when the spec is provided inline as -// the EnvoyGateway default, since that path does not go through CRD admission. -func validateEnvoyGatewayDefaultEnvoyProxy(spec *egv1a1.EnvoyProxySpec) error { - if spec == nil || spec.PolicyDefaults == nil || spec.PolicyDefaults.BackendTrafficPolicy == nil || - spec.PolicyDefaults.BackendTrafficPolicy.MergeType == nil { - return nil - } - switch *spec.PolicyDefaults.BackendTrafficPolicy.MergeType { - case egv1a1.StrategicMerge, egv1a1.JSONMerge: - return nil - default: - return fmt.Errorf("envoyProxy.policyDefaults.backendTrafficPolicy.mergeType must be one of StrategicMerge or JSONMerge, got %q", - *spec.PolicyDefaults.BackendTrafficPolicy.MergeType) - } -} - // WarnEnvoyGateway returns deprecation warnings for the provided EnvoyGateway configuration. func WarnEnvoyGateway(eg *egv1a1.EnvoyGateway) []string { if eg == nil || eg.ExtensionAPIs == nil { diff --git a/api/v1alpha1/validation/envoygateway_validate_test.go b/api/v1alpha1/validation/envoygateway_validate_test.go index 9df74f6e00..de79017ce8 100644 --- a/api/v1alpha1/validation/envoygateway_validate_test.go +++ b/api/v1alpha1/validation/envoygateway_validate_test.go @@ -1054,40 +1054,6 @@ func TestValidateEnvoyGateway(t *testing.T) { }, expect: true, }, - { - name: "default EnvoyProxy with valid backendTrafficPolicy.mergeType", - eg: &egv1a1.EnvoyGateway{ - EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ - Gateway: egv1a1.DefaultGateway(), - Provider: egv1a1.DefaultEnvoyGatewayProvider(), - EnvoyProxy: &egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.StrategicMerge), - }}, - }, - }, - }, - }, - expect: true, - }, - { - name: "default EnvoyProxy with invalid backendTrafficPolicy.mergeType", - eg: &egv1a1.EnvoyGateway{ - EnvoyGatewaySpec: egv1a1.EnvoyGatewaySpec{ - Gateway: egv1a1.DefaultGateway(), - Provider: egv1a1.DefaultEnvoyGatewayProvider(), - EnvoyProxy: &egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.Replace), - }}, - }, - }, - }, - }, - expect: false, - }, } for _, tc := range testCases { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index eea829c850..b86a3bb8e3 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -770,22 +770,6 @@ func (in *BackendTrafficPolicy) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendTrafficPolicyDefaults) DeepCopyInto(out *BackendTrafficPolicyDefaults) { - *out = *in - in.MergeSettings.DeepCopyInto(&out.MergeSettings) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicyDefaults. -func (in *BackendTrafficPolicyDefaults) DeepCopy() *BackendTrafficPolicyDefaults { - if in == nil { - return nil - } - out := new(BackendTrafficPolicyDefaults) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendTrafficPolicyList) DeepCopyInto(out *BackendTrafficPolicyList) { *out = *in @@ -828,6 +812,11 @@ func (in *BackendTrafficPolicySpec) DeepCopyInto(out *BackendTrafficPolicySpec) *out = new(MergeType) **out = **in } + if in.DefaultChildMergeType != nil { + in, out := &in.DefaultChildMergeType, &out.DefaultChildMergeType + *out = new(MergeType) + **out = **in + } if in.RateLimit != nil { in, out := &in.RateLimit, &out.RateLimit *out = new(RateLimitSpec) @@ -3297,11 +3286,6 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) { *out = new(MergeType) **out = **in } - if in.PolicyDefaults != nil { - in, out := &in.PolicyDefaults, &out.PolicyDefaults - *out = new(PolicyDefaults) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyProxySpec. @@ -5962,31 +5946,6 @@ func (in *Lua) DeepCopy() *Lua { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MergeSettings) DeepCopyInto(out *MergeSettings) { - *out = *in - if in.MergeType != nil { - in, out := &in.MergeType, &out.MergeType - *out = new(MergeType) - **out = **in - } - if in.MergeExcludeLabel != nil { - in, out := &in.MergeExcludeLabel, &out.MergeExcludeLabel - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeSettings. -func (in *MergeSettings) DeepCopy() *MergeSettings { - if in == nil { - return nil - } - out := new(MergeSettings) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MethodMatch) DeepCopyInto(out *MethodMatch) { *out = *in @@ -6531,26 +6490,6 @@ func (in *PerRetryPolicy) DeepCopy() *PerRetryPolicy { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PolicyDefaults) DeepCopyInto(out *PolicyDefaults) { - *out = *in - if in.BackendTrafficPolicy != nil { - in, out := &in.BackendTrafficPolicy, &out.BackendTrafficPolicy - *out = new(BackendTrafficPolicyDefaults) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PolicyDefaults. -func (in *PolicyDefaults) DeepCopy() *PolicyDefaults { - if in == nil { - return nil - } - out := new(PolicyDefaults) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyTargetReferences) DeepCopyInto(out *PolicyTargetReferences) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 72e4b69d56..4748426a78 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -513,6 +513,17 @@ spec: Note that when the suffix is not provided, the value is interpreted as bytes. x-kubernetes-int-or-string: true type: object + defaultChildMergeType: + description: |- + DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + an xRoute under this policy's target) that do not set their own mergeType, so a child + policy merges into this policy instead of replacing it. A child policy can opt out by + setting mergeType to Replace. + This field can only be set on policies targeting a parent resource (Gateway). + enum: + - StrategicMerge + - JSONMerge + type: string dns: description: DNS includes dns resolution settings. properties: @@ -3357,6 +3368,11 @@ spec: || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' + - message: defaultChildMergeType can only be used with Gateway targets + rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || + self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, + ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml index adb551e8ea..93db35107b 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -794,29 +794,6 @@ spec: - StrategicMerge - JSONMerge type: string - policyDefaults: - description: |- - PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - Gateways that use this EnvoyProxy. - properties: - backendTrafficPolicy: - description: BackendTrafficPolicy defines defaults applied to - BackendTrafficPolicy resources. - properties: - mergeExcludeLabel: - description: MergeExcludeLabel, when present on a policy, - opts that policy out of the default MergeType. - type: string - mergeType: - description: |- - MergeType is the mergeType applied to a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - type: object - type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index a79ec46e60..a12773620c 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -512,6 +512,17 @@ spec: Note that when the suffix is not provided, the value is interpreted as bytes. x-kubernetes-int-or-string: true type: object + defaultChildMergeType: + description: |- + DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + an xRoute under this policy's target) that do not set their own mergeType, so a child + policy merges into this policy instead of replacing it. A child policy can opt out by + setting mergeType to Replace. + This field can only be set on policies targeting a parent resource (Gateway). + enum: + - StrategicMerge + - JSONMerge + type: string dns: description: DNS includes dns resolution settings. properties: @@ -3356,6 +3367,11 @@ spec: || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' + - message: defaultChildMergeType can only be used with Gateway targets + rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || + self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, + ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 6fbfd1d6d0..eafaa9486a 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -793,29 +793,6 @@ spec: - StrategicMerge - JSONMerge type: string - policyDefaults: - description: |- - PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - Gateways that use this EnvoyProxy. - properties: - backendTrafficPolicy: - description: BackendTrafficPolicy defines defaults applied to - BackendTrafficPolicy resources. - properties: - mergeExcludeLabel: - description: MergeExcludeLabel, when present on a policy, - opts that policy out of the default MergeType. - type: string - mergeType: - description: |- - MergeType is the mergeType applied to a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - type: object - type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index 5c495c5cc3..a26f8acf53 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -496,7 +496,10 @@ func (t *Translator) processBackendTrafficPolicyForRoute( return } - if policy.Spec.MergeType == nil && !t.anyGatewayMergeDefault(policy, parentRefCtxs) { + // A policy that sets mergeType: Replace explicitly opts out of merging, even when a parent + // policy configures defaultChildMergeType. + explicitReplace := policy.Spec.MergeType != nil && *policy.Spec.MergeType == egv1a1.Replace + if explicitReplace || (policy.Spec.MergeType == nil && !anyParentPolicyMergeDefault(parentRefCtxs, gatewayPolicyMap)) { // Set conditions for translation error if it got any if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, nil, nil); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, @@ -512,10 +515,6 @@ func (t *Translator) processBackendTrafficPolicyForRoute( gwNN := utils.NamespacedName(listener.gateway.Gateway) ancestorRef := getAncestorRefForPolicy(gwNN, &listener.Name) - // Resolve the effective mergeType for this gateway: the policy's own value, or - // the default from the gateway's EnvoyProxy unless the policy is excluded. - mergeType := t.effectiveMergeType(policy, listener.gateway.envoyProxy) - // Find Gateway listener level policy listenerMapKey := NamespacedNameWithSection{ NamespacedName: gwNN, @@ -528,7 +527,18 @@ func (t *Translator) processBackendTrafficPolicyForRoute( NamespacedName: gwNN, } gwPolicy := gatewayPolicyMap[gwMapKey] - if mergeType == nil || (gwPolicy == nil && listenerPolicy == nil) { + + // The closest parent policy is the one the child would merge into: the + // listener-level policy if it exists, otherwise the gateway-level policy. + parentPolicy := gwPolicy + if listenerPolicy != nil { + parentPolicy = listenerPolicy + } + + // Resolve the effective mergeType: the policy's own value, or the closest + // parent policy's defaultChildMergeType. + mergeType := effectiveMergeType(policy, parentPolicy) + if mergeType == nil || parentPolicy == nil { // No merge for this gateway: apply the policy standalone. if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, &gwNN, &listener.Name); err != nil { status.SetConditionForPolicyAncestor(&policy.Status, @@ -543,11 +553,6 @@ func (t *Translator) processBackendTrafficPolicyForRoute( continue } - parentPolicy := gwPolicy - if listenerPolicy != nil { - parentPolicy = listenerPolicy - } - // Carry the effective mergeType so a defaulted policy (no explicit mergeType) // merges like an explicit one. policy is a deep copy; nothing is persisted. mergePolicy := policy @@ -1072,44 +1077,42 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, } } -// effectiveMergeType returns the mergeType to use when merging a route-level policy into the -// parent policy on a given gateway: the policy's own value if set, otherwise the default from -// the gateway's EnvoyProxy, unless the policy is in the control-plane namespace or carries the -// exclude label. -func (t *Translator) effectiveMergeType(policy *egv1a1.BackendTrafficPolicy, ep *egv1a1.EnvoyProxy) *egv1a1.MergeType { +// effectiveMergeType returns the mergeType to use when merging a route-level policy into its +// parent policy: the policy's own value if set (Replace meaning "do not merge"), otherwise the +// parent policy's defaultChildMergeType. +func effectiveMergeType(policy, parentPolicy *egv1a1.BackendTrafficPolicy) *egv1a1.MergeType { if policy.Spec.MergeType != nil { + if *policy.Spec.MergeType == egv1a1.Replace { + return nil + } return policy.Spec.MergeType } - if ep == nil || ep.Spec.PolicyDefaults == nil || ep.Spec.PolicyDefaults.BackendTrafficPolicy == nil || - ep.Spec.PolicyDefaults.BackendTrafficPolicy.MergeType == nil { - return nil - } - if policy.Namespace == t.ControllerNamespace { + if parentPolicy == nil || parentPolicy.Spec.DefaultChildMergeType == nil { return nil } - d := ep.Spec.PolicyDefaults.BackendTrafficPolicy - if label := ptr.Deref(d.MergeExcludeLabel, ""); label != "" { - if _, ok := policy.Labels[label]; ok { - return nil - } - } - // Defense in depth: the CRD enum restricts MergeType to StrategicMerge/JSONMerge, but the - // EnvoyGateway default EnvoyProxySpec is not subject to CRD validation. Ignore anything that is - // not a real merge so a stray value (e.g. Replace) can never produce a "merged" status while - // actually replacing the parent. - if *d.MergeType != egv1a1.StrategicMerge && *d.MergeType != egv1a1.JSONMerge { + // Defense in depth: the CRD enum restricts DefaultChildMergeType to StrategicMerge/JSONMerge. + // Ignore anything that is not a real merge so a stray value can never produce a "merged" + // status while actually replacing the parent. + if *parentPolicy.Spec.DefaultChildMergeType != egv1a1.StrategicMerge && + *parentPolicy.Spec.DefaultChildMergeType != egv1a1.JSONMerge { return nil } - return d.MergeType + return parentPolicy.Spec.DefaultChildMergeType } -// anyGatewayMergeDefault reports whether any of the route's parent gateways supplies a default -// mergeType for the policy via its EnvoyProxy. -func (t *Translator) anyGatewayMergeDefault(policy *egv1a1.BackendTrafficPolicy, parentRefCtxs []*RouteParentContext) bool { +// anyParentPolicyMergeDefault reports whether any parent policy (gateway- or listener-level) of +// the route's parent gateways sets defaultChildMergeType. +func anyParentPolicyMergeDefault(parentRefCtxs []*RouteParentContext, gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy) bool { for _, p := range parentRefCtxs { for _, l := range p.listeners { - if t.effectiveMergeType(policy, l.gateway.envoyProxy) != nil { - return true + gwNN := utils.NamespacedName(l.gateway.Gateway) + for _, key := range []NamespacedNameWithSection{ + {NamespacedName: gwNN, SectionName: l.Name}, + {NamespacedName: gwNN}, + } { + if pp := gatewayPolicyMap[key]; pp != nil && pp.Spec.DefaultChildMergeType != nil { + return true + } } } } diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go index eb4ea38b8f..54871f83a4 100644 --- a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -20,40 +20,38 @@ import ( func TestEffectiveMergeType(t *testing.T) { strategic := egv1a1.StrategicMerge jsonMerge := egv1a1.JSONMerge + replace := egv1a1.Replace - ep := func(mt *egv1a1.MergeType, label string) *egv1a1.EnvoyProxy { - d := &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{MergeType: mt}} - if label != "" { - d.MergeExcludeLabel = new(label) + child := func(mt *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { + return &egv1a1.BackendTrafficPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "app", Name: "child"}, + Spec: egv1a1.BackendTrafficPolicySpec{MergeType: mt}, } - return &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{PolicyDefaults: &egv1a1.PolicyDefaults{BackendTrafficPolicy: d}}} } - btp := func(ns string, labels map[string]string, mt *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { + parent := func(defaultMT *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { return &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: ns, Labels: labels}, - Spec: egv1a1.BackendTrafficPolicySpec{MergeType: mt}, + ObjectMeta: metav1.ObjectMeta{Namespace: "app", Name: "parent"}, + Spec: egv1a1.BackendTrafficPolicySpec{DefaultChildMergeType: defaultMT}, } } - tr := &Translator{ControllerNamespace: "eg"} - tests := []struct { - name string - pol *egv1a1.BackendTrafficPolicy - ep *egv1a1.EnvoyProxy - want *egv1a1.MergeType + name string + pol *egv1a1.BackendTrafficPolicy + parent *egv1a1.BackendTrafficPolicy + want *egv1a1.MergeType }{ - {"explicit value wins over default", btp("app", nil, &jsonMerge), ep(&strategic, ""), &jsonMerge}, - {"default applied when unset", btp("app", nil, nil), ep(&strategic, ""), &strategic}, - {"no envoyproxy stays nil", btp("app", nil, nil), nil, nil}, - {"no default in envoyproxy stays nil", btp("app", nil, nil), ep(nil, ""), nil}, - {"control-plane namespace excluded", btp("eg", nil, nil), ep(&strategic, ""), nil}, - {"exclude label opts out", btp("app", map[string]string{"skip": "x"}, nil), ep(&strategic, "skip"), nil}, + {"explicit value wins over parent default", child(&jsonMerge), parent(&strategic), &jsonMerge}, + {"explicit replace opts out of parent default", child(&replace), parent(&strategic), nil}, + {"parent default applied when unset", child(nil), parent(&strategic), &strategic}, + {"no parent policy stays nil", child(nil), nil, nil}, + {"parent without default stays nil", child(nil), parent(nil), nil}, + {"invalid parent default is ignored", child(nil), parent(&replace), nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := tr.effectiveMergeType(tt.pol, tt.ep) + got := effectiveMergeType(tt.pol, tt.parent) if tt.want == nil { assert.Nil(t, got) return @@ -64,154 +62,74 @@ func TestEffectiveMergeType(t *testing.T) { } } -// TestEffectiveMergeType_AdditionalBranches covers the effectiveMergeType branches that -// TestEffectiveMergeType does not: an EnvoyProxy without a BackendTrafficPolicy section, and an -// exclude label that is configured but not present on the policy (so the default still applies). -func TestEffectiveMergeType_AdditionalBranches(t *testing.T) { +// TestAnyParentPolicyMergeDefault covers anyParentPolicyMergeDefault, which reports whether any +// gateway- or listener-level parent policy of the route's parent gateways sets +// defaultChildMergeType. +func TestAnyParentPolicyMergeDefault(t *testing.T) { strategic := egv1a1.StrategicMerge - - tr := &Translator{ControllerNamespace: "eg"} - - tests := []struct { - name string - pol *egv1a1.BackendTrafficPolicy - ep *egv1a1.EnvoyProxy - want *egv1a1.MergeType - }{ - { - // ep != nil but ep.Spec.PolicyDefaults == nil -> nil. - name: "envoyproxy without policyDefaults stays nil", - pol: &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, - }, - ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}}, - want: nil, - }, - { - // exclude label configured but absent on the policy -> falls through to the default. - name: "exclude label configured but not on policy applies default", - pol: &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app", Labels: map[string]string{"other": "x"}}, - }, - ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: &strategic, - MergeExcludeLabel: new("skip"), - }}, - }, + gwNN := types.NamespacedName{Namespace: "envoy-gateway", Name: "gw"} + + parentCtx := func(listenerName string) *RouteParentContext { + return &RouteParentContext{ + listeners: []*ListenerContext{{ + Listener: &gwapiv1.Listener{Name: gwapiv1.SectionName(listenerName)}, + gateway: &GatewayContext{Gateway: &gwapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Namespace: gwNN.Namespace, Name: gwNN.Name}, + }}, }}, - want: &strategic, - }, - { - // nil labels map with an exclude label configured -> default still applies. - name: "nil labels with exclude label configured applies default", - pol: &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, - }, - ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: &strategic, - MergeExcludeLabel: new("skip"), - }}, - }, - }}, - want: &strategic, - }, - { - // Defense in depth: a non-merge value (e.g. Replace, which can slip through the - // unvalidated EnvoyGateway default spec) is ignored instead of producing a "merged" - // status while actually replacing. - name: "replace default is ignored", - pol: &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, - }, - ep: &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.Replace), - }}, - }, - }}, - want: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tr.effectiveMergeType(tt.pol, tt.ep) - if tt.want == nil { - assert.Nil(t, got) - return - } - assert.NotNil(t, got) - assert.Equal(t, *tt.want, *got) - }) - } -} - -// TestAnyGatewayMergeDefault covers anyGatewayMergeDefault, which reports whether any of a route's -// parent gateways supplies a default mergeType for the policy via its EnvoyProxy. -func TestAnyGatewayMergeDefault(t *testing.T) { - strategic := egv1a1.StrategicMerge - - epWithDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{MergeType: &strategic}}}, - }} - epNoDefault := &egv1a1.EnvoyProxy{Spec: egv1a1.EnvoyProxySpec{}} - - // parentWith builds a parent context whose listeners reference gateways carrying the given - // EnvoyProxies (one listener per EnvoyProxy). - parentWith := func(eps ...*egv1a1.EnvoyProxy) *RouteParentContext { - p := &RouteParentContext{} - for _, ep := range eps { - p.listeners = append(p.listeners, &ListenerContext{ - gateway: &GatewayContext{envoyProxy: ep}, - }) } - return p } - - policy := &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app"}, + parentPolicy := func(defaultMT *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { + return &egv1a1.BackendTrafficPolicy{ + Spec: egv1a1.BackendTrafficPolicySpec{DefaultChildMergeType: defaultMT}, + } } - tr := &Translator{ControllerNamespace: "eg"} - tests := []struct { name string parents []*RouteParentContext + policy map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy want bool }{ - {"no parents", nil, false}, - {"parent with no listeners", []*RouteParentContext{parentWith()}, false}, - {"single parent supplies default", []*RouteParentContext{parentWith(epWithDefault)}, true}, - {"single parent no default", []*RouteParentContext{parentWith(epNoDefault)}, false}, - {"mixed listeners on one parent", []*RouteParentContext{parentWith(epNoDefault, epWithDefault)}, true}, + {"no parents", nil, nil, false}, + {"no parent policies", []*RouteParentContext{parentCtx("http")}, nil, false}, + { + "gateway-level policy without default", + []*RouteParentContext{parentCtx("http")}, + map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy{ + {NamespacedName: gwNN}: parentPolicy(nil), + }, + false, + }, { - "multiple parents one supplies default", - []*RouteParentContext{parentWith(epNoDefault), parentWith(epWithDefault)}, + "gateway-level policy with default", + []*RouteParentContext{parentCtx("http")}, + map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy{ + {NamespacedName: gwNN}: parentPolicy(&strategic), + }, true, }, { - "multiple parents none supply default", - []*RouteParentContext{parentWith(epNoDefault), parentWith(nil)}, - false, + "listener-level policy with default", + []*RouteParentContext{parentCtx("http")}, + map[NamespacedNameWithSection]*egv1a1.BackendTrafficPolicy{ + {NamespacedName: gwNN, SectionName: "http"}: parentPolicy(&strategic), + }, + true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tr.anyGatewayMergeDefault(policy, tt.parents)) + assert.Equal(t, tt.want, anyParentPolicyMergeDefault(tt.parents, tt.policy)) }) } } // TestApplyTrafficFeatureToRoute_MergeGatewayScoping covers the MergeGateways scoping in // applyTrafficFeatureToRoute for the TCP and UDP listener loops: a listener belonging to a -// different Gateway must be skipped, so a defaulted merged policy does not bleed across Gateways -// that share one IR. The HTTP path is covered by the merged-gateways golden fixture. +// different Gateway must be skipped, so a merged policy does not bleed across Gateways that +// share one IR. The HTTP path is covered by the merged-gateways golden fixture. func TestApplyTrafficFeatureToRoute_MergeGatewayScoping(t *testing.T) { tr := &Translator{} gwNN := &types.NamespacedName{Namespace: "envoy-gateway", Name: "gw"} diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml deleted file mode 100644 index 3079913437..0000000000 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.in.yaml +++ /dev/null @@ -1,86 +0,0 @@ -namespaces: - - apiVersion: v1 - kind: Namespace - metadata: - name: envoy-gateway-system -envoyProxyForGatewayClass: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - namespace: envoy-gateway-system - name: test - spec: - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge -gateways: - - apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - namespace: envoy-gateway - name: gateway-1 - spec: - gatewayClassName: envoy-gateway-class - listeners: - - name: http - protocol: HTTP - port: 80 - allowedRoutes: - namespaces: - from: All -httpRoutes: - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: envoy-gateway-system - name: httproute-1 - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway - name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/" - backendRefs: - - name: service-1 - port: 8080 -backendTrafficPolicies: - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: BackendTrafficPolicy - metadata: - namespace: envoy-gateway - name: policy-for-gateway - spec: - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - timeout: - tcp: - connectTimeout: 15s - http: - connectionIdleTimeout: 16s - maxConnectionDuration: 17s - httpUpgrade: - - type: websocket - # Route-level policy lives in the control-plane (controller) namespace, so the EnvoyProxy - # mergeType does not apply and the policy replaces the gateway-level policy. - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: BackendTrafficPolicy - metadata: - namespace: envoy-gateway-system - name: policy-for-route - spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: httproute-1 - timeout: - tcp: - connectTimeout: 10s - connection: - bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml deleted file mode 100644 index c3eeca1a11..0000000000 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-control-plane-ns.out.yaml +++ /dev/null @@ -1,259 +0,0 @@ -backendTrafficPolicies: -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: BackendTrafficPolicy - metadata: - name: policy-for-route - namespace: envoy-gateway-system - spec: - connection: - bufferLimit: 100M - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: httproute-1 - timeout: - tcp: - connectTimeout: 10s - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - sectionName: http - conditions: - - lastTransitionTime: null - message: Policy has been accepted. - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: spec.targetRef is deprecated, use spec.targetRefs instead - reason: DeprecatedField - status: "True" - type: Warning - controllerName: gateway.envoyproxy.io/gatewayclass-controller -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: BackendTrafficPolicy - metadata: - name: policy-for-gateway - namespace: envoy-gateway - spec: - httpUpgrade: - - type: websocket - targetRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - timeout: - http: - connectionIdleTimeout: 16s - maxConnectionDuration: 17s - tcp: - connectTimeout: 15s - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - conditions: - - lastTransitionTime: null - message: Policy has been accepted. - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: spec.targetRef is deprecated, use spec.targetRefs instead - reason: DeprecatedField - status: "True" - type: Warning - - lastTransitionTime: null - message: 'This policy is being overridden by other backendTrafficPolicies - for these routes: [envoy-gateway-system/httproute-1]' - reason: Overridden - status: "True" - type: Overridden - controllerName: gateway.envoyproxy.io/gatewayclass-controller -gateways: -- apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - name: gateway-1 - namespace: envoy-gateway - spec: - gatewayClassName: envoy-gateway-class - listeners: - - allowedRoutes: - namespaces: - from: All - name: http - port: 80 - protocol: HTTP - status: - listeners: - - attachedRoutes: 1 - conditions: - - lastTransitionTime: null - message: Sending translated listener configuration to the data plane - reason: Programmed - status: "True" - type: Programmed - - lastTransitionTime: null - message: Listener has been successfully translated - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: Listener references have been resolved - reason: ResolvedRefs - status: "True" - type: ResolvedRefs - name: http - supportedKinds: - - group: gateway.networking.k8s.io - kind: HTTPRoute - - group: gateway.networking.k8s.io - kind: GRPCRoute -httpRoutes: -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: httproute-1 - namespace: envoy-gateway-system - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - name: gateway-1 - namespace: envoy-gateway - sectionName: http - rules: - - backendRefs: - - name: service-1 - port: 8080 - matches: - - path: - value: / - status: - parents: - - conditions: - - lastTransitionTime: null - message: Route is accepted - reason: Accepted - status: "True" - type: Accepted - - lastTransitionTime: null - message: 'Failed to process route rule 0 backendRef 0: service envoy-gateway-system/service-1 - not found.' - reason: BackendNotFound - status: "False" - type: ResolvedRefs - controllerName: gateway.envoyproxy.io/gatewayclass-controller - parentRef: - name: gateway-1 - namespace: envoy-gateway - sectionName: http -infraIR: - envoy-gateway/gateway-1: - proxy: - config: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - name: test - namespace: envoy-gateway-system - spec: - logging: {} - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - status: {} - listeners: - - name: envoy-gateway/gateway-1/http - ports: - - containerPort: 10080 - name: http-80 - protocol: HTTP - servicePort: 80 - metadata: - labels: - gateway.envoyproxy.io/owning-gateway-name: gateway-1 - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway - ownerReference: - kind: GatewayClass - name: envoy-gateway-class - name: envoy-gateway/gateway-1 - namespace: envoy-gateway-system -xdsIR: - envoy-gateway/gateway-1: - accessLog: - json: - - path: /dev/stdout - globalResources: - proxyServiceCluster: - metadata: - kind: Service - name: envoy-envoy-gateway-gateway-1-196ae069 - namespace: envoy-gateway-system - sectionName: "8080" - name: envoy-gateway/gateway-1 - settings: - - addressType: IP - endpoints: - - host: 7.6.5.4 - port: 8080 - zone: zone1 - metadata: - kind: Service - name: envoy-envoy-gateway-gateway-1-196ae069 - namespace: envoy-gateway-system - sectionName: "8080" - name: envoy-gateway/gateway-1 - protocol: TCP - http: - - address: 0.0.0.0 - externalPort: 80 - hostnames: - - '*' - metadata: - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - sectionName: http - name: envoy-gateway/gateway-1/http - path: - escapedSlashesAction: UnescapeAndRedirect - mergeSlashes: true - port: 10080 - routes: - - directResponse: - statusCode: 500 - hostname: gateway.envoyproxy.io - isHTTP2: false - metadata: - kind: HTTPRoute - name: httproute-1 - namespace: envoy-gateway-system - policies: - - kind: BackendTrafficPolicy - name: policy-for-route - namespace: envoy-gateway-system - name: httproute/envoy-gateway-system/httproute-1/rule/0/match/0/gateway_envoyproxy_io - pathMatch: - distinct: false - name: "" - prefix: / - traffic: - backendConnection: - bufferLimit: 100000000 - timeout: - tcp: - connectTimeout: 10s - readyListener: - address: 0.0.0.0 - ipFamily: IPv4 - path: /ready - port: 19003 diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml index 8304735311..e94a983ca9 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.in.yaml @@ -6,9 +6,6 @@ envoyProxyForGatewayClass: namespace: envoy-gateway-system spec: mergeGateways: true - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -73,6 +70,7 @@ backendTrafficPolicies: group: gateway.networking.k8s.io kind: Gateway name: gateway-1 + defaultChildMergeType: StrategicMerge circuitBreaker: maxConnections: 1111 - apiVersion: gateway.envoyproxy.io/v1alpha1 @@ -85,6 +83,7 @@ backendTrafficPolicies: group: gateway.networking.k8s.io kind: Gateway name: gateway-2 + defaultChildMergeType: StrategicMerge circuitBreaker: maxConnections: 2222 # Route-level policy with no mergeType: should merge into each Gateway's own parent, so the route diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml index 18a9188883..20e30b7e37 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-merged-gateways.out.yaml @@ -67,6 +67,7 @@ backendTrafficPolicies: spec: circuitBreaker: maxConnections: 1111 + defaultChildMergeType: StrategicMerge targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -104,6 +105,7 @@ backendTrafficPolicies: spec: circuitBreaker: maxConnections: 2222 + defaultChildMergeType: StrategicMerge targetRef: group: gateway.networking.k8s.io kind: Gateway @@ -281,9 +283,6 @@ infraIR: spec: logging: {} mergeGateways: true - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge status: {} listeners: - name: envoy-gateway/gateway-1/http diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml index 58527f09b1..71e4b64bfb 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.in.yaml @@ -1,14 +1,3 @@ -envoyProxiesForGateways: - # Attached to gateway-1 only: supplies a default mergeType. - - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - namespace: envoy-gateway - name: ep-merge-default - spec: - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -17,11 +6,6 @@ gateways: name: gateway-1 spec: gatewayClassName: envoy-gateway-class - infrastructure: - parametersRef: - group: gateway.envoyproxy.io - kind: EnvoyProxy - name: ep-merge-default listeners: - name: http protocol: HTTP @@ -29,7 +13,7 @@ gateways: allowedRoutes: namespaces: from: All - # gateway-2 has no EnvoyProxy default, so the same route policy applies standalone here. + # gateway-2's policy has no defaultChildMergeType, so the same route policy applies standalone here. - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: @@ -78,6 +62,7 @@ backendTrafficPolicies: group: gateway.networking.k8s.io kind: Gateway name: gateway-1 + defaultChildMergeType: StrategicMerge timeout: tcp: connectTimeout: 15s @@ -101,8 +86,8 @@ backendTrafficPolicies: connectTimeout: 25s httpUpgrade: - type: websocket - # Route-level policy with no mergeType: merges into gateway-1's policy (which has the default), - # but applies standalone on gateway-2 (which has no default). + # Route-level policy with no mergeType: merges into gateway-1's policy (which sets + # defaultChildMergeType), but applies standalone on gateway-2 (whose policy does not). - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml index abd95856dc..a058487b62 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-multi-parent.out.yaml @@ -63,6 +63,7 @@ backendTrafficPolicies: name: policy-for-gateway-1 namespace: envoy-gateway spec: + defaultChildMergeType: StrategicMerge httpUpgrade: - type: websocket targetRef: @@ -140,30 +141,6 @@ backendTrafficPolicies: status: "True" type: Overridden controllerName: gateway.envoyproxy.io/gatewayclass-controller -envoyProxiesForGateways: -- apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - name: ep-merge-default - namespace: envoy-gateway - spec: - logging: {} - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - conditions: - - lastTransitionTime: null - message: EnvoyProxy has been accepted. - reason: Accepted - status: "True" - type: Accepted gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -172,11 +149,6 @@ gateways: namespace: envoy-gateway spec: gatewayClassName: envoy-gateway-class - infrastructure: - parametersRef: - group: gateway.envoyproxy.io - kind: EnvoyProxy - name: ep-merge-default listeners: - allowedRoutes: namespaces: @@ -308,30 +280,6 @@ httpRoutes: infraIR: envoy-gateway/gateway-1: proxy: - config: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - name: ep-merge-default - namespace: envoy-gateway - spec: - logging: {} - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: gateway-1 - namespace: envoy-gateway - conditions: - - lastTransitionTime: null - message: EnvoyProxy has been accepted. - reason: Accepted - status: "True" - type: Accepted listeners: - name: envoy-gateway/gateway-1/http ports: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.in.yaml similarity index 75% rename from internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml rename to internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.in.yaml index f40f78b92f..8c541daedd 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.in.yaml @@ -1,14 +1,3 @@ -envoyProxyForGatewayClass: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - namespace: envoy-gateway-system - name: test - spec: - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -55,6 +44,7 @@ backendTrafficPolicies: group: gateway.networking.k8s.io kind: Gateway name: gateway-1 + defaultChildMergeType: StrategicMerge timeout: tcp: connectTimeout: 15s @@ -63,20 +53,19 @@ backendTrafficPolicies: maxConnectionDuration: 17s httpUpgrade: - type: websocket - # Route-level policy carries the EnvoyProxy mergeExcludeLabel, so it opts out of the - # mergeType and replaces (rather than merges into) the gateway-level policy. + # Route-level policy with mergeType Replace: it opts out of the parent policy's + # defaultChildMergeType and replaces the gateway-level policy for this route. - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: namespace: default name: policy-for-route - labels: - gateway.envoyproxy.io/skip-merge-default: "true" spec: targetRef: group: gateway.networking.k8s.io kind: HTTPRoute name: httproute-1 + mergeType: Replace timeout: tcp: connectTimeout: 10s diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.out.yaml similarity index 93% rename from internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml rename to internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.out.yaml index d0591a5aa9..d679696218 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-exclude-label.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-replace-opt-out.out.yaml @@ -2,13 +2,12 @@ backendTrafficPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: - labels: - gateway.envoyproxy.io/skip-merge-default: "true" name: policy-for-route namespace: default spec: connection: bufferLimit: 100M + mergeType: Replace targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -42,6 +41,7 @@ backendTrafficPolicies: name: policy-for-gateway namespace: envoy-gateway spec: + defaultChildMergeType: StrategicMerge httpUpgrade: - type: websocket targetRef: @@ -160,19 +160,6 @@ httpRoutes: infraIR: envoy-gateway/gateway-1: proxy: - config: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - name: test - namespace: envoy-gateway-system - spec: - logging: {} - policyDefaults: - backendTrafficPolicy: - mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default - mergeType: StrategicMerge - status: {} listeners: - name: envoy-gateway/gateway-1/http ports: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml index cb232a60aa..238da3ed5c 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.in.yaml @@ -1,13 +1,3 @@ -envoyProxyForGatewayClass: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - namespace: envoy-gateway-system - name: test - spec: - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -54,6 +44,7 @@ backendTrafficPolicies: group: gateway.networking.k8s.io kind: Gateway name: gateway-1 + defaultChildMergeType: StrategicMerge timeout: tcp: connectTimeout: 15s @@ -62,8 +53,8 @@ backendTrafficPolicies: maxConnectionDuration: 17s httpUpgrade: - type: websocket - # Route-level policy with no mergeType: the EnvoyProxy mergeType makes it merge - # into the gateway-level policy instead of replacing it. + # Route-level policy with no mergeType: the parent policy's defaultChildMergeType makes it + # merge into the gateway-level policy instead of replacing it. - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: BackendTrafficPolicy metadata: diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml index cb6f9cb806..38b910bb37 100644 --- a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype.out.yaml @@ -45,6 +45,7 @@ backendTrafficPolicies: name: policy-for-gateway namespace: envoy-gateway spec: + defaultChildMergeType: StrategicMerge httpUpgrade: - type: websocket targetRef: @@ -163,18 +164,6 @@ httpRoutes: infraIR: envoy-gateway/gateway-1: proxy: - config: - apiVersion: gateway.envoyproxy.io/v1alpha1 - kind: EnvoyProxy - metadata: - name: test - namespace: envoy-gateway-system - spec: - logging: {} - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - status: {} listeners: - name: envoy-gateway/gateway-1/http ports: diff --git a/release-notes/current/new_features/9334-backendtrafficpolicy-default-child-mergetype.md b/release-notes/current/new_features/9334-backendtrafficpolicy-default-child-mergetype.md new file mode 100644 index 0000000000..eec212a7b2 --- /dev/null +++ b/release-notes/current/new_features/9334-backendtrafficpolicy-default-child-mergetype.md @@ -0,0 +1 @@ +Added `defaultChildMergeType` to `BackendTrafficPolicy` for policies targeting a Gateway, so a route-level policy that doesn't set `mergeType` merges into the parent policy instead of replacing it. A child policy with an explicit `mergeType` keeps its own behavior. diff --git a/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md b/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md deleted file mode 100644 index 7c08c9bba6..0000000000 --- a/release-notes/current/new_features/9334-backendtrafficpolicy-default-mergetype-envoyproxy.md +++ /dev/null @@ -1 +0,0 @@ -Added `backendTrafficPolicy.defaultMergeType` to `EnvoyProxy`, so a route-level `BackendTrafficPolicy` that doesn't set `mergeType` merges into its parent policy instead of replacing it. An optional `excludeLabel` lets individual policies opt out. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index b7613b9a65..28c290e0b3 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -559,21 +559,6 @@ between the Envoy Proxy listener and the backend service. | `status` | _[PolicyStatus](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#policystatus)_ | true | | status defines the current status of BackendTrafficPolicy. | -#### BackendTrafficPolicyDefaults - - - -BackendTrafficPolicyDefaults defines defaults applied to BackendTrafficPolicy resources. - -_Appears in:_ -- [PolicyDefaults](#policydefaults) - -| Field | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType is the mergeType applied to a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | -| `mergeExcludeLabel` | _string_ | false | | MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. | - - #### BackendTrafficPolicySpec @@ -599,6 +584,7 @@ _Appears in:_ | `dns` | _[DNS](#dns)_ | false | | DNS includes dns resolution settings. | | `http2` | _[HTTP2Settings](#http2settings)_ | false | | HTTP2 provides HTTP/2 configuration for backend connections. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType determines how this configuration is merged with existing BackendTrafficPolicy
configurations targeting a parent resource. When set, this configuration will be merged
into a parent BackendTrafficPolicy (i.e. the one targeting a Gateway or Listener).
This field cannot be set when targeting a parent resource (Gateway).
If unset, no merging occurs, and only the most specific configuration takes effect. | +| `defaultChildMergeType` | _[MergeType](#mergetype)_ | false | | DefaultChildMergeType is the merge strategy applied to child policies (policies targeting
an xRoute under this policy's target) that do not set their own mergeType, so a child
policy merges into this policy instead of replacing it. A child policy can opt out by
setting mergeType to Replace.
This field can only be set on policies targeting a parent resource (Gateway). | | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `bandwidthLimit` | _[BandwidthLimitSpec](#bandwidthlimitspec)_ | false | | BandwidthLimit allows the user to limit the bandwidth of traffic
sent to and received from the backend. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | @@ -2256,7 +2242,6 @@ _Appears in:_ | `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). | | `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. | -| `policyDefaults` | _[PolicyDefaults](#policydefaults)_ | false | | PolicyDefaults defines defaults applied to Envoy Gateway policies attached to
Gateways that use this EnvoyProxy. | #### EnvoyProxyStatus @@ -4171,21 +4156,6 @@ _Appears in:_ | `ValueRef` | LuaValueTypeValueRef defines the "ValueRef" Lua type.
| -#### MergeSettings - - - -MergeSettings defines how an Envoy Gateway policy that does not set a mergeType is merged by default. - -_Appears in:_ -- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) - -| Field | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType is the mergeType applied to a policy that does not set one,
so a route-level policy merges into its parent instead of replacing it. | -| `mergeExcludeLabel` | _string_ | false | | MergeExcludeLabel, when present on a policy, opts that policy out of the default MergeType. | - - #### MergeType _Underlying type:_ _string_ @@ -4193,11 +4163,9 @@ _Underlying type:_ _string_ MergeType defines the type of merge operation _Appears in:_ -- [BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults) - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) - [EnvoyProxySpec](#envoyproxyspec) - [KubernetesPatchSpec](#kubernetespatchspec) -- [MergeSettings](#mergesettings) - [SecurityPolicySpec](#securitypolicyspec) | Value | Description | @@ -4599,20 +4567,6 @@ _Appears in:_ | `backOff` | _[BackOffPolicy](#backoffpolicy)_ | false | | Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential
back-off algorithm for retries. For additional details,
see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries | -#### PolicyDefaults - - - -PolicyDefaults defines defaults applied to Envoy Gateway policies, keyed by policy kind. - -_Appears in:_ -- [EnvoyProxySpec](#envoyproxyspec) - -| Field | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| `backendTrafficPolicy` | _[BackendTrafficPolicyDefaults](#backendtrafficpolicydefaults)_ | false | | BackendTrafficPolicy defines defaults applied to BackendTrafficPolicy resources. | - - #### PolicyTargetReferences diff --git a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md index bf9520716e..48549ff5ca 100644 --- a/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md +++ b/site/content/en/latest/concepts/gateway_api_extensions/backend-traffic-policy.md @@ -147,7 +147,7 @@ spec: In this example, `alpha-policy` would take precedence due to its earlier creation time, so the HTTPRoute would use `maxConnections: 30`. -When the `mergeType` field is unset, no merging occurs and only the most specific configuration takes effect, unless a default is configured on the EnvoyProxy (see [Defaulting mergeType via EnvoyProxy](#defaulting-mergetype-via-envoyproxy)). Policies can also opt into merging explicitly using the `mergeType` field (see [Policy Merging](#policy-merging) section below). +When the `mergeType` field is unset, no merging occurs and only the most specific configuration takes effect, unless the parent policy sets `defaultChildMergeType` (see [Defaulting mergeType via the parent policy](#defaulting-mergetype-via-the-parent-policy)). Policies can also opt into merging explicitly using the `mergeType` field (see [Policy Merging](#policy-merging) section below). ## Policy Merging @@ -214,38 +214,39 @@ spec: In this example, the route-level policy merges with the gateway-level policy, resulting in both rate limits being enforced: the global 100 requests/second abuse limit and the route-specific 5 requests/minute limit. -### Defaulting mergeType via EnvoyProxy +### Defaulting mergeType via the parent policy -Setting `mergeType` on every route-level policy can be repetitive when an organization wants merging to be the norm. To make merging the default, configure it under `policyDefaults.backendTrafficPolicy` on the [EnvoyProxy](../../api/extension_types#envoyproxy) attached to the GatewayClass or Gateway: +Setting `mergeType` on every route-level policy can be repetitive when an organization wants merging to be the norm. To make merging the default, set `defaultChildMergeType` on the parent (Gateway-targeting) BackendTrafficPolicy: ```yaml apiVersion: gateway.envoyproxy.io/v1alpha1 -kind: EnvoyProxy +kind: BackendTrafficPolicy metadata: - name: custom-proxy-config - namespace: envoy-gateway-system + name: gateway-baseline spec: - policyDefaults: - backendTrafficPolicy: - mergeType: StrategicMerge - # Optional: a policy carrying this label key opts out of the default and replaces - # its parent instead of merging. The label's value is ignored. - mergeExcludeLabel: gateway.envoyproxy.io/skip-merge-default + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: eg + defaultChildMergeType: StrategicMerge + rateLimit: + # ... baseline configuration for all routes under this Gateway ``` -With this configuration, a route-level BackendTrafficPolicy that does **not** set `mergeType` merges into its parent (gateway-level or listener-level) policy as if it had set `mergeType: StrategicMerge`. The default follows the same precedence as other EnvoyProxy settings, so it can be set once at the GatewayClass level or overridden per Gateway. +With this configuration, a route-level BackendTrafficPolicy that does **not** set `mergeType` merges into this parent policy as if it had set `mergeType: StrategicMerge`. This keeps merge behavior discoverable from the Policy resources alone, following the pattern described in [GEP-713](https://gateway-api.sigs.k8s.io/geps/gep-713/#example-3-merged-specs). + +The effective merge strategy for a child policy is resolved in this order: -The defaulting behavior follows these rules: +1. The child policy's own `mergeType`, if set. `Replace` explicitly opts the child out of merging, so it replaces the parent even when the parent sets `defaultChildMergeType`. +2. Otherwise, the closest parent policy's `defaultChildMergeType` (the listener-level policy if one exists, else the gateway-level policy). +3. Otherwise, no merging occurs and only the most specific policy takes effect. -- A policy's own `mergeType` always wins; the default only applies when `mergeType` is unset. -- `mergeType` accepts only `StrategicMerge` or `JSONMerge`. `Replace` is not allowed because defaulting to a replace would have no effect. -- A policy that carries the configured `mergeExcludeLabel` key opts out of the default and replaces its parent, even when `mergeType` is unset. -- Policies in the Envoy Gateway control-plane namespace are not defaulted. +Note that `defaultChildMergeType` accepts only `StrategicMerge` or `JSONMerge` (defaulting to a replace would have no effect), and it can only be set on policies targeting a Gateway. ### Key Constraints - The `mergeType` field can only be set on policies targeting child resources (like HTTPRoute), not parent resources (like Gateway) -- When `mergeType` is unset, no merging occurs - only the most specific policy takes effect - unless a [default is configured on the EnvoyProxy](#defaulting-mergetype-via-envoyproxy) +- When `mergeType` is unset, no merging occurs - only the most specific policy takes effect - unless the parent policy sets a [defaultChildMergeType](#defaulting-mergetype-via-the-parent-policy) - The merged configuration combines both policies, enabling layered protection strategies ## Related Resources diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index 6bd00eb1c0..30699f0c50 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -3726,6 +3726,60 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "defaultChildMergeType is valid on gateway targets", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + }, + }, + DefaultChildMergeType: new(egv1a1.StrategicMerge), + } + }, + wantErrors: []string{}, + }, + { + desc: "defaultChildMergeType is rejected on route targets", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("HTTPRoute"), + Name: gwapiv1.ObjectName("httpbin-route"), + }, + }, + }, + DefaultChildMergeType: new(egv1a1.StrategicMerge), + } + }, + wantErrors: []string{"defaultChildMergeType can only be used with Gateway targets"}, + }, + { + desc: "defaultChildMergeType Replace is rejected", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + }, + }, + DefaultChildMergeType: new(egv1a1.Replace), + } + }, + wantErrors: []string{"Unsupported value: \"Replace\": supported values: \"StrategicMerge\", \"JSONMerge\""}, + }, } for _, tc := range cases { diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 052e799c85..20e425157c 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -2485,46 +2485,6 @@ func TestEnvoyProxyProvider(t *testing.T) { }, wantErrors: []string{"If type is Remote, local field must not be set"}, }, - { - desc: "policyDefaults backendTrafficPolicy mergeType StrategicMerge is valid", - mutate: func(envoy *egv1a1.EnvoyProxy) { - envoy.Spec = egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.StrategicMerge), - }}, - }, - } - }, - wantErrors: []string{}, - }, - { - desc: "policyDefaults backendTrafficPolicy mergeType JSONMerge with mergeExcludeLabel is valid", - mutate: func(envoy *egv1a1.EnvoyProxy) { - envoy.Spec = egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.JSONMerge), - MergeExcludeLabel: new("gateway.envoyproxy.io/skip-merge-default"), - }}, - }, - } - }, - wantErrors: []string{}, - }, - { - desc: "policyDefaults backendTrafficPolicy mergeType Replace is rejected", - mutate: func(envoy *egv1a1.EnvoyProxy) { - envoy.Spec = egv1a1.EnvoyProxySpec{ - PolicyDefaults: &egv1a1.PolicyDefaults{ - BackendTrafficPolicy: &egv1a1.BackendTrafficPolicyDefaults{MergeSettings: egv1a1.MergeSettings{ - MergeType: new(egv1a1.MergeType("Replace")), - }}, - }, - } - }, - wantErrors: []string{"Unsupported value: \"Replace\": supported values: \"StrategicMerge\", \"JSONMerge\""}, - }, } for _, tc := range cases { diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 34172d5957..04d2cc537c 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -25130,6 +25130,17 @@ spec: Note that when the suffix is not provided, the value is interpreted as bytes. x-kubernetes-int-or-string: true type: object + defaultChildMergeType: + description: |- + DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + an xRoute under this policy's target) that do not set their own mergeType, so a child + policy merges into this policy instead of replacing it. A child policy can opt out by + setting mergeType to Replace. + This field can only be set on policies targeting a parent resource (Gateway). + enum: + - StrategicMerge + - JSONMerge + type: string dns: description: DNS includes dns resolution settings. properties: @@ -27974,6 +27985,11 @@ spec: || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' + - message: defaultChildMergeType can only be used with Gateway targets + rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || + self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, + ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && @@ -34453,29 +34469,6 @@ spec: - StrategicMerge - JSONMerge type: string - policyDefaults: - description: |- - PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - Gateways that use this EnvoyProxy. - properties: - backendTrafficPolicy: - description: BackendTrafficPolicy defines defaults applied to - BackendTrafficPolicy resources. - properties: - mergeExcludeLabel: - description: MergeExcludeLabel, when present on a policy, - opts that policy out of the default MergeType. - type: string - mergeType: - description: |- - MergeType is the mergeType applied to a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - type: object - type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index db6fed1e24..5e2e6bd3ce 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -1068,6 +1068,17 @@ spec: Note that when the suffix is not provided, the value is interpreted as bytes. x-kubernetes-int-or-string: true type: object + defaultChildMergeType: + description: |- + DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + an xRoute under this policy's target) that do not set their own mergeType, so a child + policy merges into this policy instead of replacing it. A child policy can opt out by + setting mergeType to Replace. + This field can only be set on policies targeting a parent resource (Gateway). + enum: + - StrategicMerge + - JSONMerge + type: string dns: description: DNS includes dns resolution settings. properties: @@ -3912,6 +3923,11 @@ spec: || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' + - message: defaultChildMergeType can only be used with Gateway targets + rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || + self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, + ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && @@ -10391,29 +10407,6 @@ spec: - StrategicMerge - JSONMerge type: string - policyDefaults: - description: |- - PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - Gateways that use this EnvoyProxy. - properties: - backendTrafficPolicy: - description: BackendTrafficPolicy defines defaults applied to - BackendTrafficPolicy resources. - properties: - mergeExcludeLabel: - description: MergeExcludeLabel, when present on a policy, - opts that policy out of the default MergeType. - type: string - mergeType: - description: |- - MergeType is the mergeType applied to a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - type: object - type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index f8c0732a51..de3f379c1b 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -1068,6 +1068,17 @@ spec: Note that when the suffix is not provided, the value is interpreted as bytes. x-kubernetes-int-or-string: true type: object + defaultChildMergeType: + description: |- + DefaultChildMergeType is the merge strategy applied to child policies (policies targeting + an xRoute under this policy's target) that do not set their own mergeType, so a child + policy merges into this policy instead of replacing it. A child policy can opt out by + setting mergeType to Replace. + This field can only be set on policies targeting a parent resource (Gateway). + enum: + - StrategicMerge + - JSONMerge + type: string dns: description: DNS includes dns resolution settings. properties: @@ -3912,6 +3923,11 @@ spec: || self.targetRefs.all(ref, ref.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' + - message: defaultChildMergeType can only be used with Gateway targets + rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || + self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, + ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, + sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && @@ -10391,29 +10407,6 @@ spec: - StrategicMerge - JSONMerge type: string - policyDefaults: - description: |- - PolicyDefaults defines defaults applied to Envoy Gateway policies attached to - Gateways that use this EnvoyProxy. - properties: - backendTrafficPolicy: - description: BackendTrafficPolicy defines defaults applied to - BackendTrafficPolicy resources. - properties: - mergeExcludeLabel: - description: MergeExcludeLabel, when present on a policy, - opts that policy out of the default MergeType. - type: string - mergeType: - description: |- - MergeType is the mergeType applied to a policy that does not set one, - so a route-level policy merges into its parent instead of replacing it. - enum: - - StrategicMerge - - JSONMerge - type: string - type: object - type: object preserveRouteOrder: description: |- PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API From 72c5161a4a3bc27cb68f6ff49421f50bcd7c73e8 Mon Sep 17 00:00:00 2001 From: Maksim Kuchkovskiy Date: Mon, 20 Jul 2026 16:56:34 +0200 Subject: [PATCH 6/6] feat: restrict defaultChildMergeType to top-level Gateway targets Reject defaultChildMergeType on Listener (sectionName) targets via CEL so the child merge default is declared only at the top-level Gateway. The default that children inherit always comes from the Gateway-level policy Add CEL, unit, and golden test coverage. Signed-off-by: Maksim Kuchkovskiy --- api/v1alpha1/backendtrafficpolicy_types.go | 6 +- ....envoyproxy.io_backendtrafficpolicies.yaml | 12 +- ....envoyproxy.io_backendtrafficpolicies.yaml | 12 +- internal/gatewayapi/backendtrafficpolicy.go | 49 ++- .../backendtrafficpolicy_mergedefault_test.go | 66 +++- ...-default-mergetype-listener-parent.in.yaml | 88 +++++ ...default-mergetype-listener-parent.out.yaml | 315 ++++++++++++++++++ site/content/en/latest/api/extension_types.md | 2 +- .../backendtrafficpolicy_test.go | 19 ++ test/helm/gateway-crds-helm/all.out.yaml | 12 +- test/helm/gateway-crds-helm/e2e.out.yaml | 12 +- .../envoy-gateway-crds.out.yaml | 12 +- 12 files changed, 551 insertions(+), 54 deletions(-) create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.in.yaml create mode 100644 internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.out.yaml diff --git a/api/v1alpha1/backendtrafficpolicy_types.go b/api/v1alpha1/backendtrafficpolicy_types.go index d74f76bf83..90d6c23c3f 100644 --- a/api/v1alpha1/backendtrafficpolicy_types.go +++ b/api/v1alpha1/backendtrafficpolicy_types.go @@ -46,7 +46,7 @@ type BackendTrafficPolicy struct { // +kubebuilder:validation:XValidation:rule="!has(self.compression) || !has(self.compressor)", message="either compression or compressor can be set, not both" // +kubebuilder:validation:XValidation:rule="!has(self.requestBuffer) || !has(self.httpUpgrade) || self.httpUpgrade.size() == 0", message="requestBuffer cannot be used together with httpUpgrade" // +kubebuilder:validation:XValidation:rule="!has(self.admissionControl) || ((!has(self.targetRef) || self.targetRef.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute']) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in ['Gateway', 'HTTPRoute', 'GRPCRoute'])))", message="admissionControl can only be used with HTTPRoute, GRPCRoute, or Gateway targets" -// +kubebuilder:validation:XValidation:rule="!has(self.defaultChildMergeType) || ((!has(self.targetRef) || self.targetRef.kind == 'Gateway') && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == 'Gateway')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind == 'Gateway')))", message="defaultChildMergeType can only be used with Gateway targets" +// +kubebuilder:validation:XValidation:rule="!has(self.defaultChildMergeType) || ((!has(self.targetRef) || (self.targetRef.kind == 'Gateway' && !has(self.targetRef.sectionName))) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == 'Gateway' && !has(ref.sectionName))) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind == 'Gateway')))", message="defaultChildMergeType can only be used with Gateway targets without a sectionName" type BackendTrafficPolicySpec struct { PolicyTargetReferences `json:",inline"` ClusterSettings `json:",inline"` @@ -65,7 +65,9 @@ type BackendTrafficPolicySpec struct { // an xRoute under this policy's target) that do not set their own mergeType, so a child // policy merges into this policy instead of replacing it. A child policy can opt out by // setting mergeType to Replace. - // This field can only be set on policies targeting a parent resource (Gateway). + // This field can only be set on policies targeting an entire Gateway. It is rejected on + // policies targeting a specific Listener (via sectionName), so the default is defined at a + // single top-level parent rather than at multiple intermediate parents. // // +kubebuilder:validation:Enum=StrategicMerge;JSONMerge // +optional diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 4748426a78..9cf4cdf39f 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -519,7 +519,9 @@ spec: an xRoute under this policy's target) that do not set their own mergeType, so a child policy merges into this policy instead of replacing it. A child policy can opt out by setting mergeType to Replace. - This field can only be set on policies targeting a parent resource (Gateway). + This field can only be set on policies targeting an entire Gateway. It is rejected on + policies targeting a specific Listener (via sectionName), so the default is defined at a + single top-level parent rather than at multiple intermediate parents. enum: - StrategicMerge - JSONMerge @@ -3369,10 +3371,12 @@ spec: ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: defaultChildMergeType can only be used with Gateway targets + without a sectionName rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || - self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, - ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, - sel.kind == ''Gateway'')))' + (self.targetRef.kind == ''Gateway'' && !has(self.targetRef.sectionName))) + && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == + ''Gateway'' && !has(ref.sectionName))) && (!has(self.targetSelectors) + || self.targetSelectors.all(sel, sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index a12773620c..7c5ee9a09e 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -518,7 +518,9 @@ spec: an xRoute under this policy's target) that do not set their own mergeType, so a child policy merges into this policy instead of replacing it. A child policy can opt out by setting mergeType to Replace. - This field can only be set on policies targeting a parent resource (Gateway). + This field can only be set on policies targeting an entire Gateway. It is rejected on + policies targeting a specific Listener (via sectionName), so the default is defined at a + single top-level parent rather than at multiple intermediate parents. enum: - StrategicMerge - JSONMerge @@ -3368,10 +3370,12 @@ spec: ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: defaultChildMergeType can only be used with Gateway targets + without a sectionName rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || - self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, - ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, - sel.kind == ''Gateway'')))' + (self.targetRef.kind == ''Gateway'' && !has(self.targetRef.sectionName))) + && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == + ''Gateway'' && !has(ref.sectionName))) && (!has(self.targetSelectors) + || self.targetSelectors.all(sel, sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go index a26f8acf53..897b65be7c 100644 --- a/internal/gatewayapi/backendtrafficpolicy.go +++ b/internal/gatewayapi/backendtrafficpolicy.go @@ -535,9 +535,14 @@ func (t *Translator) processBackendTrafficPolicyForRoute( parentPolicy = listenerPolicy } - // Resolve the effective mergeType: the policy's own value, or the closest - // parent policy's defaultChildMergeType. - mergeType := effectiveMergeType(policy, parentPolicy) + // The default-merge intent comes from the nearest ancestor that declares + // defaultChildMergeType (listener before gateway), independent of which parent + // the child merges into. This keeps a listener-level policy from suppressing the + // gateway-level default. + defaultMergeType := resolveDefaultChildMergeType(listenerPolicy, gwPolicy) + + // Resolve the effective mergeType: the policy's own value, or the resolved default. + mergeType := effectiveMergeType(policy, defaultMergeType) if mergeType == nil || parentPolicy == nil { // No merge for this gateway: apply the policy standalone. if err := t.translateBackendTrafficPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, &gwNN, &listener.Name); err != nil { @@ -1079,25 +1084,39 @@ func (t *Translator) applyTrafficFeatureToRoute(route RouteContext, // effectiveMergeType returns the mergeType to use when merging a route-level policy into its // parent policy: the policy's own value if set (Replace meaning "do not merge"), otherwise the -// parent policy's defaultChildMergeType. -func effectiveMergeType(policy, parentPolicy *egv1a1.BackendTrafficPolicy) *egv1a1.MergeType { +// resolved defaultChildMergeType from the nearest ancestor that declares one. +func effectiveMergeType(policy *egv1a1.BackendTrafficPolicy, defaultMergeType *egv1a1.MergeType) *egv1a1.MergeType { if policy.Spec.MergeType != nil { if *policy.Spec.MergeType == egv1a1.Replace { return nil } return policy.Spec.MergeType } - if parentPolicy == nil || parentPolicy.Spec.DefaultChildMergeType == nil { - return nil - } - // Defense in depth: the CRD enum restricts DefaultChildMergeType to StrategicMerge/JSONMerge. - // Ignore anything that is not a real merge so a stray value can never produce a "merged" - // status while actually replacing the parent. - if *parentPolicy.Spec.DefaultChildMergeType != egv1a1.StrategicMerge && - *parentPolicy.Spec.DefaultChildMergeType != egv1a1.JSONMerge { - return nil + return defaultMergeType +} + +// resolveDefaultChildMergeType returns the defaultChildMergeType declared by the nearest ancestor +// policy, checking from the closest parent to the furthest (listener before gateway). CEL restricts +// defaultChildMergeType to policies targeting an entire Gateway, so the merge target for a child +// under a listener may be the listener-level policy while the default intent is declared on the +// gateway-level policy. Resolving the default independently of the merge target keeps a +// listener-level policy from suppressing the gateway-level default. +func resolveDefaultChildMergeType(ancestors ...*egv1a1.BackendTrafficPolicy) *egv1a1.MergeType { + for _, p := range ancestors { + if p == nil || p.Spec.DefaultChildMergeType == nil { + continue + } + mergeType := p.Spec.DefaultChildMergeType + // Defense in depth: the CRD enum restricts DefaultChildMergeType to StrategicMerge/JSONMerge. + // The nearest declarer wins, so if its value is not a real merge, return nil rather than + // falling through, ensuring a stray value can never produce a "merged" status while actually + // replacing the parent. + if *mergeType != egv1a1.StrategicMerge && *mergeType != egv1a1.JSONMerge { + return nil + } + return mergeType } - return parentPolicy.Spec.DefaultChildMergeType + return nil } // anyParentPolicyMergeDefault reports whether any parent policy (gateway- or listener-level) of diff --git a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go index 54871f83a4..a793fc14e4 100644 --- a/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go +++ b/internal/gatewayapi/backendtrafficpolicy_mergedefault_test.go @@ -28,30 +28,64 @@ func TestEffectiveMergeType(t *testing.T) { Spec: egv1a1.BackendTrafficPolicySpec{MergeType: mt}, } } - parent := func(defaultMT *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { - return &egv1a1.BackendTrafficPolicy{ - ObjectMeta: metav1.ObjectMeta{Namespace: "app", Name: "parent"}, - Spec: egv1a1.BackendTrafficPolicySpec{DefaultChildMergeType: defaultMT}, - } + + tests := []struct { + name string + pol *egv1a1.BackendTrafficPolicy + defaultMT *egv1a1.MergeType + want *egv1a1.MergeType + }{ + {"explicit value wins over default", child(&jsonMerge), &strategic, &jsonMerge}, + {"explicit replace opts out of default", child(&replace), &strategic, nil}, + {"default applied when unset", child(nil), &strategic, &strategic}, + {"no default stays nil", child(nil), nil, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := effectiveMergeType(tt.pol, tt.defaultMT) + if tt.want == nil { + assert.Nil(t, got) + return + } + assert.NotNil(t, got) + assert.Equal(t, *tt.want, *got) + }) + } +} + +// TestResolveDefaultChildMergeType covers resolveDefaultChildMergeType, which returns the +// defaultChildMergeType declared by the nearest ancestor policy (closest first). This decouples +// the default-merge intent (which, after CEL restricts defaultChildMergeType to Gateway targets, +// is only declared on the Gateway-level policy) from the merge target (the closest parent config). +func TestResolveDefaultChildMergeType(t *testing.T) { + strategic := egv1a1.StrategicMerge + jsonMerge := egv1a1.JSONMerge + replace := egv1a1.Replace + + withDefault := func(mt *egv1a1.MergeType) *egv1a1.BackendTrafficPolicy { + return &egv1a1.BackendTrafficPolicy{Spec: egv1a1.BackendTrafficPolicySpec{DefaultChildMergeType: mt}} } tests := []struct { - name string - pol *egv1a1.BackendTrafficPolicy - parent *egv1a1.BackendTrafficPolicy - want *egv1a1.MergeType + name string + ancestors []*egv1a1.BackendTrafficPolicy + want *egv1a1.MergeType }{ - {"explicit value wins over parent default", child(&jsonMerge), parent(&strategic), &jsonMerge}, - {"explicit replace opts out of parent default", child(&replace), parent(&strategic), nil}, - {"parent default applied when unset", child(nil), parent(&strategic), &strategic}, - {"no parent policy stays nil", child(nil), nil, nil}, - {"parent without default stays nil", child(nil), parent(nil), nil}, - {"invalid parent default is ignored", child(nil), parent(&replace), nil}, + {"no ancestors", nil, nil}, + {"nil ancestor", []*egv1a1.BackendTrafficPolicy{nil}, nil}, + {"single ancestor with default", []*egv1a1.BackendTrafficPolicy{withDefault(&strategic)}, &strategic}, + {"single ancestor without default", []*egv1a1.BackendTrafficPolicy{withDefault(nil)}, nil}, + // Reading B: the closest parent (listener) declares no default, so resolution falls through + // to the gateway-level policy that does. + {"closest without default falls through", []*egv1a1.BackendTrafficPolicy{withDefault(nil), withDefault(&strategic)}, &strategic}, + {"closest declarer wins", []*egv1a1.BackendTrafficPolicy{withDefault(&jsonMerge), withDefault(&strategic)}, &jsonMerge}, + {"invalid nearest default is ignored", []*egv1a1.BackendTrafficPolicy{withDefault(&replace), withDefault(&strategic)}, nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := effectiveMergeType(tt.pol, tt.parent) + got := resolveDefaultChildMergeType(tt.ancestors...) if tt.want == nil { assert.Nil(t, got) return diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.in.yaml new file mode 100644 index 0000000000..0f92993909 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.in.yaml @@ -0,0 +1,88 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backendTrafficPolicies: + # Gateway-level policy declares defaultChildMergeType. It is the only place the default can be + # declared (CEL rejects it on listener targets). + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + defaultChildMergeType: StrategicMerge + timeout: + tcp: + connectTimeout: 15s + httpUpgrade: + - type: websocket + # Listener-level policy is the closest parent config a child merges into. It sets no + # defaultChildMergeType, so under "closest declared default" resolution the child still inherits + # the gateway-level default and merges into this policy. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: envoy-gateway + name: policy-for-listener + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: http + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + httpUpgrade: + - type: websocket + # Route-level policy with no mergeType: merges into the listener-level policy using the + # gateway-level defaultChildMergeType. + - apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + connection: + bufferLimit: 100M diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.out.yaml new file mode 100644 index 0000000000..c5c1791b24 --- /dev/null +++ b/internal/gatewayapi/testdata/backendtrafficpolicy-default-mergetype-listener-parent.out.yaml @@ -0,0 +1,315 @@ +backendTrafficPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-route + namespace: default + spec: + connection: + bufferLimit: 100M + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Merged with policy envoy-gateway/policy-for-listener + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-listener + namespace: envoy-gateway + spec: + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: http + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: BackendTrafficPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + defaultChildMergeType: StrategicMerge + httpUpgrade: + - type: websocket + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + timeout: + tcp: + connectTimeout: 15s + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: spec.targetRef is deprecated, use spec.targetRefs instead + reason: DeprecatedField + status: "True" + type: Warning + - lastTransitionTime: null + message: 'This policy is being merged by other backendTrafficPolicies for + these routes: [default/httproute-1]' + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: 'This policy is being overridden by other backendTrafficPolicies + for these listeners: [http]' + reason: Overridden + status: "True" + type: Overridden + controllerName: gateway.envoyproxy.io/gatewayclass-controller +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + ownerReference: + kind: GatewayClass + name: envoy-gateway-class + name: envoy-gateway/gateway-1 + namespace: envoy-gateway-system +xdsIR: + envoy-gateway/gateway-1: + accessLog: + json: + - path: /dev/stdout + globalResources: + proxyServiceCluster: + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + settings: + - addressType: IP + endpoints: + - host: 7.6.5.4 + port: 8080 + zone: zone1 + metadata: + kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + protocol: TCP + http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - '*' + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + metadata: + kind: Service + name: service-1 + namespace: default + sectionName: "8080" + name: httproute/default/httproute-1/rule/0/backend/0 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + policies: + - kind: BackendTrafficPolicy + name: policy-for-route + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + traffic: + backendConnection: + bufferLimit: 100000000 + httpUpgrade: + - type: websocket + timeout: + http: + connectionIdleTimeout: 16s + maxConnectionDuration: 17s + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 28c290e0b3..ca0036c04c 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -584,7 +584,7 @@ _Appears in:_ | `dns` | _[DNS](#dns)_ | false | | DNS includes dns resolution settings. | | `http2` | _[HTTP2Settings](#http2settings)_ | false | | HTTP2 provides HTTP/2 configuration for backend connections. | | `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType determines how this configuration is merged with existing BackendTrafficPolicy
configurations targeting a parent resource. When set, this configuration will be merged
into a parent BackendTrafficPolicy (i.e. the one targeting a Gateway or Listener).
This field cannot be set when targeting a parent resource (Gateway).
If unset, no merging occurs, and only the most specific configuration takes effect. | -| `defaultChildMergeType` | _[MergeType](#mergetype)_ | false | | DefaultChildMergeType is the merge strategy applied to child policies (policies targeting
an xRoute under this policy's target) that do not set their own mergeType, so a child
policy merges into this policy instead of replacing it. A child policy can opt out by
setting mergeType to Replace.
This field can only be set on policies targeting a parent resource (Gateway). | +| `defaultChildMergeType` | _[MergeType](#mergetype)_ | false | | DefaultChildMergeType is the merge strategy applied to child policies (policies targeting
an xRoute under this policy's target) that do not set their own mergeType, so a child
policy merges into this policy instead of replacing it. A child policy can opt out by
setting mergeType to Replace.
This field can only be set on policies targeting an entire Gateway. It is rejected on
policies targeting a specific Listener (via sectionName), so the default is defined at a
single top-level parent rather than at multiple intermediate parents. | | `rateLimit` | _[RateLimitSpec](#ratelimitspec)_ | false | | RateLimit allows the user to limit the number of incoming requests
to a predefined value based on attributes within the traffic flow. | | `bandwidthLimit` | _[BandwidthLimitSpec](#bandwidthlimitspec)_ | false | | BandwidthLimit allows the user to limit the bandwidth of traffic
sent to and received from the backend. | | `faultInjection` | _[FaultInjection](#faultinjection)_ | false | | FaultInjection defines the fault injection policy to be applied. This configuration can be used to
inject delays and abort requests to mimic failure scenarios such as service failures and overloads | diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go index 30699f0c50..52f86fbcde 100644 --- a/test/cel-validation/backendtrafficpolicy_test.go +++ b/test/cel-validation/backendtrafficpolicy_test.go @@ -3744,6 +3744,25 @@ func TestBackendTrafficPolicyTarget(t *testing.T) { }, wantErrors: []string{}, }, + { + desc: "defaultChildMergeType is rejected on gateway listener (sectionName) targets", + mutate: func(btp *egv1a1.BackendTrafficPolicy) { + btp.Spec = egv1a1.BackendTrafficPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group("gateway.networking.k8s.io"), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + SectionName: §ionName, + }, + }, + DefaultChildMergeType: new(egv1a1.StrategicMerge), + } + }, + wantErrors: []string{"defaultChildMergeType can only be used with Gateway targets"}, + }, { desc: "defaultChildMergeType is rejected on route targets", mutate: func(btp *egv1a1.BackendTrafficPolicy) { diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 04d2cc537c..2455325675 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -25136,7 +25136,9 @@ spec: an xRoute under this policy's target) that do not set their own mergeType, so a child policy merges into this policy instead of replacing it. A child policy can opt out by setting mergeType to Replace. - This field can only be set on policies targeting a parent resource (Gateway). + This field can only be set on policies targeting an entire Gateway. It is rejected on + policies targeting a specific Listener (via sectionName), so the default is defined at a + single top-level parent rather than at multiple intermediate parents. enum: - StrategicMerge - JSONMerge @@ -27986,10 +27988,12 @@ spec: ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: defaultChildMergeType can only be used with Gateway targets + without a sectionName rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || - self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, - ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, - sel.kind == ''Gateway'')))' + (self.targetRef.kind == ''Gateway'' && !has(self.targetRef.sectionName))) + && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == + ''Gateway'' && !has(ref.sectionName))) && (!has(self.targetSelectors) + || self.targetSelectors.all(sel, sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 5e2e6bd3ce..989d0fdae2 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -1074,7 +1074,9 @@ spec: an xRoute under this policy's target) that do not set their own mergeType, so a child policy merges into this policy instead of replacing it. A child policy can opt out by setting mergeType to Replace. - This field can only be set on policies targeting a parent resource (Gateway). + This field can only be set on policies targeting an entire Gateway. It is rejected on + policies targeting a specific Listener (via sectionName), so the default is defined at a + single top-level parent rather than at multiple intermediate parents. enum: - StrategicMerge - JSONMerge @@ -3924,10 +3926,12 @@ spec: ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: defaultChildMergeType can only be used with Gateway targets + without a sectionName rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || - self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, - ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, - sel.kind == ''Gateway'')))' + (self.targetRef.kind == ''Gateway'' && !has(self.targetRef.sectionName))) + && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == + ''Gateway'' && !has(ref.sectionName))) && (!has(self.targetSelectors) + || self.targetSelectors.all(sel, sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) && diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index de3f379c1b..bd962bc7cc 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -1074,7 +1074,9 @@ spec: an xRoute under this policy's target) that do not set their own mergeType, so a child policy merges into this policy instead of replacing it. A child policy can opt out by setting mergeType to Replace. - This field can only be set on policies targeting a parent resource (Gateway). + This field can only be set on policies targeting an entire Gateway. It is rejected on + policies targeting a specific Listener (via sectionName), so the default is defined at a + single top-level parent rather than at multiple intermediate parents. enum: - StrategicMerge - JSONMerge @@ -3924,10 +3926,12 @@ spec: ''GRPCRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in [''Gateway'', ''HTTPRoute'', ''GRPCRoute''])))' - message: defaultChildMergeType can only be used with Gateway targets + without a sectionName rule: '!has(self.defaultChildMergeType) || ((!has(self.targetRef) || - self.targetRef.kind == ''Gateway'') && (!has(self.targetRefs) || self.targetRefs.all(ref, - ref.kind == ''Gateway'')) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, - sel.kind == ''Gateway'')))' + (self.targetRef.kind == ''Gateway'' && !has(self.targetRef.sectionName))) + && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind == + ''Gateway'' && !has(ref.sectionName))) && (!has(self.targetSelectors) + || self.targetSelectors.all(sel, sel.kind == ''Gateway'')))' - message: predictivePercent in preconnect policy only works with RoundRobin or Random load balancers rule: '!((has(self.connection) && has(self.connection.preconnect) &&