Skip to content
13 changes: 13 additions & 0 deletions api/v1alpha1/backendtrafficpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.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"`
Expand All @@ -60,6 +61,18 @@ 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 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
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
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,19 @@ 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 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
type: string
dns:
description: DNS includes dns resolution settings.
properties:
Expand Down Expand Up @@ -3399,6 +3412,13 @@ 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
without a sectionName
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: predictivePercent in preconnect policy only works with RoundRobin
or Random load balancers
rule: '!((has(self.connection) && has(self.connection.preconnect) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,19 @@ 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 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
type: string
dns:
description: DNS includes dns resolution settings.
properties:
Expand Down Expand Up @@ -3398,6 +3411,13 @@ 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
without a sectionName
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: predictivePercent in preconnect policy only works with RoundRobin
or Random load balancers
rule: '!((has(self.connection) && has(self.connection.preconnect) &&
Expand Down
120 changes: 111 additions & 9 deletions internal/gatewayapi/backendtrafficpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,10 @@ func (t *Translator) processBackendTrafficPolicyForRoute(
return
}

if policy.Spec.MergeType == nil {
// 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,
Expand Down Expand Up @@ -522,8 +525,24 @@ func (t *Translator) processBackendTrafficPolicyForRoute(
NamespacedName: gwNN,
}
gwPolicy := gatewayPolicyMap[gwMapKey]
if gwPolicy == nil && listenerPolicy == nil {
// not found, fall back to the current policy

// 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
}

// 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 {
status.SetConditionForPolicyAncestor(&policy.Status,
&ancestorRef,
Expand All @@ -537,13 +556,16 @@ 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
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,
Expand Down Expand Up @@ -836,7 +858,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
Expand Down Expand Up @@ -897,16 +919,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 errs
}

// listenerBelongsToGateway reports whether an IR listener (named "<gw-ns>/<gw-name>/<section>...")
// 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 := ""
Expand All @@ -916,6 +951,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
Expand Down Expand Up @@ -944,6 +984,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
Expand All @@ -966,6 +1009,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
Expand Down Expand Up @@ -1034,6 +1080,62 @@ 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
// 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
}
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 nil
}

// 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 {
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
}
}
}
}
return false
}

// mergeBackendTrafficPolicy merges route policy into gateway policy, returning the merged
// policy and the per-field owners used to resolve references against the contributing
// policy's namespace.
Expand Down
Loading