From a31dda10c0140fd1cd357999821213cc63df68cc Mon Sep 17 00:00:00 2001 From: kkk777-7 Date: Wed, 20 May 2026 21:00:05 +0900 Subject: [PATCH] feat: mergeType in envoy extension policy Signed-off-by: kkk777-7 --- api/v1alpha1/envoyextensionypolicy_types.go | 10 + api/v1alpha1/zz_generated.deepcopy.go | 5 + ....envoyproxy.io_envoyextensionpolicies.yaml | 11 + ....envoyproxy.io_envoyextensionpolicies.yaml | 11 + internal/gatewayapi/envoyextensionpolicy.go | 375 +++++++++++++++--- .../gatewayapi/envoyextensionpolicy_test.go | 110 +++++ internal/gatewayapi/helpers.go | 59 ++- internal/gatewayapi/securitypolicy.go | 23 -- ...policy-with-merge-needs-fieldowner.in.yaml | 151 +++++++ ...olicy-with-merge-needs-fieldowner.out.yaml | 342 ++++++++++++++++ internal/utils/merge_test.go | 2 + .../testdata/envoyextensionpolicy_all.in.yaml | 33 ++ ...nvoyextensionpolicy_all.jsonmerge.out.yaml | 37 ++ .../envoyextensionpolicy_all.patch.yaml | 23 ++ ...xtensionpolicy_all.strategicmerge.out.yaml | 37 ++ release-notes/current.yaml | 1 + site/content/en/latest/api/extension_types.md | 2 + .../testdata/envoyextensionpolicy-merged.yaml | 85 ++++ test/e2e/tests/envoyextensionpolicy_merged.go | 103 +++++ test/e2e/tests/utils.go | 23 ++ test/helm/gateway-crds-helm/all.out.yaml | 11 + test/helm/gateway-crds-helm/e2e.out.yaml | 11 + .../envoy-gateway-crds.out.yaml | 11 + 23 files changed, 1361 insertions(+), 115 deletions(-) create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.in.yaml create mode 100644 internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.out.yaml create mode 100644 internal/utils/testdata/envoyextensionpolicy_all.in.yaml create mode 100644 internal/utils/testdata/envoyextensionpolicy_all.jsonmerge.out.yaml create mode 100644 internal/utils/testdata/envoyextensionpolicy_all.patch.yaml create mode 100644 internal/utils/testdata/envoyextensionpolicy_all.strategicmerge.out.yaml create mode 100644 test/e2e/testdata/envoyextensionpolicy-merged.yaml create mode 100644 test/e2e/tests/envoyextensionpolicy_merged.go diff --git a/api/v1alpha1/envoyextensionypolicy_types.go b/api/v1alpha1/envoyextensionypolicy_types.go index 4fadb982f3..a85b2ade15 100644 --- a/api/v1alpha1/envoyextensionypolicy_types.go +++ b/api/v1alpha1/envoyextensionypolicy_types.go @@ -44,6 +44,16 @@ type EnvoyExtensionPolicy struct { type EnvoyExtensionPolicySpec struct { PolicyTargetReferences `json:",inline"` + // MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + // configurations targeting a parent resource. When set, this configuration will be merged + // into a parent EnvoyExtensionPolicy (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. + // + // +kubebuilder:validation:XValidation:rule="self != 'Replace'",message="Replace is not a valid MergeType for EnvoyExtensionPolicy" + // +optional + MergeType *MergeType `json:"mergeType,omitempty"` + // Wasm is a list of Wasm extensions to be loaded by the Gateway. // Order matters, as the extensions will be loaded in the order they are // defined in this list. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ff395861b1..903e0df647 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2300,6 +2300,11 @@ func (in *EnvoyExtensionPolicyList) DeepCopyObject() runtime.Object { func (in *EnvoyExtensionPolicySpec) DeepCopyInto(out *EnvoyExtensionPolicySpec) { *out = *in in.PolicyTargetReferences.DeepCopyInto(&out.PolicyTargetReferences) + if in.MergeType != nil { + in, out := &in.MergeType, &out.MergeType + *out = new(MergeType) + **out = **in + } if in.Wasm != nil { in, out := &in.Wasm, &out.Wasm *out = make([]Wasm, len(*in)) diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index 8b24dac2fc..390f443f76 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -1659,6 +1659,17 @@ spec: || (self.type == 'ValueRef' && !has(self.inline) && has(self.valueRef)) maxItems: 16 type: array + mergeType: + description: |- + MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + configurations targeting a parent resource. When set, this configuration will be merged + into a parent EnvoyExtensionPolicy (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. + type: string + x-kubernetes-validations: + - message: Replace is not a valid MergeType for EnvoyExtensionPolicy + rule: self != 'Replace' targetRef: description: |- TargetRef is the name of the resource this policy is being attached to. diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index 51964f1d6a..68d92bb614 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -1658,6 +1658,17 @@ spec: || (self.type == 'ValueRef' && !has(self.inline) && has(self.valueRef)) maxItems: 16 type: array + mergeType: + description: |- + MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + configurations targeting a parent resource. When set, this configuration will be merged + into a parent EnvoyExtensionPolicy (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. + type: string + x-kubernetes-validations: + - message: Replace is not a valid MergeType for EnvoyExtensionPolicy + rule: self != 'Replace' targetRef: description: |- TargetRef is the name of the resource this policy is being attached to. diff --git a/internal/gatewayapi/envoyextensionpolicy.go b/internal/gatewayapi/envoyextensionpolicy.go index 83ed3b96ed..275e45c8cc 100644 --- a/internal/gatewayapi/envoyextensionpolicy.go +++ b/internal/gatewayapi/envoyextensionpolicy.go @@ -21,6 +21,7 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/gatewayapi/luavalidator" @@ -82,8 +83,12 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( var res []*egv1a1.EnvoyExtensionPolicy // EnvoyExtensionPolicies are already sorted by the provider layer + routeMapSize := len(routes) + gatewayMapSize := len(gateways) + policyMapSize := len(envoyExtensionPolicies) + // First build a map out of the routes and gateways for faster lookup since users might have thousands of routes or more. - routeMap := map[policyTargetRouteKey]*policyRouteTargetContext{} + routeMap := make(map[policyTargetRouteKey]*policyRouteTargetContext, routeMapSize) for _, route := range routes { key := policyTargetRouteKey{ Kind: string(route.GetRouteType()), @@ -93,7 +98,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( routeMap[key] = &policyRouteTargetContext{RouteContext: route} } - gatewayMap := map[types.NamespacedName]*policyGatewayTargetContext{} + gatewayMap := make(map[types.NamespacedName]*policyGatewayTargetContext, gatewayMapSize) for _, gw := range gateways { key := utils.NamespacedName(gw) gatewayMap[key] = &policyGatewayTargetContext{GatewayContext: gw} @@ -101,11 +106,23 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( // Map of Gateway to the routes attached to it. // The routes are grouped by sectionNames of their targetRefs. - gatewayRouteMap := make(map[string]map[string]sets.Set[string]) + gatewayRouteMap := &GatewayPolicyRouteMap{ + Routes: make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize), + SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize), + } policyCopies := envoyExtensionPolicyCopiesWithStatusDeepCopy(envoyExtensionPolicies) - handledPolicies := make(map[types.NamespacedName]*egv1a1.EnvoyExtensionPolicy) + handledPolicies := make(map[types.NamespacedName]*egv1a1.EnvoyExtensionPolicy, policyMapSize) + + // Map of attached Policy to Gateway. Used for policy merge process. + gatewayPolicyMap := make(map[NamespacedNameWithSection]*egv1a1.EnvoyExtensionPolicy, gatewayMapSize) + + // Map of Gateway to the routes merged to it. + gatewayPolicyMerged := &GatewayPolicyRouteMap{ + Routes: make(map[NamespacedNameWithSection]sets.Set[string], gatewayMapSize), + SectionIndex: make(map[types.NamespacedName]sets.Set[string], gatewayMapSize), + } // Translate // 1. First translate Policies targeting RouteRules @@ -113,6 +130,9 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( // 3. Then translate Policies targeting Listeners // 4. Finally, the policies targeting Gateways + // Build gateway policy maps, which are needed when processing the policies targeting xRoutes. + t.buildGatewayEnvoyExtensionPolicyMap(envoyExtensionPolicies, gateways, gatewayMap, gatewayPolicyMap, resources.ReferenceGrants) + // Process the policies targeting RouteRules for i, currPolicy := range envoyExtensionPolicies { policyName := utils.NamespacedName(currPolicy) @@ -128,7 +148,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( } t.processEnvoyExtensionPolicyForRoute(resources, xdsIR, - routeMap, gatewayRouteMap, policy, currTarget) + routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) } } } @@ -154,7 +174,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( } t.processEnvoyExtensionPolicyForRoute(resources, xdsIR, - routeMap, gatewayRouteMap, policy, currTarget) + routeMap, gatewayRouteMap, gatewayPolicyMerged, gatewayPolicyMap, policy, currTarget) } } } @@ -174,7 +194,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( } t.processEnvoyExtensionPolicyForGateway(resources, xdsIR, - gatewayMap, gatewayRouteMap, policy, currTarget) + gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) } } } @@ -200,7 +220,7 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( } t.processEnvoyExtensionPolicyForGateway(resources, xdsIR, - gatewayMap, gatewayRouteMap, policy, currTarget) + gatewayMap, gatewayRouteMap, gatewayPolicyMerged, policy, currTarget) } } } @@ -214,17 +234,71 @@ func (t *Translator) ProcessEnvoyExtensionPolicies( return res } +func (t *Translator) buildGatewayEnvoyExtensionPolicyMap( + envoyExtensionPolicies []*egv1a1.EnvoyExtensionPolicy, + gateways []*GatewayContext, + gatewayMap map[types.NamespacedName]*policyGatewayTargetContext, + gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.EnvoyExtensionPolicy, + referenceGrants []*gwapiv1b1.ReferenceGrant, +) { + for _, currPolicy := range envoyExtensionPolicies { + targetRefs := resolvePolicyTargets( + currPolicy.Spec.PolicyTargetReferences, + gateways, + referenceGrants, + egv1a1.GroupName, + egv1a1.KindEnvoyExtensionPolicy, + currPolicy.Namespace, + t.GetNamespace) + for _, currTarget := range targetRefs { + if currTarget.Kind == resource.KindGateway { + // Check if the gateway exists + key := types.NamespacedName{ + Name: string(currTarget.Name), + Namespace: string(currTarget.Namespace), + } + gateway, ok := gatewayMap[key] + if !ok { + continue + } + + // Check if the specified listener exists when sectionName is set + if currTarget.SectionName != nil { + if err := validateGatewayListenerSectionName( + *currTarget.SectionName, + key, + gateway.listeners, + ); err != nil { + continue + } + } + + mapKey := NamespacedNameWithSection{ + NamespacedName: key, + SectionName: ptr.Deref(currTarget.SectionName, ""), + } + // Only store the first policy for this Gateway/Listener - conflicts are handled elsewhere + if _, ok := gatewayPolicyMap[mapKey]; ok { + continue + } + gatewayPolicyMap[mapKey] = currPolicy + } + } + } +} + func (t *Translator) processEnvoyExtensionPolicyForRoute( resources *resource.Resources, xdsIR resource.XdsIRMap, routeMap map[policyTargetRouteKey]*policyRouteTargetContext, - gatewayRouteMap map[string]map[string]sets.Set[string], + gatewayRouteMap *GatewayPolicyRouteMap, + gatewayPolicyMerged *GatewayPolicyRouteMap, + gatewayPolicyMap map[NamespacedNameWithSection]*egv1a1.EnvoyExtensionPolicy, policy *egv1a1.EnvoyExtensionPolicy, currTarget policyTargetReferenceWithSectionName, ) { var ( targetedRoute RouteContext - ancestorRefs []*gwapiv1.ParentReference resolveErr *status.PolicyResolveError ) @@ -241,34 +315,42 @@ func (t *Translator) processEnvoyExtensionPolicyForRoute( // gatewayRouteMap and ancestor list, which will be used to check // policy overrides and populate its ancestor status. parentRefs := GetManagedParentReferences(targetedRoute) + ancestorRefs := make([]*gwapiv1.ParentReference, 0, len(parentRefs)) + parentRefCtxs := make([]*RouteParentContext, 0, len(parentRefs)) for _, p := range parentRefs { if p.Kind == nil || *p.Kind == resource.KindGateway { namespace := targetedRoute.GetNamespace() if p.Namespace != nil { namespace = string(*p.Namespace) } - gwNN := types.NamespacedName{ - Namespace: namespace, - Name: string(p.Name), + mapKey := NamespacedNameWithSection{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: string(p.Name), + }, + SectionName: ptr.Deref(p.SectionName, ""), } - key := gwNN.String() - if _, ok := gatewayRouteMap[key]; !ok { - gatewayRouteMap[key] = make(map[string]sets.Set[string]) - } - listenerRouteMap := gatewayRouteMap[key] - sectionName := "" - if p.SectionName != nil { - sectionName = string(*p.SectionName) + if _, ok := gatewayRouteMap.Routes[mapKey]; !ok { + gatewayRouteMap.Routes[mapKey] = make(sets.Set[string]) } - if _, ok := listenerRouteMap[sectionName]; !ok { - listenerRouteMap[sectionName] = make(sets.Set[string]) + gatewayRouteMap.Routes[mapKey].Insert(utils.NamespacedName(targetedRoute).String()) + + // Register section name to Gateway index for efficient lookup when retrieving overridden and merged targets + if _, ok := gatewayRouteMap.SectionIndex[mapKey.NamespacedName]; !ok { + gatewayRouteMap.SectionIndex[mapKey.NamespacedName] = make(sets.Set[string]) } - listenerRouteMap[sectionName].Insert(utils.NamespacedName(targetedRoute).String()) + gatewayRouteMap.SectionIndex[mapKey.NamespacedName].Insert(string(mapKey.SectionName)) // Do need a section name since the policy is targeting to a route - ancestorRef := getAncestorRefForPolicy(gwNN, p.SectionName) + ancestorRef := getAncestorRefForPolicy(mapKey.NamespacedName, p.SectionName) ancestorRefs = append(ancestorRefs, &ancestorRef) + + // Only process parentRefs that were handled by this translator + // (skip those referencing Gateways with different GatewayClasses) + if parentRefCtx := targetedRoute.GetRouteParentContext(p); parentRefCtx != nil { + parentRefCtxs = append(parentRefCtxs, parentRefCtx) + } } } @@ -283,14 +365,106 @@ func (t *Translator) processEnvoyExtensionPolicyForRoute( return } - // Set conditions for translation error if it got any - if err := t.translateEnvoyExtensionPolicyForRoute(policy, targetedRoute, currTarget, xdsIR, resources); err != nil { - status.SetTranslationErrorForPolicyAncestors(&policy.Status, - ancestorRefs, - t.GatewayControllerName, - policy.Generation, - status.Error2ConditionMsg(err), - ) + // Check if merging is enabled + if policy.Spec.MergeType == nil { + if err := t.translateEnvoyExtensionPolicyForRoute(policy, &envoyExtensionPolicyOwners{}, targetedRoute, currTarget, xdsIR, resources, nil, nil); err != nil { + status.SetTranslationErrorForPolicyAncestors(&policy.Status, + ancestorRefs, + t.GatewayControllerName, + policy.Generation, + status.Error2ConditionMsg(err), + ) + } + } else { + // Merging enabled - merge with parent policies + for _, parentRefCtx := range parentRefCtxs { + for _, listener := range parentRefCtx.listeners { + gwNN := utils.NamespacedName(listener.gateway.Gateway) + ancestorRef := getAncestorRefForPolicy(gwNN, &listener.Name) + + // Find Gateway listener level policy + listenerMapKey := NamespacedNameWithSection{ + NamespacedName: gwNN, + SectionName: listener.Name, + } + listenerPolicy := gatewayPolicyMap[listenerMapKey] + + // Find Gateway level policy + gwMapKey := NamespacedNameWithSection{ + NamespacedName: gwNN, + } + gwPolicy := gatewayPolicyMap[gwMapKey] + + if gwPolicy == nil && listenerPolicy == nil { + // No parent policy found, fall back to current policy + if err := t.translateEnvoyExtensionPolicyForRoute(policy, &envoyExtensionPolicyOwners{}, targetedRoute, currTarget, xdsIR, resources, &gwNN, &listener.Name); err != nil { + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse, + egv1a1.PolicyReasonInvalid, + status.Error2ConditionMsg(err), + policy.Generation, + ) + } + continue + } + + parentPolicy := gwPolicy + if listenerPolicy != nil { + parentPolicy = listenerPolicy + } + + // Merge with parent policy + mergedPolicy, owners, err := mergeEnvoyExtensionPolicy(policy, parentPolicy) + if err != nil { + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse, + egv1a1.PolicyReasonInvalid, + fmt.Sprintf("error merging policies: %v", err), + policy.Generation, + ) + continue + } + + // Apply merged policy + if err := t.translateEnvoyExtensionPolicyForRoute(mergedPolicy, owners, targetedRoute, currTarget, xdsIR, resources, &gwNN, &listener.Name); err != nil { + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + gwapiv1.PolicyConditionAccepted, metav1.ConditionFalse, + egv1a1.PolicyReasonInvalid, + status.Error2ConditionMsg(err), + policy.Generation, + ) + continue + } + + // Record the merged routes for gateway + if _, ok := gatewayPolicyMerged.Routes[listenerMapKey]; !ok { + gatewayPolicyMerged.Routes[listenerMapKey] = make(sets.Set[string]) + } + gatewayPolicyMerged.Routes[listenerMapKey].Insert(utils.NamespacedName(targetedRoute).String()) + + // Register section name to Gateway index + if _, ok := gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName]; !ok { + gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName] = make(sets.Set[string]) + } + gatewayPolicyMerged.SectionIndex[listenerMapKey.NamespacedName].Insert(string(listenerMapKey.SectionName)) + + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + egv1a1.PolicyConditionMerged, + metav1.ConditionTrue, + egv1a1.PolicyReasonMerged, + fmt.Sprintf("Merged with policy %s/%s", parentPolicy.Namespace, parentPolicy.Name), + policy.Generation, + ) + } + } } // Set Accepted condition if it is unset @@ -302,6 +476,11 @@ func (t *Translator) processEnvoyExtensionPolicyForRoute( } // Check if this policy is overridden by other policies targeting at route rule levels + // If policy target is route rule, we can skip the check + if currTarget.SectionName != nil { + return + } + key := policyTargetRouteKey{ Kind: string(currTarget.Kind), Name: string(currTarget.Name), @@ -325,7 +504,8 @@ func (t *Translator) processEnvoyExtensionPolicyForGateway( resources *resource.Resources, xdsIR resource.XdsIRMap, gatewayMap map[types.NamespacedName]*policyGatewayTargetContext, - gatewayRouteMap map[string]map[string]sets.Set[string], + gatewayRouteMap *GatewayPolicyRouteMap, + gatewayPolicyMergedMap *GatewayPolicyRouteMap, policy *egv1a1.EnvoyExtensionPolicy, currTarget policyTargetReferenceWithSectionName, ) { @@ -377,17 +557,28 @@ func (t *Translator) processEnvoyExtensionPolicyForGateway( status.SetDeprecatedFieldsWarningForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation, deprecatedFields) } - // Check if this policy is overridden by other policies targeting at route rule, route and listener levels - overriddenTargetsMessage := getOverriddenTargetsMessageForGateway( - gatewayMap[gatewayNN], gatewayRouteMap[gatewayNN.String()], currTarget.SectionName) - if overriddenTargetsMessage != "" { + overriddenMessage, mergedMessage := getOverriddenAndMergedTargetsMessageForGateway( + gatewayMap[gatewayNN], gatewayRouteMap, gatewayPolicyMergedMap, currTarget.SectionName) + + if mergedMessage != "" { + status.SetConditionForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + egv1a1.PolicyConditionMerged, + metav1.ConditionTrue, + egv1a1.PolicyReasonMerged, + "This policy is being merged by other envoyExtensionPolicies for "+mergedMessage, + policy.Generation, + ) + } + if overriddenMessage != "" { status.SetConditionForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, egv1a1.PolicyConditionOverridden, metav1.ConditionTrue, egv1a1.PolicyReasonOverridden, - "This policy is being overridden by other envoyExtensionPolicies for "+overriddenTargetsMessage, + "This policy is being overridden by other envoyExtensionPolicies for "+overriddenMessage, policy.Generation, ) } @@ -514,10 +705,13 @@ func resolveEnvoyExtensionPolicyRouteTargetRef( func (t *Translator) translateEnvoyExtensionPolicyForRoute( policy *egv1a1.EnvoyExtensionPolicy, + owners *envoyExtensionPolicyOwners, route RouteContext, target policyTargetReferenceWithSectionName, xdsIR resource.XdsIRMap, resources *resource.Resources, + policyTargetGateway *types.NamespacedName, + policyTargetListener *gwapiv1.SectionName, ) error { var ( wasms []ir.Wasm @@ -527,7 +721,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( errs error ) - if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources); wasmError != nil { + if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, owners, resources); wasmError != nil { wasmError = perr.WithMessage(wasmError, "Wasm") errs = errors.Join(errs, wasmError) } @@ -548,25 +742,41 @@ func (t *Translator) translateEnvoyExtensionPolicyForRoute( continue } - if luas, luaError = t.buildLuas(policy, gtwCtx.envoyProxy); luaError != nil { + // If policyTargetGateway is set, only apply to the specific gateway + if policyTargetGateway != nil { + gtwNN := types.NamespacedName{ + Namespace: gtwCtx.Namespace, + Name: gtwCtx.Name, + } + if gtwNN != *policyTargetGateway { + continue + } + } + + if luas, luaError = t.buildLuas(policy, owners, gtwCtx.envoyProxy); luaError != nil { luaError = perr.WithMessage(luaError, "Lua") errs = errors.Join(errs, luaError) } var extProcs []ir.ExtProc - if extProcs, extProcError, extProcFailOpen = t.buildExtProcs(policy, resources, gtwCtx); extProcError != nil { + if extProcs, extProcError, extProcFailOpen = t.buildExtProcs(policy, owners, resources, gtwCtx); extProcError != nil { extProcError = perr.WithMessage(extProcError, "ExtProc") errs = errors.Join(errs, extProcError) } var dynamicModules []ir.DynamicModule - if dynamicModules, dynamicModuleError = t.buildDynamicModules(policy, gtwCtx.envoyProxy); dynamicModuleError != nil { + if dynamicModules, dynamicModuleError = t.buildDynamicModules(policy, owners, gtwCtx.envoyProxy); dynamicModuleError != nil { dynamicModuleError = perr.WithMessage(dynamicModuleError, "DynamicModule") errs = errors.Join(errs, dynamicModuleError) } irKey := t.getIRKey(gtwCtx.Gateway) for _, listener := range parentRefCtx.listeners { + // If policyTargetListener is set, only apply to the specific listener + if policyTargetListener != nil && *policyTargetListener != listener.Name { + continue + } + irListener := xdsIR[irKey].GetHTTPListener(irListenerName(listener)) if irListener != nil { for _, r := range irListener.Routes { @@ -634,6 +844,8 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( xdsIR resource.XdsIRMap, resources *resource.Resources, ) error { + noOwners := &envoyExtensionPolicyOwners{} + var ( extProcs []ir.ExtProc wasms []ir.Wasm @@ -644,19 +856,19 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( errs error ) - if extProcs, extProcError, extProcFailOpen = t.buildExtProcs(policy, resources, gateway); extProcError != nil { + if extProcs, extProcError, extProcFailOpen = t.buildExtProcs(policy, noOwners, resources, gateway); extProcError != nil { extProcError = perr.WithMessage(extProcError, "ExtProc") errs = errors.Join(errs, extProcError) } - if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, resources); wasmError != nil { + if wasms, wasmError, wasmFailOpen = t.buildWasms(policy, noOwners, resources); wasmError != nil { wasmError = perr.WithMessage(wasmError, "Wasm") errs = errors.Join(errs, wasmError) } - if luas, luaError = t.buildLuas(policy, gateway.envoyProxy); luaError != nil { + if luas, luaError = t.buildLuas(policy, noOwners, gateway.envoyProxy); luaError != nil { luaError = perr.WithMessage(luaError, "Lua") errs = errors.Join(errs, luaError) } - if dynamicModules, dynamicModuleError = t.buildDynamicModules(policy, gateway.envoyProxy); dynamicModuleError != nil { + if dynamicModules, dynamicModuleError = t.buildDynamicModules(policy, noOwners, gateway.envoyProxy); dynamicModuleError != nil { dynamicModuleError = perr.WithMessage(dynamicModuleError, "DynamicModule") errs = errors.Join(errs, dynamicModuleError) } @@ -729,6 +941,7 @@ func (t *Translator) translateEnvoyExtensionPolicyForGateway( func (t *Translator) buildLuas( policy *egv1a1.EnvoyExtensionPolicy, + owners *envoyExtensionPolicyOwners, envoyProxy *egv1a1.EnvoyProxy, ) ([]ir.Lua, error) { if policy == nil { @@ -742,9 +955,10 @@ func (t *Translator) buildLuas( luaIRList := make([]ir.Lua, 0, len(policy.Spec.Lua)) + ownerPolicy := policyOwnerOr(owners.lua, policy) for idx, ep := range policy.Spec.Lua { - name := irConfigNameForLua(policy, idx) - luaIR, err := t.buildLua(name, policy, ep, envoyProxy) + name := irConfigNameForLua(ownerPolicy, idx) + luaIR, err := t.buildLua(name, ownerPolicy, ep, envoyProxy) if err != nil { return nil, err } @@ -805,7 +1019,12 @@ func (t *Translator) getLuaBodyFromLocalObjectReference( } } -func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resources *resource.Resources, gtwCtx *GatewayContext) ([]ir.ExtProc, error, bool) { +func (t *Translator) buildExtProcs( + policy *egv1a1.EnvoyExtensionPolicy, + owners *envoyExtensionPolicyOwners, + resources *resource.Resources, + gtwCtx *GatewayContext, +) ([]ir.ExtProc, error, bool) { var ( failOpen bool errs error @@ -818,9 +1037,10 @@ func (t *Translator) buildExtProcs(policy *egv1a1.EnvoyExtensionPolicy, resource extProcIRList := make([]ir.ExtProc, 0, len(policy.Spec.ExtProc)) hasFailClose := false + ownerPolicy := policyOwnerOr(owners.extProc, policy) for idx, ep := range policy.Spec.ExtProc { - name := irConfigNameForExtProc(policy, idx) - extProcIR, err := t.buildExtProc(name, policy, ep, idx, resources, gtwCtx) + name := irConfigNameForExtProc(ownerPolicy, idx) + extProcIR, err := t.buildExtProc(name, ownerPolicy, ep, idx, resources, gtwCtx) if err != nil { errs = errors.Join(errs, err) if ep.FailOpen == nil || !*ep.FailOpen { @@ -949,6 +1169,7 @@ func irConfigNameForLua(policy *egv1a1.EnvoyExtensionPolicy, index int) string { func (t *Translator) buildWasms( policy *egv1a1.EnvoyExtensionPolicy, + owners *envoyExtensionPolicyOwners, resources *resource.Resources, ) ([]ir.Wasm, error, bool) { var ( @@ -971,9 +1192,10 @@ func (t *Translator) buildWasms( } hasFailClose := false + ownerPolicy := policyOwnerOr(owners.wasm, policy) for idx, wasm := range policy.Spec.Wasm { - name := irConfigNameForWasm(policy, idx) - wasmIR, err := t.buildWasm(name, &wasm, policy, idx, resources) + name := irConfigNameForWasm(ownerPolicy, idx) + wasmIR, err := t.buildWasm(name, &wasm, ownerPolicy, idx, resources) if err != nil { errs = errors.Join(errs, err) if wasm.FailOpen == nil || !*wasm.FailOpen { @@ -1199,6 +1421,7 @@ func irConfigNameForDynamicModule(policy *egv1a1.EnvoyExtensionPolicy, index int func (t *Translator) buildDynamicModules( policy *egv1a1.EnvoyExtensionPolicy, + owners *envoyExtensionPolicyOwners, envoyProxy *egv1a1.EnvoyProxy, ) ([]ir.DynamicModule, error) { var errs error @@ -1217,9 +1440,9 @@ func (t *Translator) buildDynamicModules( } dmIRList := make([]ir.DynamicModule, 0, len(policy.Spec.DynamicModule)) - + ownerPolicy := policyOwnerOr(owners.dynamicModule, policy) for idx, dm := range policy.Spec.DynamicModule { - name := irConfigNameForDynamicModule(policy, idx) + name := irConfigNameForDynamicModule(ownerPolicy, idx) // Validate module exists in registry entry, ok := registry[dm.Name] @@ -1289,3 +1512,43 @@ func envoyExtensionPolicyCopiesWithStatusDeepCopy(policies []*egv1a1.EnvoyExtens } return copies } + +type envoyExtensionPolicyOwners struct { + wasm *egv1a1.EnvoyExtensionPolicy + extProc *egv1a1.EnvoyExtensionPolicy + lua *egv1a1.EnvoyExtensionPolicy + dynamicModule *egv1a1.EnvoyExtensionPolicy +} + +// mergeEnvoyExtensionPolicy merges a route-level EnvoyExtensionPolicy with a parent (Gateway/Listener) EnvoyExtensionPolicy. +func mergeEnvoyExtensionPolicy(routePolicy, parentPolicy *egv1a1.EnvoyExtensionPolicy) (*egv1a1.EnvoyExtensionPolicy, *envoyExtensionPolicyOwners, error) { + if routePolicy.Spec.MergeType == nil || parentPolicy == nil { + return routePolicy, nil, nil + } + mergedPolicy, err := utils.Merge(parentPolicy, routePolicy, *routePolicy.Spec.MergeType) + if err != nil { + return nil, nil, err + } + return mergedPolicy, buildEnvoyExtensionPolicyOwners(routePolicy, parentPolicy), nil +} + +// buildEnvoyExtensionPolicyOwners determines, for each merged field, which policy +// (route or parent) is considered the owner. The owner is used later to resolve +// references (e.g. Secrets, BackendRefs) scoped to the owning policy's namespace, +// and to derive IR resource names tied to the owning policy. +func buildEnvoyExtensionPolicyOwners(route, parent *egv1a1.EnvoyExtensionPolicy) *envoyExtensionPolicyOwners { + return &envoyExtensionPolicyOwners{ + wasm: ownerOf(route, parent, func(p *egv1a1.EnvoyExtensionPolicy) bool { + return len(p.Spec.Wasm) > 0 + }), + extProc: ownerOf(route, parent, func(p *egv1a1.EnvoyExtensionPolicy) bool { + return len(p.Spec.ExtProc) > 0 + }), + lua: ownerOf(route, parent, func(p *egv1a1.EnvoyExtensionPolicy) bool { + return len(p.Spec.Lua) > 0 + }), + dynamicModule: ownerOf(route, parent, func(p *egv1a1.EnvoyExtensionPolicy) bool { + return len(p.Spec.DynamicModule) > 0 + }), + } +} diff --git a/internal/gatewayapi/envoyextensionpolicy_test.go b/internal/gatewayapi/envoyextensionpolicy_test.go index 9a29f5a863..5a846f9de4 100644 --- a/internal/gatewayapi/envoyextensionpolicy_test.go +++ b/internal/gatewayapi/envoyextensionpolicy_test.go @@ -9,6 +9,11 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) func Test_hasTag(t *testing.T) { @@ -105,3 +110,108 @@ func TestValidateDynamicModuleRemoteURL(t *testing.T) { }) } } + +func Test_envoyExtensionPolicyOwnerChoose(t *testing.T) { + t.Run("route policy overrides parent for the same owner fields", func(t *testing.T) { + parentPolicy := &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "parent-ns"}, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + Wasm: []egv1a1.Wasm{{ + Name: new("parent-wasm"), + }}, + ExtProc: []egv1a1.ExtProc{{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "parent-extproc"}, + }}, + }, + }}, + Lua: []egv1a1.Lua{{ + Type: egv1a1.LuaValueTypeValueRef, + ValueRef: &gwapiv1.LocalObjectReference{Name: "parent-lua-cm"}, + }}, + DynamicModule: []egv1a1.DynamicModule{{ + Name: "parent-dm", + }}, + }, + } + + routePolicy := &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: "route-ns"}, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + MergeType: new(egv1a1.StrategicMerge), + Wasm: []egv1a1.Wasm{{ + Name: new("route-wasm"), + }}, + ExtProc: []egv1a1.ExtProc{{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "route-extproc"}, + }}, + }, + }}, + Lua: []egv1a1.Lua{{ + Type: egv1a1.LuaValueTypeValueRef, + ValueRef: &gwapiv1.LocalObjectReference{Name: "route-lua-cm"}, + }}, + DynamicModule: []egv1a1.DynamicModule{{ + Name: "route-dm", + }}, + }, + } + + _, owners, err := mergeEnvoyExtensionPolicy(routePolicy, parentPolicy) + require.NoError(t, err) + require.NotNil(t, owners) + + assert.Same(t, routePolicy, owners.wasm) + assert.Same(t, routePolicy, owners.extProc) + assert.Same(t, routePolicy, owners.lua) + assert.Same(t, routePolicy, owners.dynamicModule) + }) + + t.Run("uses parent owner when route does not set the field", func(t *testing.T) { + parentPolicy := &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "parent", Namespace: "parent-ns"}, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + MergeType: new(egv1a1.StrategicMerge), + Wasm: []egv1a1.Wasm{{ + Name: new("parent-wasm"), + }}, + ExtProc: []egv1a1.ExtProc{{ + BackendCluster: egv1a1.BackendCluster{ + BackendRefs: []egv1a1.BackendRef{{ + BackendObjectReference: gwapiv1.BackendObjectReference{Name: "parent-extproc"}, + }}, + }, + }}, + Lua: []egv1a1.Lua{{ + Type: egv1a1.LuaValueTypeValueRef, + ValueRef: &gwapiv1.LocalObjectReference{Name: "parent-lua-cm"}, + }}, + DynamicModule: []egv1a1.DynamicModule{{ + Name: "parent-dm", + }}, + }, + } + + routePolicy := &egv1a1.EnvoyExtensionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "route", Namespace: "route-ns"}, + Spec: egv1a1.EnvoyExtensionPolicySpec{ + MergeType: new(egv1a1.StrategicMerge), + Wasm: []egv1a1.Wasm{{ + Name: new("route-wasm"), + }}, + }, + } + + _, owners, err := mergeEnvoyExtensionPolicy(routePolicy, parentPolicy) + require.NoError(t, err) + require.NotNil(t, owners) + + assert.Same(t, routePolicy, owners.wasm) + assert.Same(t, parentPolicy, owners.extProc) + assert.Same(t, parentPolicy, owners.lua) + assert.Same(t, parentPolicy, owners.dynamicModule) + }) +} diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index ade73750e4..88b75aba36 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -1237,42 +1237,6 @@ func getOverriddenTargetsMessageForRoute( return "" } -func getOverriddenTargetsMessageForGateway( - targetContext *policyGatewayTargetContext, - listenerRouteMap map[string]sets.Set[string], - sectionName *gwapiv1.SectionName, -) string { - var listeners, routes []string - if sectionName == nil { - if targetContext != nil { - listeners = targetContext.attachedToListeners.UnsortedList() - } - for _, routeSet := range listenerRouteMap { - routes = append(routes, routeSet.UnsortedList()...) - } - } else if listenerRouteMap != nil { - if routeSet, ok := listenerRouteMap[string(*sectionName)]; ok { - routes = routeSet.UnsortedList() - } - if routeSet, ok := listenerRouteMap[""]; ok { - routes = append(routes, routeSet.UnsortedList()...) - } - } - if len(listeners) > 0 { - sort.Strings(listeners) - if len(routes) > 0 { - sort.Strings(routes) - return fmt.Sprintf("these listeners: %v and these routes: %v", listeners, routes) - } else { - return fmt.Sprintf("these listeners: %v", listeners) - } - } else if len(routes) > 0 { - sort.Strings(routes) - return fmt.Sprintf("these routes: %v", routes) - } - return "" -} - // getOverriddenAndMergedTargetsMessageForGateway generates status messages for policies // indicating which listeners and routes are being overridden or merged. func getOverriddenAndMergedTargetsMessageForGateway( @@ -1376,3 +1340,26 @@ func getOverriddenAndMergedTargetsMessageForGateway( } return overrideMessage, mergedMessage } + +// policyOwnerOr returns owner if non-nil, otherwise fallback. +// Used to resolve per-field owners from PolicyOwners: the owner is the policy +// that contributed the field (route overrides parent), falling back to the active policy +// when no merge occurred or the field was not set by either side. +func policyOwnerOr[T any](owner, fallback *T) *T { + if owner != nil { + return owner + } + return fallback +} + +// ownerOf returns route if routeOwns(route) is true, otherwise parent. +// Use this when ownership of a merged field is determined by a single predicate. +func ownerOf[T any]( + route, parent *T, + routeOwns func(*T) bool, +) *T { + if routeOwns(route) { + return route + } + return parent +} diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 779634d9ec..e7e9f82e2b 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -2559,17 +2559,6 @@ type securityPolicyOwners struct { jwtProviders *egv1a1.SecurityPolicy } -// policyOwnerOr returns owner if non-nil, otherwise fallback. -// Used to resolve per-field owners from securityPolicyOwners: the owner is the policy -// that contributed the field (route overrides parent), falling back to the active policy -// when no merge occurred or the field was not set by either side. -func policyOwnerOr(owner, fallback *egv1a1.SecurityPolicy) *egv1a1.SecurityPolicy { - if owner != nil { - return owner - } - return fallback -} - // mergeSecurityPolicy merges a route-level SecurityPolicy with a parent (Gateway/Listener) SecurityPolicy. func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv1a1.SecurityPolicy, *securityPolicyOwners, error) { if routePolicy.Spec.MergeType == nil || parentPolicy == nil { @@ -2582,18 +2571,6 @@ func mergeSecurityPolicy(routePolicy, parentPolicy *egv1a1.SecurityPolicy) (*egv return mergedPolicy, buildSecurityPolicyOwners(routePolicy, parentPolicy), nil } -// ownerOf returns route if routeOwns(route) is true, otherwise parent. -// Use this when ownership of a merged field is determined by a single predicate. -func ownerOf( - route, parent *egv1a1.SecurityPolicy, - routeOwns func(*egv1a1.SecurityPolicy) bool, -) *egv1a1.SecurityPolicy { - if routeOwns(route) { - return route - } - return parent -} - // buildSecurityPolicyOwners determines, for each merged field, which policy // (route or parent) is considered the owner. The owner is used later to resolve // references (e.g. Secrets, BackendRefs) scoped to the owning policy's namespace, diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.in.yaml new file mode 100644 index 0000000000..4213bb5222 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.in.yaml @@ -0,0 +1,151 @@ +configmaps: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-gateway + namespace: envoy-gateway + data: + lua: | + function envoy_on_request(request_handle) + request_handle:logInfo('hello from parent') + end +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-policy-for-http-route + namespace: default + data: + lua: | + function envoy_on_response(response_handle) + response_handle:logWarn('hello from route') + end +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: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +services: +- apiVersion: v1 + kind: Service + metadata: + namespace: envoy-gateway + name: grpc-backend + spec: + ports: + - port: 9000 + name: grpc + protocol: TCP +endpointSlices: +- apiVersion: discovery.k8s.io/v1 + kind: EndpointSlice + metadata: + namespace: envoy-gateway + name: endpointslice-grpc-backend + labels: + kubernetes.io/service-name: grpc-backend + addressType: IPv4 + ports: + - name: grpc + protocol: TCP + port: 9000 + endpoints: + - addresses: + - 8.8.8.8 + conditions: + ready: true +envoyextensionpolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: http + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-gateway + kind: ConfigMap + group: v1 + extProc: + - backendRefs: + - name: grpc-backend + port: 9000 + processingMode: + allowModeOverride: true + request: + body: Buffered + attributes: + - request.path + response: + body: Streamed + attributes: + - xds.route_metadata + - connection.requested_server_name + metadata: + accessibleNamespaces: + - envoy.filters.http.ext_authz + writableNamespaces: + - envoy.filters.http.my_custom + messageTimeout: 5s + failOpen: true +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + namespace: default + name: policy-for-route + spec: + mergeType: StrategicMerge + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + lua: + - type: ValueRef + valueRef: + name: cm-policy-for-http-route + kind: ConfigMap + group: v1 + wasm: + - name: wasm-filter-1 + code: + type: HTTP + http: + url: https://www.example.com/wasm-filter-1.wasm + sha256: 2d89c4c6ab2a1c615c7696ed37ade9e50654ac70384b5d45100eb08e62130ff4 + env: + hostKeys: + - SOME_KEY + - ANOTHER_KEY diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.out.yaml new file mode 100644 index 0000000000..ecdb8579d2 --- /dev/null +++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-merge-needs-fieldowner.out.yaml @@ -0,0 +1,342 @@ +envoyExtensionPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + name: policy-for-route + namespace: default + spec: + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: cm-policy-for-http-route + mergeType: StrategicMerge + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + wasm: + - code: + http: + sha256: 2d89c4c6ab2a1c615c7696ed37ade9e50654ac70384b5d45100eb08e62130ff4 + url: https://www.example.com/wasm-filter-1.wasm + type: HTTP + env: + hostKeys: + - SOME_KEY + - ANOTHER_KEY + name: wasm-filter-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 + reason: Merged + status: "True" + type: Merged + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyExtensionPolicy + metadata: + name: policy-for-gateway + namespace: envoy-gateway + spec: + extProc: + - backendRefs: + - name: grpc-backend + port: 9000 + failOpen: true + messageTimeout: 5s + metadata: + accessibleNamespaces: + - envoy.filters.http.ext_authz + writableNamespaces: + - envoy.filters.http.my_custom + processingMode: + allowModeOverride: true + request: + attributes: + - request.path + body: Buffered + response: + attributes: + - xds.route_metadata + - connection.requested_server_name + body: Streamed + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: cm-policy-for-gateway + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + sectionName: http + 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: 'This policy is being merged by other envoyExtensionPolicies 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: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + 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: + envoyClientCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + name: envoy-gateway-system/envoy + privateKey: '[redacted]' + 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 + envoyExtensions: + extProcs: + - allowModeOverride: true + authority: grpc-backend.envoy-gateway:9000 + destination: + metadata: + kind: EnvoyExtensionPolicy + name: policy-for-gateway + namespace: envoy-gateway + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/extproc/0 + settings: + - addressType: IP + endpoints: + - host: 8.8.8.8 + port: 9000 + metadata: + kind: Service + name: grpc-backend + namespace: envoy-gateway + sectionName: "9000" + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/extproc/0/backend/0 + protocol: GRPC + weight: 1 + failOpen: true + forwardingMetadataNamespaces: + - envoy.filters.http.ext_authz + messageTimeout: 5s + name: envoyextensionpolicy/envoy-gateway/policy-for-gateway/extproc/0 + receivingMetadataNamespaces: + - envoy.filters.http.my_custom + requestAttributes: + - request.path + requestBodyProcessingMode: Buffered + requestHeaderProcessing: true + responseAttributes: + - xds.route_metadata + - connection.requested_server_name + responseBodyProcessingMode: Streamed + responseHeaderProcessing: true + luas: + - Code: | + function envoy_on_response(response_handle) + response_handle:logWarn('hello from route') + end + Name: envoyextensionpolicy/default/policy-for-route/lua/0 + wasms: + - config: null + failOpen: false + hostKeys: + - SOME_KEY + - ANOTHER_KEY + httpWasmCode: + originalDownloadingURL: https://www.example.com/wasm-filter-1.wasm + servingURL: https://envoy-gateway.envoy-gateway-system.svc.cluster.local:18002/5c90b9a82642ce00a7753923fabead306b9d9a54a7c0bd2463a1af3efcfb110b.wasm + sha256: 2d89c4c6ab2a1c615c7696ed37ade9e50654ac70384b5d45100eb08e62130ff4 + name: envoyextensionpolicy/default/policy-for-route/wasm/0 + wasmName: wasm-filter-1 + hostname: '*' + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/* + pathMatch: + distinct: false + name: "" + prefix: /foo + readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19003 diff --git a/internal/utils/merge_test.go b/internal/utils/merge_test.go index 2c91e27bcd..56f409d647 100644 --- a/internal/utils/merge_test.go +++ b/internal/utils/merge_test.go @@ -32,6 +32,8 @@ func TestMergePolicy(t *testing.T) { switch policyType { case "backendtrafficpolicy": runMergePolicyTest[*egv1a1.BackendTrafficPolicy](t, caseFile) + case "envoyextensionpolicy": + runMergePolicyTest[*egv1a1.EnvoyExtensionPolicy](t, caseFile) case "securitypolicy": runMergePolicyTest[*egv1a1.SecurityPolicy](t, caseFile) default: diff --git a/internal/utils/testdata/envoyextensionpolicy_all.in.yaml b/internal/utils/testdata/envoyextensionpolicy_all.in.yaml new file mode 100644 index 0000000000..dcdd1908e6 --- /dev/null +++ b/internal/utils/testdata/envoyextensionpolicy_all.in.yaml @@ -0,0 +1,33 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: original +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + wasm: + - name: wasm-parent + rootID: parent-root + code: + type: HTTP + http: + url: https://wasm.parent.example.com/filter.wasm + failOpen: false + extProc: + - backendRefs: + - name: ext-proc-parent + kind: Service + port: 9001 + messageTimeout: 5s + failOpen: false + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: parent-lua + dynamicModule: + - name: parent-dm + filterName: parent-filter diff --git a/internal/utils/testdata/envoyextensionpolicy_all.jsonmerge.out.yaml b/internal/utils/testdata/envoyextensionpolicy_all.jsonmerge.out.yaml new file mode 100644 index 0000000000..97f2dc6d22 --- /dev/null +++ b/internal/utils/testdata/envoyextensionpolicy_all.jsonmerge.out.yaml @@ -0,0 +1,37 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: patched +spec: + dynamicModule: + - filterName: parent-filter + name: parent-dm + extProc: + - backendRefs: + - kind: Service + name: ext-proc-route + port: 9002 + failOpen: true + messageTimeout: 10s + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: route-lua + mergeType: StrategicMerge + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-1 + wasm: + - code: + http: + sha256: null + url: https://wasm.parent.example.com/filter.wasm + type: HTTP + failOpen: false + name: wasm-parent + rootID: parent-root +status: + ancestors: null diff --git a/internal/utils/testdata/envoyextensionpolicy_all.patch.yaml b/internal/utils/testdata/envoyextensionpolicy_all.patch.yaml new file mode 100644 index 0000000000..49350a565a --- /dev/null +++ b/internal/utils/testdata/envoyextensionpolicy_all.patch.yaml @@ -0,0 +1,23 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: patched +spec: + mergeType: StrategicMerge + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-1 + extProc: + - backendRefs: + - name: ext-proc-route + kind: Service + port: 9002 + messageTimeout: 10s + failOpen: true + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: route-lua diff --git a/internal/utils/testdata/envoyextensionpolicy_all.strategicmerge.out.yaml b/internal/utils/testdata/envoyextensionpolicy_all.strategicmerge.out.yaml new file mode 100644 index 0000000000..97f2dc6d22 --- /dev/null +++ b/internal/utils/testdata/envoyextensionpolicy_all.strategicmerge.out.yaml @@ -0,0 +1,37 @@ +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: patched +spec: + dynamicModule: + - filterName: parent-filter + name: parent-dm + extProc: + - backendRefs: + - kind: Service + name: ext-proc-route + port: 9002 + failOpen: true + messageTimeout: 10s + lua: + - type: ValueRef + valueRef: + group: v1 + kind: ConfigMap + name: route-lua + mergeType: StrategicMerge + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: route-1 + wasm: + - code: + http: + sha256: null + url: https://wasm.parent.example.com/filter.wasm + type: HTTP + failOpen: false + name: wasm-parent + rootID: parent-root +status: + ancestors: null diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 2dcce71d8e..fcc844880f 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -9,6 +9,7 @@ security updates: | # New features or capabilities added in this release. new features: | + Added support for MergeType in EnvoyExtensionPolicy. bug fixes: | Fixed the xDS server in GatewayNamespaceMode serving a stale certificate after cert-manager rotation by re-reading the cert from disk on every TLS handshake. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 3be3c6fa8c..929c229290 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1591,6 +1591,7 @@ _Appears in:_ | `targetRef` | _[LocalPolicyTargetReferenceWithSectionName](#localpolicytargetreferencewithsectionname)_ | true | | TargetRef is the name of the resource this policy is being attached to.
This policy and the TargetRef MUST be in the same namespace for this
Policy to have effect
Deprecated: use targetRefs/targetSelectors instead | | `targetRefs` | _LocalPolicyTargetReferenceWithSectionName array_ | true | | TargetRefs are the names of the Gateway resources this policy
is being attached to. | | `targetSelectors` | _[TargetSelector](#targetselector) array_ | true | | TargetSelectors allow targeting resources for this policy based on labels | +| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy
configurations targeting a parent resource. When set, this configuration will be merged
into a parent EnvoyExtensionPolicy (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. | | `wasm` | _[Wasm](#wasm) array_ | false | | Wasm is a list of Wasm extensions to be loaded by the Gateway.
Order matters, as the extensions will be loaded in the order they are
defined in this list. | | `extProc` | _[ExtProc](#extproc) array_ | false | | ExtProc is an ordered list of external processing filters
that should be added to the envoy filter chain | | `lua` | _[Lua](#lua) array_ | false | | Lua is an ordered list of Lua filters
that should be added to the envoy filter chain | @@ -4146,6 +4147,7 @@ MergeType defines the type of merge operation _Appears in:_ - [BackendTrafficPolicySpec](#backendtrafficpolicyspec) +- [EnvoyExtensionPolicySpec](#envoyextensionpolicyspec) - [EnvoyProxySpec](#envoyproxyspec) - [KubernetesPatchSpec](#kubernetespatchspec) - [SecurityPolicySpec](#securitypolicyspec) diff --git a/test/e2e/testdata/envoyextensionpolicy-merged.yaml b/test/e2e/testdata/envoyextensionpolicy-merged.yaml new file mode 100644 index 0000000000..d5d2c6c503 --- /dev/null +++ b/test/e2e/testdata/envoyextensionpolicy-merged.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: eep-merged-route + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + sectionName: http + hostnames: ["www.example.com"] + rules: + - matches: + - path: + type: PathPrefix + value: /merged + backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: eep-merged-parent-lua + namespace: gateway-conformance-infra +data: + lua: | + function envoy_on_response(response_handle) + response_handle:headers():add("X-Custom-Lua-Header", "merged-parent") + end +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: eep-merged-gateway + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: same-namespace + lua: + - type: ValueRef + valueRef: + name: eep-merged-parent-lua + kind: ConfigMap + group: v1 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: BackendTLSPolicy +metadata: + name: eep-merged-grpc-ext-proc-btls + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: '' + kind: Service + name: grpc-ext-proc + sectionName: grpc + validation: + caCertificateRefs: + - name: grpc-ext-proc-ca + group: '' + kind: ConfigMap + hostname: grpc-ext-proc.envoygateway +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyExtensionPolicy +metadata: + name: eep-merged-route + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: eep-merged-route + mergeType: StrategicMerge + extProc: + - backendRefs: + - name: grpc-ext-proc + namespace: gateway-conformance-infra + port: 9002 + processingMode: + request: {} + response: {} diff --git a/test/e2e/tests/envoyextensionpolicy_merged.go b/test/e2e/tests/envoyextensionpolicy_merged.go new file mode 100644 index 0000000000..419cb80e8a --- /dev/null +++ b/test/e2e/tests/envoyextensionpolicy_merged.go @@ -0,0 +1,103 @@ +// 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. + +//go:build e2e + +package tests + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + httputils "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, EnvoyExtensionPolicyMergedTest) +} + +var EnvoyExtensionPolicyMergedTest = suite.ConformanceTest{ + ShortName: "EnvoyExtensionPolicyMerged", + Description: "Test route-level EnvoyExtensionPolicy merge with parent Gateway policy", + Manifests: []string{"testdata/ext-proc-service.yaml", "testdata/envoyextensionpolicy-merged.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("EnvoyExtensionPolicyMerged", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "eep-merged-route", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted( + t, + suite.Client, + suite.TimeoutConfig, + suite.ControllerName, + kubernetes.NewGatewayRef(gwNN), + routeNN, + ) + + ancestorRef := gwapiv1.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + SectionName: new(gwapiv1.SectionName("http")), + } + + EnvoyExtensionPolicyMustBeAccepted(t, + suite.Client, + types.NamespacedName{Name: "eep-merged-route", Namespace: ns}, + suite.ControllerName, + ancestorRef, + ) + + EnvoyExtensionPolicyMustBeMerged(t, + suite.Client, + types.NamespacedName{Name: "eep-merged-route", Namespace: ns}, + suite.ControllerName, + ancestorRef, + ) + + podReady := corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue} + WaitForPods(t, suite.Client, ns, map[string]string{"app": "grpc-ext-proc"}, corev1.PodRunning, &podReady) + + expectedResponse := httputils.ExpectedResponse{ + Request: httputils.Request{ + Host: "www.example.com", + Path: "/merged", + Headers: map[string]string{ + "x-request-client-header": "original", + }, + }, + ExpectedRequest: &httputils.ExpectedRequest{ + Request: httputils.Request{ + Path: "/merged", + Headers: map[string]string{ + "x-request-ext-processed": "true", + "x-request-client-header-received": "original", + "x-request-client-header": "mutated", + }, + }, + }, + Response: httputils.Response{ + StatusCodes: []int{200}, + Headers: map[string]string{ + "X-Custom-Lua-Header": "merged-parent", + "x-response-ext-processed": "true", + }, + }, + Namespace: ns, + } + + httputils.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, expectedResponse) + }) + }, +} diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go index 75a89a39fa..5bf5f89943 100644 --- a/test/e2e/tests/utils.go +++ b/test/e2e/tests/utils.go @@ -501,6 +501,29 @@ func EnvoyExtensionPolicyMustBeAccepted(t *testing.T, client client.Client, poli require.NoErrorf(t, waitErr, "error waiting for EnvoyExtensionPolicy to be accepted") } +// EnvoyExtensionPolicyMustBeMerged waits for the specified EnvoyExtensionPolicy to have Merged condition. +func EnvoyExtensionPolicyMustBeMerged(t *testing.T, client client.Client, policyName types.NamespacedName, controllerName string, ancestorRef gwapiv1.ParentReference) { + t.Helper() + + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 60*time.Second, true, func(ctx context.Context) (bool, error) { + policy := &egv1a1.EnvoyExtensionPolicy{} + err := client.Get(ctx, policyName, policy) + if err != nil { + return false, fmt.Errorf("error fetching EnvoyExtensionPolicy: %w", err) + } + + if policyMergedByAncestor(policy.Status.Ancestors, controllerName, ancestorRef) { + tlog.Logf(t, "EnvoyExtensionPolicy has Merged condition: %+v", policy) + return true, nil + } + + tlog.Logf(t, "EnvoyExtensionPolicy does not have Merged condition yet: %+v", policy) + return false, nil + }) + + require.NoErrorf(t, waitErr, "error waiting for EnvoyExtensionPolicy to have Merged condition") +} + // BackendMustBeAccepted waits for the specified Backend to be accepted. func BackendMustBeAccepted(t *testing.T, client client.Client, backendName types.NamespacedName) { t.Helper() diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 688b3ae4a9..3cc271efb2 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -29932,6 +29932,17 @@ spec: || (self.type == 'ValueRef' && !has(self.inline) && has(self.valueRef)) maxItems: 16 type: array + mergeType: + description: |- + MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + configurations targeting a parent resource. When set, this configuration will be merged + into a parent EnvoyExtensionPolicy (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. + type: string + x-kubernetes-validations: + - message: Replace is not a valid MergeType for EnvoyExtensionPolicy + rule: self != 'Replace' targetRef: description: |- TargetRef is the name of the resource this policy is being attached to. diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 7c40461c76..db769e2297 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -7905,6 +7905,17 @@ spec: || (self.type == 'ValueRef' && !has(self.inline) && has(self.valueRef)) maxItems: 16 type: array + mergeType: + description: |- + MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + configurations targeting a parent resource. When set, this configuration will be merged + into a parent EnvoyExtensionPolicy (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. + type: string + x-kubernetes-validations: + - message: Replace is not a valid MergeType for EnvoyExtensionPolicy + rule: self != 'Replace' targetRef: description: |- TargetRef is the name of the resource this policy is being attached to. 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 93efcc2e69..3705641c18 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -7905,6 +7905,17 @@ spec: || (self.type == 'ValueRef' && !has(self.inline) && has(self.valueRef)) maxItems: 16 type: array + mergeType: + description: |- + MergeType determines how this configuration is merged with existing EnvoyExtensionPolicy + configurations targeting a parent resource. When set, this configuration will be merged + into a parent EnvoyExtensionPolicy (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. + type: string + x-kubernetes-validations: + - message: Replace is not a valid MergeType for EnvoyExtensionPolicy + rule: self != 'Replace' targetRef: description: |- TargetRef is the name of the resource this policy is being attached to.