From 4792c83df920ef3fec52566fe75f1c226667cf4e Mon Sep 17 00:00:00 2001 From: zirain Date: Thu, 12 Mar 2026 16:42:18 +0800 Subject: [PATCH 1/5] fix: support patch multiple resources in EPP Signed-off-by: zirain --- api/v1alpha1/envoypatchpolicy_types.go | 14 +- api/v1alpha1/zz_generated.deepcopy.go | 10 + ...eway.envoyproxy.io_envoypatchpolicies.yaml | 30 ++- ...eway.envoyproxy.io_envoypatchpolicies.yaml | 30 ++- internal/gatewayapi/envoypatchpolicy.go | 55 ++++- ...atchpolicy-invalid-merge-gateways.out.yaml | 5 +- ...voypatchpolicy-invalid-target-name.in.yaml | 22 +- ...oypatchpolicy-invalid-target-name.out.yaml | 34 +++- ...ypatchpolicy-valid-merge-gateways.out.yaml | 10 +- .../testdata/envoypatchpolicy-valid.out.yaml | 10 +- internal/ir/xds.go | 4 +- internal/ir/zz_generated.deepcopy.go | 1 + internal/xds/translator/jsonpatch.go | 158 +++++++------- internal/xds/translator/listener_ready.go | 11 +- .../jsonpatch-add-op-empty-jsonpath.yaml | 3 +- .../jsonpatch-add-op-without-value.yaml | 6 +- .../in/xds-ir/jsonpatch-invalid-listener.yaml | 12 +- .../in/xds-ir/jsonpatch-invalid-patch.yaml | 3 +- .../testdata/in/xds-ir/jsonpatch-invalid.yaml | 12 +- .../in/xds-ir/jsonpatch-missing-resource.yaml | 3 +- .../xds-ir/jsonpatch-move-op-with-value.yaml | 6 +- .../jsonpatch-patch-multiple-resources.yaml | 192 ++++++++++++++++++ .../jsonpatch-with-jsonpath-invalid.yaml | 3 +- .../in/xds-ir/jsonpatch-with-jsonpath.yaml | 33 ++- .../testdata/in/xds-ir/jsonpatch.yaml | 30 ++- ...tch-patch-multiple-resources.clusters.yaml | 80 ++++++++ ...ch-patch-multiple-resources.endpoints.yaml | 51 +++++ ...multiple-resources.envoypatchpolicies.yaml | 16 ++ ...ch-patch-multiple-resources.listeners.yaml | 137 +++++++++++++ ...patch-patch-multiple-resources.routes.yaml | 42 ++++ ...atch-patch-multiple-resources.secrets.yaml | 22 ++ internal/xds/translator/translator.go | 166 ++++++++++++--- internal/xds/translator/translator_test.go | 3 + site/content/en/latest/api/extension_types.md | 6 +- .../tasks/extensibility/envoy-patch-policy.md | 7 - test/cel-validation/envoypatchpolicy_test.go | 155 ++++++++++++++ ...envoy-patch-policy-xds-name-scheme-v2.yaml | 2 +- test/e2e/testdata/envoy-patch-policy.yaml | 139 ++++++++++--- test/e2e/tests/envoy_patch_policy.go | 31 ++- .../envoy_patch_policy_xds_name_scheme_v2.go | 2 +- test/helm/gateway-crds-helm/all.out.yaml | 30 ++- test/helm/gateway-crds-helm/e2e.out.yaml | 30 ++- .../envoy-gateway-crds.out.yaml | 30 ++- 43 files changed, 1422 insertions(+), 224 deletions(-) create mode 100644 internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml create mode 100644 test/cel-validation/envoypatchpolicy_test.go diff --git a/api/v1alpha1/envoypatchpolicy_types.go b/api/v1alpha1/envoypatchpolicy_types.go index b131b7a46a..7f8886e758 100644 --- a/api/v1alpha1/envoypatchpolicy_types.go +++ b/api/v1alpha1/envoypatchpolicy_types.go @@ -78,12 +78,20 @@ const ( ) // EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource -// using JSONPatch semantic +// using JSONPatch semantics. +// +// +kubebuilder:validation:XValidation:rule="!(has(self.name) && has(self.nameSelector))",message="only one of name and nameSelector can be specified" type EnvoyJSONPatchConfig struct { // Type is the typed URL of the Envoy xDS Resource Type EnvoyResourceType `json:"type"` - // Name is the name of the resource - Name string `json:"name"` + // Name is the name of the resource. + // + // +optional + Name *string `json:"name,omitempty"` + // NameSelector is a StringMatch that is used to select the resources to patch based on their name. + // + // +optional + NameSelector *StringMatch `json:"nameSelector,omitempty"` // Patch defines the JSON Patch Operation Operation JSONPatchOperation `json:"operation"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7245a49145..0e36dc83ca 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2930,6 +2930,16 @@ func (in *EnvoyGatewayTraces) DeepCopy() *EnvoyGatewayTraces { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvoyJSONPatchConfig) DeepCopyInto(out *EnvoyJSONPatchConfig) { *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.NameSelector != nil { + in, out := &in.NameSelector, &out.NameSelector + *out = new(StringMatch) + (*in).DeepCopyInto(*out) + } in.Operation.DeepCopyInto(&out.Operation) } diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index 6c3579fee1..5a236bafdb 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -61,11 +61,33 @@ spec: items: description: |- EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource - using JSONPatch semantic + using JSONPatch semantics. properties: name: - description: Name is the name of the resource + description: Name is the name of the resource. type: string + nameSelector: + description: NameSelector is a StringMatch that is used to select + the resources to patch based on their name. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object operation: description: Patch defines the JSON Patch Operation properties: @@ -117,10 +139,12 @@ spec: - type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret type: string required: - - name - operation - type type: object + x-kubernetes-validations: + - message: only one of name and nameSelector can be specified + rule: '!(has(self.name) && has(self.nameSelector))' type: array priority: description: |- diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index c3f38eead5..acc0ed1eb5 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -60,11 +60,33 @@ spec: items: description: |- EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource - using JSONPatch semantic + using JSONPatch semantics. properties: name: - description: Name is the name of the resource + description: Name is the name of the resource. type: string + nameSelector: + description: NameSelector is a StringMatch that is used to select + the resources to patch based on their name. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object operation: description: Patch defines the JSON Patch Operation properties: @@ -116,10 +138,12 @@ spec: - type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret type: string required: - - name - operation - type type: object + x-kubernetes-validations: + - message: only one of name and nameSelector can be specified + rule: '!(has(self.name) && has(self.nameSelector))' type: array priority: description: |- diff --git a/internal/gatewayapi/envoypatchpolicy.go b/internal/gatewayapi/envoypatchpolicy.go index e184bbb069..d3e1ea665c 100644 --- a/internal/gatewayapi/envoypatchpolicy.go +++ b/internal/gatewayapi/envoypatchpolicy.go @@ -7,8 +7,10 @@ package gatewayapi import ( "fmt" + "regexp" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -129,7 +131,31 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo for _, patch := range policy.Spec.JSONPatches { irPatch := ir.JSONPatchConfig{} irPatch.Type = string(patch.Type) - irPatch.Name = patch.Name + irPatch.Name = ir.StringMatch{ + Exact: patch.Name, + } + if patch.NameSelector != nil { + irPatch.Name = *toIRStringMatch(patch.NameSelector) + } + // Validate that regex is valid if it's a regex match + if r := irPatch.Name.SafeRegex; r != nil { + _, err := regexp.Compile(*r) + if err != nil { + message := fmt.Sprintf("invalid regex in NameSelector: %v", err) + resolveErr = &status.PolicyResolveError{ + Reason: egv1a1.PolicyReasonInvalid, + Message: message, + } + status.SetResolveErrorForPolicyAncestor(&policy.Status, + &ancestorRef, + t.GatewayControllerName, + policy.Generation, + resolveErr, + ) + + continue + } + } irPatch.Operation.Op = ir.JSONPatchOp(patch.Operation.Op) irPatch.Operation.Path = patch.Operation.Path irPatch.Operation.JSONPath = patch.Operation.JSONPath @@ -145,3 +171,30 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo return res } + +func toIRStringMatch(sm *egv1a1.StringMatch) *ir.StringMatch { + if sm == nil { + return nil + } + + switch ptr.Deref(sm.Type, egv1a1.StringMatchExact) { + case egv1a1.StringMatchExact: + return &ir.StringMatch{ + Exact: &sm.Value, + } + case egv1a1.StringMatchPrefix: + return &ir.StringMatch{ + Prefix: &sm.Value, + } + case egv1a1.StringMatchSuffix: + return &ir.StringMatch{ + Suffix: &sm.Value, + } + case egv1a1.StringMatchRegularExpression: + return &ir.StringMatch{ + SafeRegex: &sm.Value, + } + default: + return nil + } +} diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-merge-gateways.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-merge-gateways.out.yaml index 0822c1734b..401380a58d 100644 --- a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-merge-gateways.out.yaml +++ b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-merge-gateways.out.yaml @@ -139,7 +139,10 @@ xdsIR: - path: /dev/stdout envoyPatchPolicies: - jsonPatches: - - name: envoy-gateway-gateway-1-http + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /ignore_global_conn_limit diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.in.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.in.yaml index 96b42db3bd..2b704cca0b 100644 --- a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.in.yaml +++ b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.in.yaml @@ -4,7 +4,6 @@ envoyPatchPolicies: metadata: namespace: envoy-gateway name: gateway-not-exists - generation: 10 spec: type: "JSONPatch" targetRef: @@ -23,7 +22,6 @@ envoyPatchPolicies: metadata: namespace: envoy-gateway name: edit-ignore-global-limit - generation: 10 spec: type: "JSONPatch" targetRef: @@ -39,6 +37,26 @@ envoyPatchPolicies: op: replace path: "/ignore_global_conn_limit" value: "true" +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyPatchPolicy + metadata: + namespace: envoy-gateway + name: invalid-regex + spec: + type: "JSONPatch" + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-not-exists + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + nameSelector: + type: RegularExpression + value: "*.illegal.regex" + operation: + op: replace + path: "/per_connection_buffer_limit_bytes" + value: "1024" gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.out.yaml index 7944bff175..27121f490b 100644 --- a/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.out.yaml +++ b/internal/gatewayapi/testdata/envoypatchpolicy-invalid-target-name.out.yaml @@ -2,7 +2,6 @@ envoyPatchPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyPatchPolicy metadata: - generation: 10 name: gateway-not-exists namespace: envoy-gateway spec: @@ -23,7 +22,6 @@ envoyPatchPolicies: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyPatchPolicy metadata: - generation: 10 name: edit-ignore-global-limit namespace: envoy-gateway spec: @@ -50,11 +48,32 @@ envoyPatchPolicies: conditions: - lastTransitionTime: null message: Policy has been accepted. - observedGeneration: 10 reason: Accepted status: "True" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyPatchPolicy + metadata: + name: invalid-regex + namespace: envoy-gateway + spec: + jsonPatches: + - nameSelector: + type: RegularExpression + value: '*.illegal.regex' + operation: + op: replace + path: /per_connection_buffer_limit_bytes + value: "1024" + type: type.googleapis.com/envoy.config.listener.v3.Listener + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-not-exists + type: JSONPatch + status: + ancestors: null gateways: - apiVersion: gateway.networking.k8s.io/v1 kind: Gateway @@ -120,9 +139,11 @@ xdsIR: json: - path: /dev/stdout envoyPatchPolicies: - - generation: 10 - jsonPatches: - - name: envoy-gateway-gateway-1-http + - jsonPatches: + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /ignore_global_conn_limit @@ -140,7 +161,6 @@ xdsIR: conditions: - lastTransitionTime: null message: Policy has been accepted. - observedGeneration: 10 reason: Accepted status: "True" type: Accepted diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml index b66fc89770..82265e266d 100644 --- a/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml +++ b/internal/gatewayapi/testdata/envoypatchpolicy-valid-merge-gateways.out.yaml @@ -137,7 +137,10 @@ xdsIR: - path: /dev/stdout envoyPatchPolicies: - jsonPatches: - - name: envoy-gateway-gateway-1-http + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /per_connection_buffer_limit_bytes @@ -159,7 +162,10 @@ xdsIR: type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller - jsonPatches: - - name: envoy-gateway-gateway-1-http + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /ignore_global_conn_limit diff --git a/internal/gatewayapi/testdata/envoypatchpolicy-valid.out.yaml b/internal/gatewayapi/testdata/envoypatchpolicy-valid.out.yaml index 6f3f290ea2..1370c021cf 100644 --- a/internal/gatewayapi/testdata/envoypatchpolicy-valid.out.yaml +++ b/internal/gatewayapi/testdata/envoypatchpolicy-valid.out.yaml @@ -135,7 +135,10 @@ xdsIR: envoyPatchPolicies: - generation: 10 jsonPatches: - - name: envoy-gateway-gateway-1-http + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /per_connection_buffer_limit_bytes @@ -160,7 +163,10 @@ xdsIR: controllerName: gateway.envoyproxy.io/gatewayclass-controller - generation: 10 jsonPatches: - - name: envoy-gateway-gateway-1-http + - name: + distinct: false + exact: envoy-gateway-gateway-1-http + name: "" operation: op: replace path: /ignore_global_conn_limit diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c8ec2b573d..96cd7882d5 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -2909,8 +2909,8 @@ type EnvoyPatchPolicyStatus struct { type JSONPatchConfig struct { // Type is the typed URL of the Envoy xDS Resource Type string `json:"type" yaml:"type"` - // Name is the name of the resource - Name string `json:"name" yaml:"name"` + // Name is a StringMatch that is used to select the resources to patch based on their name. + Name StringMatch `json:"name" yaml:"name"` // Patch defines the JSON Patch Operation Operation JSONPatchOperation `json:"operation" yaml:"operation"` } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index fd9e5ce9fe..5401fd58d4 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -2951,6 +2951,7 @@ func (in *JSONAccessLog) DeepCopy() *JSONAccessLog { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONPatchConfig) DeepCopyInto(out *JSONPatchConfig) { *out = *in + in.Name.DeepCopyInto(&out.Name) in.Operation.DeepCopyInto(&out.Operation) } diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go index 9848c1e012..32fb1837ad 100644 --- a/internal/xds/translator/jsonpatch.go +++ b/internal/xds/translator/jsonpatch.go @@ -28,15 +28,26 @@ import ( type typedName struct { Type string - Name string + Name ir.StringMatch } func (t typedName) String() string { - return fmt.Sprintf("%s/%s", t.Type, t.Name) + name := "" + switch { + case t.Name.Exact != nil: + name = *t.Name.Exact + case t.Name.Prefix != nil: + name = *t.Name.Prefix + "*" + case t.Name.Suffix != nil: + name = "*" + *t.Name.Suffix + case t.Name.SafeRegex != nil: + name = *t.Name.SafeRegex + } + return fmt.Sprintf("%s/%s", t.Type, name) } // processJSONPatches applies each JSONPatch to the Xds Resources for a specific type. -func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*ir.EnvoyPatchPolicy) error { +func processJSONPatches(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, envoyPatchPolicies []*ir.EnvoyPatchPolicy) error { var errs error for _, e := range envoyPatchPolicies { @@ -48,9 +59,8 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* for _, p := range e.JSONPatches { var ( - resourceJSON []byte - dest cachetypes.Resource - err error + dests []cachetypes.Resource + err error ) if err := p.Operation.Validate(); err != nil { @@ -96,61 +106,80 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* continue } - // find the resource to patch and convert it to JSON - dest, err = findXdsResource(tCtx, p) + // find the resources to patch and convert them to JSON + dests, err = findXdsResources(tCtx, gResources, p) if err != nil { - if errors.Is(err, errResourceNotFound) { - tn := typedName{p.Type, p.Name} - notFoundResources = append(notFoundResources, tn.String()) - continue - } - tErrs = errors.Join(tErrs, err) continue } - resourceJSON, err = jsonMarshalOpts.Marshal(dest) - if err != nil { - tErr := fmt.Errorf("unable to marshal xds resource %s, err: %w", p.Type, err) - tErrs = errors.Join(tErrs, tErr) + if len(dests) == 0 { + tn := typedName{p.Type, p.Name} + notFoundResources = append(notFoundResources, tn.String()) continue } - modifiedJSON, err := jsonpatch.ApplyJSONPatches(resourceJSON, p.Operation) - if err != nil { - tErrs = errors.Join(tErrs, err) - continue - } + var patchErrors error + var anyPatched bool + for _, dest := range dests { + var ( + resourceJSON []byte + modifiedJSON []byte + ) - // Unmarshal back to typed resource - // Use a temp staging variable that can be marshalled - // into and validated before saving it into the xds output resource - temp, err := getXdsResourceType(p.Type) - if err != nil { - tErrs = errors.Join(tErrs, err) - continue - } + resourceJSON, err = jsonMarshalOpts.Marshal(dest) + if err != nil { + tErr := fmt.Errorf("unable to marshal xds resource %s, err: %w", p.Type, err) + patchErrors = errors.Join(patchErrors, tErr) + continue + } - if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { - tErr := errors.New(unmarshalErrorMessage(err, string(modifiedJSON))) - tErrs = errors.Join(tErrs, tErr) - continue - } + modifiedJSON, err = jsonpatch.ApplyJSONPatches(resourceJSON, p.Operation) + if err != nil { + patchErrors = errors.Join(patchErrors, err) + continue + } - // Validate the patched resource - validator, ok := temp.(interface{ Validate() error }) - if ok { - if err = validator.Validate(); err != nil { - tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", p.Type, err.Error()) - tErrs = errors.Join(tErrs, tErr) + // Unmarshal back to typed resource + // Use a temp staging variable that can be marshalled + // into and validated before saving it into the xds output resource + temp, err := getXdsResourceType(p.Type) + if err != nil { + patchErrors = errors.Join(patchErrors, err) + continue + } + + if err = protojson.Unmarshal(modifiedJSON, temp); err != nil { + tErr := errors.New(unmarshalErrorMessage(err, string(modifiedJSON))) + patchErrors = errors.Join(patchErrors, tErr) + continue + } + + // Validate the patched resource + validator, ok := temp.(interface{ Validate() error }) + if ok { + if err = validator.Validate(); err != nil { + tErr := fmt.Errorf("validation failed for xds resource %s, err:%s", p.Type, err.Error()) + patchErrors = errors.Join(patchErrors, tErr) + continue + } + } + + if err = deepCopyPtr(temp, dest); err != nil { + tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", p.Type, err) + patchErrors = errors.Join(patchErrors, tErr) continue } + + // Mark that at least one dest has been patched successfully, + // so that we can report partial success if there are multiple dests and some of them fail + anyPatched = true } - if err = deepCopyPtr(temp, dest); err != nil { - tErr := fmt.Errorf("unable to copy xds resource %s, err: %w", p.Type, err) - tErrs = errors.Join(tErrs, tErr) - continue + // If there are multiple dests and some of them fail, + // consider it as successful and ignore the failures to patch other dests. + if !anyPatched && patchErrors != nil { + tErrs = errors.Join(tErrs, patchErrors) } } @@ -192,42 +221,29 @@ func getXdsResourceType(resourceType string) (cachetypes.Resource, error) { } } -var ( - errResourceNotFound = errors.New("resource not found") - jsonMarshalOpts = protojson.MarshalOptions{ - UseProtoNames: true, - } -) +var jsonMarshalOpts = protojson.MarshalOptions{ + UseProtoNames: true, +} -// findXdsResource return the XDS resource to patch -// TODO: return multiple resources -func findXdsResource(tCtx *types.ResourceVersionTable, p *ir.JSONPatchConfig) (cachetypes.Resource, error) { +// findXdsResources returns XDS resources to patch based on the patch configuration. +func findXdsResources(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, p *ir.JSONPatchConfig) ([]cachetypes.Resource, error) { + var resources []cachetypes.Resource switch p.Type { case resourcev3.ListenerType: - if r := findXdsListener(tCtx, p.Name); r != nil { - return r, nil - } + resources = findXdsListeners(tCtx, &p.Name) case resourcev3.RouteType: - if r := findXdsRouteConfig(tCtx, p.Name); r != nil { - return r, nil - } + resources = findXdsRouteConfigs(tCtx, &p.Name) case resourcev3.ClusterType: - if r := findXdsCluster(tCtx, p.Name); r != nil { - return r, nil - } + resources = findXdsClusters(tCtx, gResources, &p.Name) case resourcev3.EndpointType: - if r := findXdsEndpoint(tCtx, p.Name); r != nil { - return r, nil - } + resources = findXdsEndpoints(tCtx, gResources, &p.Name) case resourcev3.SecretType: - if r := findXdsSecret(tCtx, p.Name); r != nil { - return r, nil - } + resources = findXdsSecrets(tCtx, gResources, &p.Name) default: return nil, fmt.Errorf("unsupported patch type %s", p.Type) } - return nil, errResourceNotFound + return resources, nil } var unescaper = strings.NewReplacer(" ", " ") diff --git a/internal/xds/translator/listener_ready.go b/internal/xds/translator/listener_ready.go index f579d99533..8c4dabf32a 100644 --- a/internal/xds/translator/listener_ready.go +++ b/internal/xds/translator/listener_ready.go @@ -21,6 +21,11 @@ import ( "github.com/envoyproxy/gateway/internal/xds/filters" ) +const ( + readyListenerPrefix = "envoy-gateway-proxy-ready" + readyRouteName = "ready_route" +) + func buildReadyListener(ready *ir.ReadyListener) (*listenerv3.Listener, error) { ipv4Compact := ready.IPFamily == egv1a1.IPv6 || ready.IPFamily == egv1a1.DualStack @@ -41,10 +46,10 @@ func buildReadyListener(ready *ir.ReadyListener) (*listenerv3.Listener, error) { StatPrefix: "eg-ready-http", RouteSpecifier: &hcmv3.HttpConnectionManager_RouteConfig{ RouteConfig: &routev3.RouteConfiguration{ - Name: "ready_route", + Name: readyRouteName, VirtualHosts: []*routev3.VirtualHost{ { - Name: "ready_route", + Name: readyRouteName, Domains: []string{"*"}, Routes: []*routev3.Route{ { @@ -73,7 +78,7 @@ func buildReadyListener(ready *ir.ReadyListener) (*listenerv3.Listener, error) { } return &listenerv3.Listener{ - Name: fmt.Sprintf("envoy-gateway-proxy-ready-%s-%d", ready.Address, ready.Port), + Name: fmt.Sprintf("%s-%s-%d", readyListenerPrefix, ready.Address, ready.Port), Address: &corev3.Address{ Address: &corev3.Address_SocketAddress{ SocketAddress: &corev3.SocketAddress{ diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml index 0a94f0a630..a049830969 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-empty-jsonpath.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: second-listener + name: + exact: "second-listener" operation: op: add value: diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-without-value.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-without-value.yaml index 43eeaa082c..a8582e4ccd 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-without-value.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-add-op-without-value.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/filter_chains/0/filters/0/typed_config/http_filters/0" @@ -28,7 +29,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/virtual_hosts/0/rate_limits" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-listener.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-listener.yaml index f4d967332a..ae9ae5c102 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-listener.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-listener.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/default_filter_chain/filters/0/typed_config/http_filters/0" @@ -28,7 +29,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/virtual_hosts/0/rate_limits" @@ -36,7 +38,8 @@ envoyPatchPolicies: - actions: - remote_address: {} - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" - name: rate-limit-cluster + name: + exact: "rate-limit-cluster" operation: op: add path: "" @@ -56,7 +59,8 @@ envoyPatchPolicies: address: ratelimit.svc.cluster.local port_value: 8081 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route" + name: + exact: "first-route" operation: op: "replace" path: "/endpoints/0/load_balancing_weight" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-patch.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-patch.yaml index 13a0929f10..6b18b18ee1 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-patch.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid-patch.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/this/path/never/existed" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid.yaml index f625113112..56301f2124 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-invalid.yaml @@ -5,7 +5,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/default_filter_chain/filters/0/typed_config/http_filters/0" @@ -22,7 +23,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/virtual_hosts/0/rate_limits" @@ -30,7 +32,8 @@ envoyPatchPolicies: - actions: - remote_address: {} - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" - name: rate-limit-cluster + name: + exact: "rate-limit-cluster" operation: op: add path: "" @@ -50,7 +53,8 @@ envoyPatchPolicies: address: ratelimit.svc.cluster.local port_value: 8081 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route" + name: + exact: "first-route" operation: op: "replace" path: "/endpoints/0/load_balancing_weight" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-missing-resource.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-missing-resource.yaml index 26f4e802f3..0a3d74fdd8 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-missing-resource.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-missing-resource.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "non-existing-listener" + name: + exact: "non-existing-listener" operation: op: "add" path: "/default_filter_chain/filters/0/typed_config/http_filters/0" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-move-op-with-value.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-move-op-with-value.yaml index fb49a07ec2..e248277c47 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-move-op-with-value.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-move-op-with-value.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/filter_chains/0/filters/0/typed_config/http_filters/0" @@ -28,7 +29,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "remove" from: "/filter_chains/0/filters/0/typed_config/http_filters/0" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml new file mode 100644 index 0000000000..cd859d3db8 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml @@ -0,0 +1,192 @@ +envoyPatchPolicies: + - status: + ancestors: + - ancestorRef: + group: "gateway.networking.k8s.io" + kind: "Gateway" + namespace: "envoy-gateway" + name: "gateway-1" + name: "first-policy" + namespace: "default" + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: + safeRegex: ".*" + operation: + op: add + path: "/filter_chains/0/filters/0/typed_config/preserve_external_request_id" + value: true + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: + safeRegex: ".*" + operation: + op: "add" + path: "/filter_chains/0/filters/0/typed_config/http_filters/0" + value: + name: "envoy.filters.http.ratelimit" + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit" + domain: "eg-ratelimit" + failure_mode_deny: true + timeout: 1s + rate_limit_service: + grpc_service: + envoy_grpc: + cluster_name: rate-limit-cluster + transport_api_version: V3 + - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" + name: + safeRegex: ".*" + operation: + op: "add" + path: "/virtual_hosts/0/rate_limits" + value: + - actions: + - remote_address: {} + - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" + name: + safeRegex: ".*" + operation: + op: add + path: "/altStatName" + value: altStatName + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: + exact: "first-route-dest" + operation: + op: "replace" + path: "/endpoints/0/load_balancing_weight" + value: "50" + - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" + name: + safeRegex: ".*" + operation: + op: "replace" + path: "/tls_certificate/certificate_chain/inline_bytes" + # replaced-secret + value: "cmVwbGFjZWQtc2VjcmV0" + - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" + name: + exact: "test-secret" + operation: + op: "add" + path: "" + value: + name: test_secret + tls_certificate: + certificate_chain: + inline_bytes: Y2VydC1kYXRh + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: + exact: "first-route-dest" + operation: + op: add + path: "/endpoints/1" + value: + lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: + prefix: "first-route" + operation: + op: "move" + from: "/endpoints/0/load_balancing_weight" + path: "/endpoints/1/load_balancing_weight" + - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" + name: + prefix: "first-route" + operation: + op: copy + from: "/endpoints/1/load_balancing_weight" + path: "/endpoints/0/load_balancing_weight" +http: + - name: "first-listener" + address: 0.0.0.0 + port: 10443 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + tls: + alpnProtocols: + - h2 + - http/1.1 + certificates: + - name: secret-1 + # byte slice representation of "key-data" + certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + # byte slice representation of "key-data" + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + - name: secret-2 + certificate: [99, 101, 114, 116, 45, 100, 97, 116, 97] + privateKey: [107, 101, 121, 45, 100, 97, 116, 97] + routes: + - name: "first-route" + hostname: "*" + headerMatches: + - name: user + exact: "jason" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "first-route-dest/backend/0" + - name: "second-listener" + address: 0.0.0.0 + port: 10080 + hostnames: + - "*" + path: + mergeSlashes: true + escapedSlashesAction: UnescapeAndRedirect + routes: + - name: "second-route" + hostname: "*" + headerMatches: + - name: user + exact: "jason" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + name: "second-route-dest/backend/0" +readyListener: + address: 0.0.0.0 + ipFamily: IPv4 + path: /ready + port: 19001 +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 diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml index e0abd3ef66..70cdf4152f 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath-invalid.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: "replace" jsonPath: "..doesNotExists" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml index 39e9bb67dd..a98e4d5b21 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml @@ -11,14 +11,16 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: first-listener + name: + exact: first-listener operation: op: "add" jsonPath: "$.filter_chains[0].filters[0].typed_config" path: "/preserve_external_request_id" value: true - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" jsonPath: "filter_chains[0].filters[0].typed_config.http_filters[0]" @@ -35,7 +37,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" jsonPath: "virtual_hosts[0]" @@ -44,7 +47,8 @@ envoyPatchPolicies: - actions: - remote_address: {} - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "replace" jsonPath: "..routes[?(@.name=='second-route')].route.upgrade_configs" @@ -52,7 +56,8 @@ envoyPatchPolicies: - upgrade_type: CONNECT connect_config: {} - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" - name: rate-limit-cluster + name: + exact: "rate-limit-cluster" operation: op: add path: "" @@ -72,19 +77,22 @@ envoyPatchPolicies: address: ratelimit.svc.cluster.local port_value: 8081 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: "replace" jsonPath: "..endpoints[*].load_balancing_weight" value: "50" - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" - name: "secret-1" + name: + exact: "secret-1" operation: op: "replace" jsonPath: "$.tls_certificate.certificate_chain.inline_bytes" value: "a2V5LWRhdGE=" - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" - name: "test-secret" + name: + exact: "test-secret" operation: op: "add" path: "" @@ -94,7 +102,8 @@ envoyPatchPolicies: certificate_chain: inline_bytes: Y2VydC1kYXRh - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: add jsonPath: "endpoints" @@ -108,13 +117,15 @@ envoyPatchPolicies: portValue: 50000 loadBalancingWeight: 1 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: "move" from: "/endpoints/0/load_balancing_weight" path: "/endpoints/1/load_balancing_weight" - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: copy from: "/endpoints/1/load_balancing_weight" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch.yaml index 21f2cdd59d..18394ba3f6 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch.yaml @@ -11,13 +11,15 @@ envoyPatchPolicies: generation: 1 jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: first-listener + name: + exact: first-listener operation: op: add path: "/filter_chains/0/filters/0/typed_config/preserve_external_request_id" value: true - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/filter_chains/0/filters/0/typed_config/http_filters/0" @@ -34,7 +36,8 @@ envoyPatchPolicies: cluster_name: rate-limit-cluster transport_api_version: V3 - type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration" - name: "first-listener" + name: + exact: "first-listener" operation: op: "add" path: "/virtual_hosts/0/rate_limits" @@ -42,7 +45,8 @@ envoyPatchPolicies: - actions: - remote_address: {} - type: "type.googleapis.com/envoy.config.cluster.v3.Cluster" - name: rate-limit-cluster + name: + exact: rate-limit-cluster operation: op: add path: "" @@ -62,19 +66,22 @@ envoyPatchPolicies: address: ratelimit.svc.cluster.local port_value: 8081 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: "replace" path: "/endpoints/0/load_balancing_weight" value: "50" - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" - name: "secret-1" + name: + exact: "secret-1" operation: op: "replace" path: "/tls_certificate/certificate_chain/inline_bytes" value: "a2V5LWRhdGE=" - type: "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" - name: "test-secret" + name: + exact: "test-secret" operation: op: "add" path: "" @@ -84,7 +91,8 @@ envoyPatchPolicies: certificate_chain: inline_bytes: Y2VydC1kYXRh - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: add path: "/endpoints/1" @@ -97,13 +105,15 @@ envoyPatchPolicies: portValue: 50000 loadBalancingWeight: 1 - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: "move" from: "/endpoints/0/load_balancing_weight" path: "/endpoints/1/load_balancing_weight" - type: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment" - name: "first-route-dest" + name: + exact: "first-route-dest" operation: op: copy from: "/endpoints/1/load_balancing_weight" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml new file mode 100644 index 0000000000..79ce50bbf1 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml @@ -0,0 +1,80 @@ +- altStatName: altStatName + circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: first-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- altStatName: altStatName + circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + localityWeightedLbConfig: {} + name: second-route-dest + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: envoy-gateway/gateway-1 + ignoreHealthOnHostRemoval: true + loadBalancingPolicy: + policies: + - typedExtensionConfig: + name: envoy.load_balancing_policies.least_request + typedConfig: + '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest + localityLbConfig: + zoneAwareLbConfig: + minClusterSize: "1" + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" + name: envoy-gateway/gateway-1 + perConnectionBufferLimitBytes: 32768 + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.endpoints.yaml new file mode 100644 index 0000000000..56d9a5cb33 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.endpoints.yaml @@ -0,0 +1,51 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 50 + locality: + region: first-route-dest/backend/0 + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 50 +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: second-route-dest/backend/0 +- clusterName: envoy-gateway/gateway-1 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.6.5.4 + portValue: 8080 + loadBalancingWeight: 1 + locality: + zone: zone1 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Service + name: envoy-envoy-gateway-gateway-1-196ae069 + namespace: envoy-gateway-system + sectionName: "8080" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml new file mode 100644 index 0000000000..61f71d060b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml @@ -0,0 +1,16 @@ +- name: first-policy + namespace: default + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Patches have been successfully applied. + reason: Programmed + status: "True" + type: Programmed + controllerName: "" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml new file mode 100644 index 0000000000..16752bd98c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml @@ -0,0 +1,137 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 19001 + bypassOverloadManager: true + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + httpFilters: + - name: envoy.filters.http.health_check + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck + headers: + - name: :path + stringMatch: + exact: /ready + passThroughMode: false + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + routeConfig: + name: ready_route + virtualHosts: + - domains: + - '*' + name: ready_route + routes: + - directResponse: + status: 500 + match: + prefix: / + statPrefix: eg-ready-http + name: envoy-gateway-proxy-ready-0.0.0.0-19001 +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10443 + filterChains: + - filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: eg-ratelimit + failureModeDeny: true + rateLimitService: + grpcService: + envoyGrpc: + clusterName: rate-limit-cluster + transportApiVersion: V3 + timeout: 1s + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + preserveExternalRequestId: true + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: https-10443 + useRemoteAddress: true + name: first-listener + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + alpnProtocols: + - h2 + - http/1.1 + tlsCertificateSdsSecretConfigs: + - name: secret-1 + sdsConfig: + ads: {} + resourceApiVersion: V3 + - name: secret-2 + sdsConfig: + ads: {} + resourceApiVersion: V3 + disableStatefulSessionResumption: true + disableStatelessSessionResumption: true + maxConnectionsToAcceptPerSocketEvent: 1 + name: first-listener + perConnectionBufferLimitBytes: 32768 +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: second-listener + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: second-listener + maxConnectionsToAcceptPerSocketEvent: 1 + name: second-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.routes.yaml new file mode 100644 index 0000000000..23cc017226 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.routes.yaml @@ -0,0 +1,42 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + rateLimits: + - actions: + - remoteAddress: {} + routes: + - match: + headers: + - name: user + stringMatch: + exact: jason + prefix: / + name: first-route + route: + cluster: first-route-dest + upgradeConfigs: + - upgradeType: websocket +- ignorePortInHostMatching: true + name: second-listener + virtualHosts: + - domains: + - '*' + name: second-listener/* + rateLimits: + - actions: + - remoteAddress: {} + routes: + - match: + headers: + - name: user + stringMatch: + exact: jason + prefix: / + name: second-route + route: + cluster: second-route-dest + upgradeConfigs: + - upgradeType: websocket diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml new file mode 100644 index 0000000000..9b4c5e3b2c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml @@ -0,0 +1,22 @@ +- name: secret-1 + tlsCertificate: + certificateChain: + inlineBytes: cmVwbGFjZWQtc2VjcmV0 + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: secret-2 + tlsCertificate: + certificateChain: + inlineBytes: cmVwbGFjZWQtc2VjcmV0 + privateKey: + inlineBytes: a2V5LWRhdGE= +- name: envoy-gateway-system/envoy + tlsCertificate: + certificateChain: + inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + privateKey: + inlineBytes: W3JlZGFjdGVkXQ== +- name: test_secret + tlsCertificate: + certificateChain: + inlineBytes: Y2VydC1kYXRh diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index ec4272e49d..06bc16650e 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -8,6 +8,7 @@ package translator import ( "errors" "fmt" + "regexp" "strings" "time" @@ -19,6 +20,7 @@ import ( hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + cachetypes "github.com/envoyproxy/go-control-plane/pkg/cache/types" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" protobuf "google.golang.org/protobuf/proto" @@ -158,7 +160,7 @@ func (t *Translator) Translate(xdsIR *ir.Xds) (*types.ResourceVersionTable, erro } // All XDS resources is ready, let's do the patch. - if err := processJSONPatches(tCtx, xdsIR.EnvoyPatchPolicies); err != nil { + if err := processJSONPatches(tCtx, xdsIR.GlobalResources, xdsIR.EnvoyPatchPolicies); err != nil { // Since JSONPatch error is user-triggered, we don't fail the entire xDS translation so that the remaining // valid xDS resources can be sent to the proxy. t.Logger.Error(err, "Failed to process JSON patches") @@ -995,95 +997,159 @@ func findXdsListenerByHostPort(tCtx *types.ResourceVersionTable, address string, return nil } -// findXdsListener finds a xds listener with the same name and returns nil if there is no match. -func findXdsListener(tCtx *types.ResourceVersionTable, name string) *listenerv3.Listener { +// findXdsListeners finds xds listeners. +func findXdsListeners(tCtx *types.ResourceVersionTable, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.ListenerType] == nil { return nil } + var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.ListenerType] { listener, ok := r.(*listenerv3.Listener) - if ok && listener.Name == name { - return listener + if !ok { + continue + } + if strings.HasPrefix(listener.Name, readyListenerPrefix) { + continue + } + if stringMatched(name, listener.Name) { + result = append(result, r) } } - return nil + return result } // findXdsRouteConfig finds a xds route with the name and returns nil if there is no match. func findXdsRouteConfig(tCtx *types.ResourceVersionTable, name string) *routev3.RouteConfiguration { + resources := findXdsRouteConfigs(tCtx, &ir.StringMatch{Exact: &name}) + if len(resources) > 0 { + return resources[0].(*routev3.RouteConfiguration) + } + return nil +} + +// findXdsRouteConfigs finds xds route configurations. +func findXdsRouteConfigs(tCtx *types.ResourceVersionTable, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.RouteType] == nil { return nil } + var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.RouteType] { route, ok := r.(*routev3.RouteConfiguration) - if ok && route.Name == name { - return route + if !ok { + continue + } + if stringMatched(name, route.Name) { + result = append(result, r) } } - return nil + return result } // findXdsCluster finds a xds cluster with the same name, and returns nil if there is no match. func findXdsCluster(tCtx *types.ResourceVersionTable, name string) *clusterv3.Cluster { + resources := findXdsClusters(tCtx, nil, &ir.StringMatch{Exact: &name}) + if len(resources) > 0 { + return resources[0].(*clusterv3.Cluster) + } + return nil +} + +// findXdsClusters finds xds clusters. +func findXdsClusters(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.ClusterType] == nil { return nil } + skippedClusters := sets.New(getRateLimitServiceClusterName(), wasmHTTPServiceClusterName) + // skip. proxy service cluster + if gResources != nil && gResources.ProxyServiceCluster != nil { + skippedClusters.Insert(gResources.ProxyServiceCluster.Name) + } + var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.ClusterType] { cluster, ok := r.(*clusterv3.Cluster) - if ok && cluster.Name == name { - return cluster + if !ok { + continue + } + if skippedClusters.Has(cluster.Name) { + continue + } + if stringMatched(name, cluster.Name) { + result = append(result, r) } } - return nil + return result } -// findXdsEndpoint finds a xds endpoint with the same cluster name, and returns nil if there is no match. -func findXdsEndpoint(tCtx *types.ResourceVersionTable, name string) *endpointv3.ClusterLoadAssignment { +// findXdsEndpoints finds xds endpoints. +func findXdsEndpoints(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.EndpointType] == nil { return nil } - + skippedClusters := sets.New(getRateLimitServiceClusterName(), wasmHTTPServiceClusterName) + // skip. proxy service cluster + if gResources != nil && gResources.ProxyServiceCluster != nil { + skippedClusters.Insert(gResources.ProxyServiceCluster.Name) + } + var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.EndpointType] { endpoint, ok := r.(*endpointv3.ClusterLoadAssignment) - if ok && endpoint.ClusterName == name { - return endpoint + if !ok { + continue + } + if skippedClusters.Has(endpoint.ClusterName) { + continue + } + if stringMatched(name, endpoint.ClusterName) { + result = append(result, r) } } - return nil -} - -// processXdsCluster processes xds cluster with args per route. -func processXdsCluster(tCtx *types.ResourceVersionTable, - name string, - settings []*ir.DestinationSetting, - route clusterArgs, - extras *ExtraArgs, - metadata *ir.ResourceMetadata, -) error { - return addXdsCluster(tCtx, route.asClusterArgs(name, settings, extras, metadata)) + return result } // findXdsSecret finds a xds secret with the same name, and returns nil if there is no match. func findXdsSecret(tCtx *types.ResourceVersionTable, name string) *tlsv3.Secret { + resources := findXdsSecrets(tCtx, nil, &ir.StringMatch{Exact: &name}) + if len(resources) > 0 { + return resources[0].(*tlsv3.Secret) + } + return nil +} + +// findXdsSecrets finds xds secrets. +func findXdsSecrets(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.SecretType] == nil { return nil } - + // skip secrets that are used for + skippedSecrets := sets.New( + "xds_trusted_ca", "jwt-sa-bearer", + ) + // envoy client certificate + if gResources != nil && gResources.EnvoyClientCertificate != nil { + skippedSecrets.Insert(gResources.EnvoyClientCertificate.Name) + } + var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.SecretType] { secret, ok := r.(*tlsv3.Secret) - if ok && secret.Name == name { - return secret + if !ok { + continue + } + if skippedSecrets.Has(secret.Name) { + continue + } + if stringMatched(name, secret.Name) { + result = append(result, r) } } - return nil + return result } // addXdsSecret adds a xds secret with args. @@ -1100,6 +1166,17 @@ func addXdsSecret(tCtx *types.ResourceVersionTable, secret *tlsv3.Secret) error return nil } +// processXdsCluster processes xds cluster with args per route. +func processXdsCluster(tCtx *types.ResourceVersionTable, + name string, + settings []*ir.DestinationSetting, + route clusterArgs, + extras *ExtraArgs, + metadata *ir.ResourceMetadata, +) error { + return addXdsCluster(tCtx, route.asClusterArgs(name, settings, extras, metadata)) +} + // addXdsCluster adds a xds cluster with args. // If the cluster already exists, it skips adding the cluster and returns nil. func addXdsCluster(tCtx *types.ResourceVersionTable, args *xdsClusterArgs) error { @@ -1360,3 +1437,26 @@ func processClientCertificates(tCtx *types.ResourceVersionTable, settings []*ir. } return errs } + +func stringMatched(matcher *ir.StringMatch, str string) bool { + if matcher == nil { + return false + } + if matcher.Exact != nil { + return *matcher.Exact == str + } + if matcher.Prefix != nil { + return strings.HasPrefix(str, *matcher.Prefix) + } + if matcher.Suffix != nil { + return strings.HasSuffix(str, *matcher.Suffix) + } + if matcher.SafeRegex != nil { + regex, err := regexp.Compile(*matcher.SafeRegex) + if err != nil { + return false + } + return regex.MatchString(str) + } + return false +} diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index cbf96c3cdd..c2c823e4c3 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -85,6 +85,9 @@ func TestTranslateXds(t *testing.T) { "jsonpatch-move-op-with-value": { requireEnvoyPatchPolicies: true, }, + "jsonpatch-patch-multiple-resources": { + requireEnvoyPatchPolicies: true, + }, "http-route-invalid": { errMsg: "validation failed for xds resource", }, diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 66d6c6cd5a..3337e8b493 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2051,7 +2051,7 @@ _Appears in:_ EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource -using JSONPatch semantic +using JSONPatch semantics. _Appears in:_ - [EnvoyPatchPolicySpec](#envoypatchpolicyspec) @@ -2059,7 +2059,8 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `type` | _[EnvoyResourceType](#envoyresourcetype)_ | true | | Type is the typed URL of the Envoy xDS Resource | -| `name` | _string_ | true | | Name is the name of the resource | +| `name` | _string_ | false | | Name is the name of the resource. | +| `nameSelector` | _[StringMatch](#stringmatch)_ | false | | NameSelector is a StringMatch that is used to select the resources to patch based on their name. | | `operation` | _[JSONPatchOperation](#jsonpatchoperation)_ | true | | Patch defines the JSON Patch Operation | @@ -6084,6 +6085,7 @@ This is a general purpose match condition that can be used by other EG APIs that need to match against a string. _Appears in:_ +- [EnvoyJSONPatchConfig](#envoyjsonpatchconfig) - [HTTP1Settings](#http1settings) - [HTTPHeaderFilter](#httpheaderfilter) - [OIDCDenyRedirectHeader](#oidcdenyredirectheader) diff --git a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md index ae08eb26ac..8ea6b253d1 100644 --- a/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md +++ b/site/content/en/latest/tasks/extensibility/envoy-patch-policy.md @@ -463,15 +463,8 @@ applied to Envoy Proxy. apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyPatchPolicy metadata: - annotations: - kubectl.kubernetes.io/last-applied-configuration: | - {"apiVersion":"gateway.envoyproxy.io/v1alpha1","kind":"EnvoyPatchPolicy","metadata":{"annotations":{},"name":"custom-response-patch-policy","namespace":"default"},"spec":{"jsonPatches":[{"name":"default/eg/http","operation":{"op":"add","path":"/default_filter_chain/filters/0/typed_config/local_reply_config","value":{"mappers":[{"body":{"inline_string":"could not find what you are looking for"},"filter":{"status_code_filter":{"comparison":{"op":"EQ","value":{"default_value":404}}}}}]}},"type":"type.googleapis.com/envoy.config.listener.v3.Listener"}],"priority":0,"targetRef":{"group":"gateway.networking.k8s.io","kind":"Gateway","name":"eg","namespace":"default"},"type":"JSONPatch"}} - creationTimestamp: "2023-07-31T21:47:53Z" - generation: 1 name: custom-response-patch-policy namespace: default - resourceVersion: "10265" - uid: a35bda6e-a0cc-46d7-a63a-cee765174bc3 spec: jsonPatches: - name: default/eg/http diff --git a/test/cel-validation/envoypatchpolicy_test.go b/test/cel-validation/envoypatchpolicy_test.go new file mode 100644 index 0000000000..61dafd9a3e --- /dev/null +++ b/test/cel-validation/envoypatchpolicy_test.go @@ -0,0 +1,155 @@ +// 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 celvalidation + +package celvalidation + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestEnvoyPatchPolicy(t *testing.T) { + ctx := context.Background() + baseEPP := egv1a1.EnvoyPatchPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "epp", + Namespace: metav1.NamespaceDefault, + }, + Spec: egv1a1.EnvoyPatchPolicySpec{}, + } + + listenerName := "listener-1" + patchPath := "/name" + + cases := []struct { + desc string + mutate func(epp *egv1a1.EnvoyPatchPolicy) + wantErrors []string + }{ + { + desc: "valid json patch with name only", + mutate: func(epp *egv1a1.EnvoyPatchPolicy) { + epp.Spec = egv1a1.EnvoyPatchPolicySpec{ + Type: egv1a1.JSONPatchEnvoyPatchType, + JSONPatches: []egv1a1.EnvoyJSONPatchConfig{ + { + Type: egv1a1.ListenerEnvoyResourceType, + Name: &listenerName, + Operation: egv1a1.JSONPatchOperation{ + Op: egv1a1.JSONPatchOperationType("test"), + Path: &patchPath, + Value: &apiextensionsv1.JSON{ + Raw: []byte(`"listener-1"`), + }, + }, + }, + }, + TargetRef: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group(gwapiv1.GroupName), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "valid json patch with nameSelector only", + mutate: func(epp *egv1a1.EnvoyPatchPolicy) { + epp.Spec = egv1a1.EnvoyPatchPolicySpec{ + Type: egv1a1.JSONPatchEnvoyPatchType, + JSONPatches: []egv1a1.EnvoyJSONPatchConfig{ + { + Type: egv1a1.ListenerEnvoyResourceType, + NameSelector: &egv1a1.StringMatch{ + Value: listenerName, + }, + Operation: egv1a1.JSONPatchOperation{ + Op: egv1a1.JSONPatchOperationType("test"), + Path: &patchPath, + Value: &apiextensionsv1.JSON{ + Raw: []byte(`"listener-1"`), + }, + }, + }, + }, + TargetRef: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group(gwapiv1.GroupName), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "invalid json patch with both name and nameSelector", + mutate: func(epp *egv1a1.EnvoyPatchPolicy) { + epp.Spec = egv1a1.EnvoyPatchPolicySpec{ + Type: egv1a1.JSONPatchEnvoyPatchType, + JSONPatches: []egv1a1.EnvoyJSONPatchConfig{ + { + Type: egv1a1.ListenerEnvoyResourceType, + Name: &listenerName, + NameSelector: &egv1a1.StringMatch{ + Value: listenerName, + }, + Operation: egv1a1.JSONPatchOperation{ + Op: egv1a1.JSONPatchOperationType("test"), + Path: &patchPath, + Value: &apiextensionsv1.JSON{ + Raw: []byte(`"listener-1"`), + }, + }, + }, + }, + TargetRef: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group(gwapiv1.GroupName), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + } + }, + wantErrors: []string{"only one of name and nameSelector can be specified"}, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + epp := baseEPP.DeepCopy() + epp.Name = fmt.Sprintf("epp-%v", time.Now().UnixNano()) + + if tc.mutate != nil { + tc.mutate(epp) + } + + err := c.Create(ctx, epp) + if (len(tc.wantErrors) != 0) != (err != nil) { + t.Fatalf("Unexpected response while creating EnvoyPatchPolicy; got err=\n%v\n;want error=%v", err, tc.wantErrors) + } + + var missingErrorStrings []string + for _, wantError := range tc.wantErrors { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) { + missingErrorStrings = append(missingErrorStrings, wantError) + } + } + if len(missingErrorStrings) != 0 { + t.Errorf("Unexpected response while creating EnvoyPatchPolicy; got err=\n%v\n;missing strings within error=%q", err, missingErrorStrings) + } + }) + } +} diff --git a/test/e2e/testdata/envoy-patch-policy-xds-name-scheme-v2.yaml b/test/e2e/testdata/envoy-patch-policy-xds-name-scheme-v2.yaml index f0ede72827..1f6a2a5407 100644 --- a/test/e2e/testdata/envoy-patch-policy-xds-name-scheme-v2.yaml +++ b/test/e2e/testdata/envoy-patch-policy-xds-name-scheme-v2.yaml @@ -14,7 +14,7 @@ spec: matches: - path: type: PathPrefix - value: /foo + value: /epp --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyPatchPolicy diff --git a/test/e2e/testdata/envoy-patch-policy.yaml b/test/e2e/testdata/envoy-patch-policy.yaml index ede3800d45..d4fab6c3fc 100644 --- a/test/e2e/testdata/envoy-patch-policy.yaml +++ b/test/e2e/testdata/envoy-patch-policy.yaml @@ -6,15 +6,15 @@ metadata: namespace: gateway-conformance-infra spec: parentRefs: - - name: same-namespace + - name: same-namespace rules: - - backendRefs: - - name: infra-backend-v1 - port: 8080 - matches: - - path: - type: PathPrefix - value: /foo + - backendRefs: + - name: infra-backend-v1 + port: 8080 + matches: + - path: + type: PathPrefix + value: /epp --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyPatchPolicy @@ -28,20 +28,109 @@ spec: name: same-namespace type: JSONPatch jsonPatches: - - type: "type.googleapis.com/envoy.config.listener.v3.Listener" - name: "gateway-conformance-infra/same-namespace/http" - operation: - op: add - path: "/default_filter_chain/filters/0/typed_config/local_reply_config" - value: - mappers: - - filter: - status_code_filter: - comparison: - op: EQ - value: - default_value: 404 - runtime_key: key_b - status_code: 406 - body: - inline_string: "not acceptable" + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + name: "gateway-conformance-infra/same-namespace/http" + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "not acceptable" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: epp-gateways + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: http-8080 + port: 8080 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: epp-http + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: epp-gateways + sectionName: http + rules: + - backendRefs: + - name: infra-backend-v2 + port: 8080 + matches: + - path: + type: PathPrefix + value: /epp +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: epp-http-8080 + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: epp-gateways + sectionName: http-8080 + rules: + - backendRefs: + - name: infra-backend-v3 + port: 8080 + matches: + - path: + type: PathPrefix + value: /epp +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: EnvoyPatchPolicy +metadata: + name: custom-response-all-listeners + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: epp-gateways + type: JSONPatch + jsonPatches: + - type: "type.googleapis.com/envoy.config.listener.v3.Listener" + nameSelector: + type: RegularExpression + value: ".*" + operation: + op: add + path: "/default_filter_chain/filters/0/typed_config/local_reply_config" + value: + mappers: + - filter: + status_code_filter: + comparison: + op: EQ + value: + default_value: 404 + runtime_key: key_b + status_code: 406 + body: + inline_string: "not acceptable" diff --git a/test/e2e/tests/envoy_patch_policy.go b/test/e2e/tests/envoy_patch_policy.go index 1d8affb1b5..31407f2c2f 100644 --- a/test/e2e/tests/envoy_patch_policy.go +++ b/test/e2e/tests/envoy_patch_policy.go @@ -8,6 +8,7 @@ package tests import ( + stdnet "net" "testing" "k8s.io/apimachinery/pkg/types" @@ -26,33 +27,45 @@ var EnvoyPatchPolicyTest = suite.ConformanceTest{ Description: "update xds using EnvoyPatchPolicy", Manifests: []string{"testdata/envoy-patch-policy.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { - t.Run("envoy patch policy", func(t *testing.T) { - testEnvoyPatchPolicy(t, suite) + t.Run("With name", func(t *testing.T) { + testEnvoyPatchPolicy(t, suite, "same-namespace", "http-envoy-patch-policy", "infra-backend-v1") + }) + t.Run("Without name", func(t *testing.T) { + testEnvoyPatchPolicy(t, suite, "epp-gateways", "epp-http", "infra-backend-v2") + testEnvoyPatchPolicyWithPort(t, suite, "epp-gateways", "epp-http-8080", "infra-backend-v3", "8080") }) }, } -func testEnvoyPatchPolicy(t *testing.T, suite *suite.ConformanceTestSuite) { +func testEnvoyPatchPolicy(t *testing.T, suite *suite.ConformanceTestSuite, gwName, routeName, backendName string) { + testEnvoyPatchPolicyWithPort(t, suite, gwName, routeName, backendName, "") +} + +func testEnvoyPatchPolicyWithPort(t *testing.T, suite *suite.ConformanceTestSuite, gwName, routeName, backendName, gwPort string) { ns := "gateway-conformance-infra" - routeNN := types.NamespacedName{Name: "http-envoy-patch-policy", Namespace: ns} - gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + routeNN := types.NamespacedName{Name: routeName, Namespace: ns} + gwNN := types.NamespacedName{Name: gwName, Namespace: ns} gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gwapiv1.HTTPRoute{}, false, routeNN) - OkResp := http.ExpectedResponse{ + if gwPort != "" { + gwAddr = stdnet.JoinHostPort(gwAddr, gwPort) + } + okResp := http.ExpectedResponse{ Request: http.Request{ - Path: "/foo", + Path: "/epp", }, Response: http.Response{ StatusCodes: []int{200}, }, + Backend: backendName, Namespace: ns, } // Send a request to a valid path and expect a successful response - http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, OkResp) + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, okResp) customResp := http.ExpectedResponse{ Request: http.Request{ - Path: "/bar", + Path: "/not-exist-path", }, Response: http.Response{ StatusCodes: []int{406}, diff --git a/test/e2e/tests/envoy_patch_policy_xds_name_scheme_v2.go b/test/e2e/tests/envoy_patch_policy_xds_name_scheme_v2.go index b7c8ce47d5..dda08620af 100644 --- a/test/e2e/tests/envoy_patch_policy_xds_name_scheme_v2.go +++ b/test/e2e/tests/envoy_patch_policy_xds_name_scheme_v2.go @@ -23,7 +23,7 @@ var EnvoyPatchPolicyXDSNameSchemeV2Test = suite.ConformanceTest{ Manifests: []string{"testdata/envoy-patch-policy-xds-name-scheme-v2.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("envoy patch policy", func(t *testing.T) { - testEnvoyPatchPolicy(t, suite) + testEnvoyPatchPolicy(t, suite, "same-namespace", "http-envoy-patch-policy", "infra-backend-v1") }) }, } diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 31f9836eeb..9766485018 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -33307,11 +33307,33 @@ spec: items: description: |- EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource - using JSONPatch semantic + using JSONPatch semantics. properties: name: - description: Name is the name of the resource + description: Name is the name of the resource. type: string + nameSelector: + description: NameSelector is a StringMatch that is used to select + the resources to patch based on their name. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object operation: description: Patch defines the JSON Patch Operation properties: @@ -33363,10 +33385,12 @@ spec: - type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret type: string required: - - name - operation - type type: object + x-kubernetes-validations: + - message: only one of name and nameSelector can be specified + rule: '!(has(self.name) && has(self.nameSelector))' type: array priority: description: |- diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 02b6065f79..475d693dc7 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -9245,11 +9245,33 @@ spec: items: description: |- EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource - using JSONPatch semantic + using JSONPatch semantics. properties: name: - description: Name is the name of the resource + description: Name is the name of the resource. type: string + nameSelector: + description: NameSelector is a StringMatch that is used to select + the resources to patch based on their name. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object operation: description: Patch defines the JSON Patch Operation properties: @@ -9301,10 +9323,12 @@ spec: - type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret type: string required: - - name - operation - type type: object + x-kubernetes-validations: + - message: only one of name and nameSelector can be specified + rule: '!(has(self.name) && has(self.nameSelector))' type: array priority: description: |- 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 0b1d7764f0..b05a3f4d70 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -9245,11 +9245,33 @@ spec: items: description: |- EnvoyJSONPatchConfig defines the configuration for patching a Envoy xDS Resource - using JSONPatch semantic + using JSONPatch semantics. properties: name: - description: Name is the name of the resource + description: Name is the name of the resource. type: string + nameSelector: + description: NameSelector is a StringMatch that is used to select + the resources to patch based on their name. + properties: + type: + default: Exact + description: Type specifies how to match against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string value that the match + must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - value + type: object operation: description: Patch defines the JSON Patch Operation properties: @@ -9301,10 +9323,12 @@ spec: - type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret type: string required: - - name - operation - type type: object + x-kubernetes-validations: + - message: only one of name and nameSelector can be specified + rule: '!(has(self.name) && has(self.nameSelector))' type: array priority: description: |- From ba9e88259af1abb2e05cd642f4e5ab1129515815 Mon Sep 17 00:00:00 2001 From: zirain Date: Tue, 14 Apr 2026 20:10:30 +0800 Subject: [PATCH 2/5] fix Signed-off-by: zirain --- api/v1alpha1/envoypatchpolicy_types.go | 1 + ...eway.envoyproxy.io_envoypatchpolicies.yaml | 2 ++ ...eway.envoyproxy.io_envoypatchpolicies.yaml | 2 ++ internal/gatewayapi/envoypatchpolicy.go | 14 +++++++--- test/cel-validation/envoypatchpolicy_test.go | 26 +++++++++++++++++++ test/helm/gateway-crds-helm/all.out.yaml | 2 ++ test/helm/gateway-crds-helm/e2e.out.yaml | 2 ++ .../envoy-gateway-crds.out.yaml | 2 ++ 8 files changed, 48 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/envoypatchpolicy_types.go b/api/v1alpha1/envoypatchpolicy_types.go index 7f8886e758..7768d44dc4 100644 --- a/api/v1alpha1/envoypatchpolicy_types.go +++ b/api/v1alpha1/envoypatchpolicy_types.go @@ -81,6 +81,7 @@ const ( // using JSONPatch semantics. // // +kubebuilder:validation:XValidation:rule="!(has(self.name) && has(self.nameSelector))",message="only one of name and nameSelector can be specified" +// +kubebuilder:validation:XValidation:rule="has(self.name) || has(self.nameSelector)",message="either name or nameSelector must be specified" type EnvoyJSONPatchConfig struct { // Type is the typed URL of the Envoy xDS Resource Type EnvoyResourceType `json:"type"` diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index 5a236bafdb..b507a390ab 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -145,6 +145,8 @@ spec: x-kubernetes-validations: - message: only one of name and nameSelector can be specified rule: '!(has(self.name) && has(self.nameSelector))' + - message: either name or nameSelector must be specified + rule: has(self.name) || has(self.nameSelector) type: array priority: description: |- diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml index acc0ed1eb5..c9b809f68e 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoypatchpolicies.yaml @@ -144,6 +144,8 @@ spec: x-kubernetes-validations: - message: only one of name and nameSelector can be specified rule: '!(has(self.name) && has(self.nameSelector))' + - message: either name or nameSelector must be specified + rule: has(self.name) || has(self.nameSelector) type: array priority: description: |- diff --git a/internal/gatewayapi/envoypatchpolicy.go b/internal/gatewayapi/envoypatchpolicy.go index d3e1ea665c..a6e47613bd 100644 --- a/internal/gatewayapi/envoypatchpolicy.go +++ b/internal/gatewayapi/envoypatchpolicy.go @@ -128,6 +128,7 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo } // Save the patch + policyRejected := false for _, patch := range policy.Spec.JSONPatches { irPatch := ir.JSONPatchConfig{} irPatch.Type = string(patch.Type) @@ -153,7 +154,8 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo resolveErr, ) - continue + policyRejected = true + break } } irPatch.Operation.Op = ir.JSONPatchOp(patch.Operation.Op) @@ -165,8 +167,14 @@ func (t *Translator) ProcessEnvoyPatchPolicies(envoyPatchPolicies []*egv1a1.Envo policyIR.JSONPatches = append(policyIR.JSONPatches, &irPatch) } - // Set Accepted=True - status.SetAcceptedForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation) + // Only set Accepted=True if policy was not rejected due to validation errors + if !policyRejected { + // Set Accepted=True + status.SetAcceptedForPolicyAncestor(&policy.Status, &ancestorRef, t.GatewayControllerName, policy.Generation) + } else { + // Clear any partial patches that were added before validation failed + policyIR.JSONPatches = nil + } } return res diff --git a/test/cel-validation/envoypatchpolicy_test.go b/test/cel-validation/envoypatchpolicy_test.go index 61dafd9a3e..9a0755e454 100644 --- a/test/cel-validation/envoypatchpolicy_test.go +++ b/test/cel-validation/envoypatchpolicy_test.go @@ -125,6 +125,32 @@ func TestEnvoyPatchPolicy(t *testing.T) { }, wantErrors: []string{"only one of name and nameSelector can be specified"}, }, + { + desc: "invalid json patch with neither name nor nameSelector", + mutate: func(epp *egv1a1.EnvoyPatchPolicy) { + epp.Spec = egv1a1.EnvoyPatchPolicySpec{ + Type: egv1a1.JSONPatchEnvoyPatchType, + JSONPatches: []egv1a1.EnvoyJSONPatchConfig{ + { + Type: egv1a1.ListenerEnvoyResourceType, + Operation: egv1a1.JSONPatchOperation{ + Op: egv1a1.JSONPatchOperationType("test"), + Path: &patchPath, + Value: &apiextensionsv1.JSON{ + Raw: []byte(`"listener-1"`), + }, + }, + }, + }, + TargetRef: gwapiv1.LocalPolicyTargetReference{ + Group: gwapiv1.Group(gwapiv1.GroupName), + Kind: gwapiv1.Kind("Gateway"), + Name: gwapiv1.ObjectName("eg"), + }, + } + }, + wantErrors: []string{"either name or nameSelector must be specified"}, + }, } 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 9766485018..279f702353 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -33391,6 +33391,8 @@ spec: x-kubernetes-validations: - message: only one of name and nameSelector can be specified rule: '!(has(self.name) && has(self.nameSelector))' + - message: either name or nameSelector must be specified + rule: has(self.name) || has(self.nameSelector) type: array priority: description: |- diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 475d693dc7..efff8878e2 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -9329,6 +9329,8 @@ spec: x-kubernetes-validations: - message: only one of name and nameSelector can be specified rule: '!(has(self.name) && has(self.nameSelector))' + - message: either name or nameSelector must be specified + rule: has(self.name) || has(self.nameSelector) type: array priority: description: |- 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 b05a3f4d70..e9c30bd8de 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -9329,6 +9329,8 @@ spec: x-kubernetes-validations: - message: only one of name and nameSelector can be specified rule: '!(has(self.name) && has(self.nameSelector))' + - message: either name or nameSelector must be specified + rule: has(self.name) || has(self.nameSelector) type: array priority: description: |- From 043b8a199c7861ea1c1d6085222c135782601fac Mon Sep 17 00:00:00 2001 From: zirain Date: Fri, 24 Apr 2026 15:56:55 +0800 Subject: [PATCH 3/5] remove skip Signed-off-by: zirain --- internal/xds/translator/jsonpatch.go | 12 +++--- ...tch-patch-multiple-resources.clusters.yaml | 3 +- ...ch-patch-multiple-resources.listeners.yaml | 12 ++++++ ...atch-patch-multiple-resources.secrets.yaml | 2 +- internal/xds/translator/translator.go | 42 +++---------------- 5 files changed, 27 insertions(+), 44 deletions(-) diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go index 32fb1837ad..e726d74917 100644 --- a/internal/xds/translator/jsonpatch.go +++ b/internal/xds/translator/jsonpatch.go @@ -47,7 +47,7 @@ func (t typedName) String() string { } // processJSONPatches applies each JSONPatch to the Xds Resources for a specific type. -func processJSONPatches(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, envoyPatchPolicies []*ir.EnvoyPatchPolicy) error { +func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []*ir.EnvoyPatchPolicy) error { var errs error for _, e := range envoyPatchPolicies { @@ -107,7 +107,7 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, gResources *ir.GlobalR } // find the resources to patch and convert them to JSON - dests, err = findXdsResources(tCtx, gResources, p) + dests, err = findXdsResources(tCtx, p) if err != nil { tErrs = errors.Join(tErrs, err) continue @@ -226,7 +226,7 @@ var jsonMarshalOpts = protojson.MarshalOptions{ } // findXdsResources returns XDS resources to patch based on the patch configuration. -func findXdsResources(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, p *ir.JSONPatchConfig) ([]cachetypes.Resource, error) { +func findXdsResources(tCtx *types.ResourceVersionTable, p *ir.JSONPatchConfig) ([]cachetypes.Resource, error) { var resources []cachetypes.Resource switch p.Type { case resourcev3.ListenerType: @@ -234,11 +234,11 @@ func findXdsResources(tCtx *types.ResourceVersionTable, gResources *ir.GlobalRes case resourcev3.RouteType: resources = findXdsRouteConfigs(tCtx, &p.Name) case resourcev3.ClusterType: - resources = findXdsClusters(tCtx, gResources, &p.Name) + resources = findXdsClusters(tCtx, &p.Name) case resourcev3.EndpointType: - resources = findXdsEndpoints(tCtx, gResources, &p.Name) + resources = findXdsEndpoints(tCtx, &p.Name) case resourcev3.SecretType: - resources = findXdsSecrets(tCtx, gResources, &p.Name) + resources = findXdsSecrets(tCtx, &p.Name) default: return nil, fmt.Errorf("unsupported patch type %s", p.Type) } diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml index 79ce50bbf1..2ebc8c8aa2 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.clusters.yaml @@ -46,7 +46,8 @@ name: second-route-dest perConnectionBufferLimitBytes: 32768 type: EDS -- circuitBreakers: +- altStatName: altStatName + circuitBreakers: thresholds: - maxRetries: 1024 commonLbConfig: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml index 16752bd98c..5e27756c98 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml @@ -9,6 +9,17 @@ typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager httpFilters: + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: eg-ratelimit + failureModeDeny: true + rateLimitService: + grpcService: + envoyGrpc: + clusterName: rate-limit-cluster + transportApiVersion: V3 + timeout: 1s - name: envoy.filters.http.health_check typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.http.health_check.v3.HealthCheck @@ -21,6 +32,7 @@ typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router suppressEnvoyHeaders: true + preserveExternalRequestId: true routeConfig: name: ready_route virtualHosts: diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml index 9b4c5e3b2c..683a18120f 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.secrets.yaml @@ -13,7 +13,7 @@ - name: envoy-gateway-system/envoy tlsCertificate: certificateChain: - inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lVRUZNaFA5ZUo5WEFCV3NRNVptNmJSazJjTE5Rd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0ZqRVVNQklHQTFVRUF3d0xabTl2TG1KaGNpNWpiMjB3SGhjTk1qUXdNakk1TURrek1ERXdXaGNOTXpRdwpNakkyTURrek1ERXdXakFXTVJRd0VnWURWUVFEREF0bWIyOHVZbUZ5TG1OdmJUQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKbEk2WXhFOVprQ1BzNnBDUXhickNtZWl4OVA1RGZ4OVJ1NUxENFQKSm1kVzdJS2R0UVYvd2ZMbXRzdTc2QithVGRDaldlMEJUZmVPT1JCYlIzY1BBRzZFbFFMaWNsUVVydW4zcStncwpKcEsrSTdjSStqNXc4STY4WEg1V1E3clZVdGJ3SHBxYncrY1ZuQnFJVU9MaUlhdGpJZjdLWDUxTTF1RjljZkVICkU0RG5jSDZyYnI1OS9SRlpCc2toeHM1T3p3Sklmb2hreXZGd2V1VHd4Sy9WcGpJKzdPYzQ4QUJDWHBOTzlEL3EKRWgrck9hdWpBTWNYZ0hRSVRrQ2lpVVRjVW82TFNIOXZMWlB0YXFmem9acTZuaE1xcFc2NUUxcEF3RjNqeVRUeAphNUk4SmNmU0Zqa2llWjIwTFVRTW43TThVNHhIamFvL2d2SDBDQWZkQjdSTFUyc0NBd0VBQWFOVE1GRXdIUVlEClZSME9CQllFRk9SQ0U4dS8xRERXN2loWnA3Y3g5dFNtUG02T01COEdBMVVkSXdRWU1CYUFGT1JDRTh1LzFERFcKN2loWnA3Y3g5dFNtUG02T01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRnQ1M3pqc3FUYUg1YThFMmNodm1XQWdDcnhSSzhiVkxNeGl3TkdqYm1FUFJ6K3c2TngrazBBOEtFY0lEc0tjClNYY2k1OHU0b1didFZKQmx6YS9adWpIUjZQMUJuT3BsK2FveTc4NGJiZDRQMzl3VExvWGZNZmJCQ20xdmV2aDkKQUpLbncyWnRxcjRta2JMY3hFcWxxM3NCTEZBUzlzUUxuS05DZTJjR0xkVHAyYm9HK3FjZ3lRZ0NJTTZmOEVNdgpXUGlmQ01NR3V6Sy9HUkY0YlBPL1lGNDhld0R1M1VlaWgwWFhkVUFPRTlDdFVhOE5JaGMxVVBhT3pQcnRZVnFyClpPR2t2L0t1K0I3OGg4U0VzTzlYclFjdXdiT25KeDZLdFIrYWV5a3ZBcFhDUTNmWkMvYllLQUFSK1A4QUpvUVoKYndJVW1YaTRnajVtK2JLUGhlK2lyK0U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + inlineBytes: cmVwbGFjZWQtc2VjcmV0 privateKey: inlineBytes: W3JlZGFjdGVkXQ== - name: test_secret diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 06bc16650e..de5072171a 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -160,7 +160,7 @@ func (t *Translator) Translate(xdsIR *ir.Xds) (*types.ResourceVersionTable, erro } // All XDS resources is ready, let's do the patch. - if err := processJSONPatches(tCtx, xdsIR.GlobalResources, xdsIR.EnvoyPatchPolicies); err != nil { + if err := processJSONPatches(tCtx, xdsIR.EnvoyPatchPolicies); err != nil { // Since JSONPatch error is user-triggered, we don't fail the entire xDS translation so that the remaining // valid xDS resources can be sent to the proxy. t.Logger.Error(err, "Failed to process JSON patches") @@ -1009,9 +1009,6 @@ func findXdsListeners(tCtx *types.ResourceVersionTable, name *ir.StringMatch) [] if !ok { continue } - if strings.HasPrefix(listener.Name, readyListenerPrefix) { - continue - } if stringMatched(name, listener.Name) { result = append(result, r) } @@ -1051,7 +1048,7 @@ func findXdsRouteConfigs(tCtx *types.ResourceVersionTable, name *ir.StringMatch) // findXdsCluster finds a xds cluster with the same name, and returns nil if there is no match. func findXdsCluster(tCtx *types.ResourceVersionTable, name string) *clusterv3.Cluster { - resources := findXdsClusters(tCtx, nil, &ir.StringMatch{Exact: &name}) + resources := findXdsClusters(tCtx, &ir.StringMatch{Exact: &name}) if len(resources) > 0 { return resources[0].(*clusterv3.Cluster) } @@ -1059,25 +1056,17 @@ func findXdsCluster(tCtx *types.ResourceVersionTable, name string) *clusterv3.Cl } // findXdsClusters finds xds clusters. -func findXdsClusters(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { +func findXdsClusters(tCtx *types.ResourceVersionTable, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.ClusterType] == nil { return nil } - skippedClusters := sets.New(getRateLimitServiceClusterName(), wasmHTTPServiceClusterName) - // skip. proxy service cluster - if gResources != nil && gResources.ProxyServiceCluster != nil { - skippedClusters.Insert(gResources.ProxyServiceCluster.Name) - } var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.ClusterType] { cluster, ok := r.(*clusterv3.Cluster) if !ok { continue } - if skippedClusters.Has(cluster.Name) { - continue - } if stringMatched(name, cluster.Name) { result = append(result, r) } @@ -1087,24 +1076,16 @@ func findXdsClusters(tCtx *types.ResourceVersionTable, gResources *ir.GlobalReso } // findXdsEndpoints finds xds endpoints. -func findXdsEndpoints(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { +func findXdsEndpoints(tCtx *types.ResourceVersionTable, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.EndpointType] == nil { return nil } - skippedClusters := sets.New(getRateLimitServiceClusterName(), wasmHTTPServiceClusterName) - // skip. proxy service cluster - if gResources != nil && gResources.ProxyServiceCluster != nil { - skippedClusters.Insert(gResources.ProxyServiceCluster.Name) - } var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.EndpointType] { endpoint, ok := r.(*endpointv3.ClusterLoadAssignment) if !ok { continue } - if skippedClusters.Has(endpoint.ClusterName) { - continue - } if stringMatched(name, endpoint.ClusterName) { result = append(result, r) } @@ -1115,7 +1096,7 @@ func findXdsEndpoints(tCtx *types.ResourceVersionTable, gResources *ir.GlobalRes // findXdsSecret finds a xds secret with the same name, and returns nil if there is no match. func findXdsSecret(tCtx *types.ResourceVersionTable, name string) *tlsv3.Secret { - resources := findXdsSecrets(tCtx, nil, &ir.StringMatch{Exact: &name}) + resources := findXdsSecrets(tCtx, &ir.StringMatch{Exact: &name}) if len(resources) > 0 { return resources[0].(*tlsv3.Secret) } @@ -1123,27 +1104,16 @@ func findXdsSecret(tCtx *types.ResourceVersionTable, name string) *tlsv3.Secret } // findXdsSecrets finds xds secrets. -func findXdsSecrets(tCtx *types.ResourceVersionTable, gResources *ir.GlobalResources, name *ir.StringMatch) []cachetypes.Resource { +func findXdsSecrets(tCtx *types.ResourceVersionTable, name *ir.StringMatch) []cachetypes.Resource { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.SecretType] == nil { return nil } - // skip secrets that are used for - skippedSecrets := sets.New( - "xds_trusted_ca", "jwt-sa-bearer", - ) - // envoy client certificate - if gResources != nil && gResources.EnvoyClientCertificate != nil { - skippedSecrets.Insert(gResources.EnvoyClientCertificate.Name) - } var result []cachetypes.Resource for _, r := range tCtx.XdsResources[resourcev3.SecretType] { secret, ok := r.(*tlsv3.Secret) if !ok { continue } - if skippedSecrets.Has(secret.Name) { - continue - } if stringMatched(name, secret.Name) { result = append(result, r) } From 8af155bf393e7669b6db0e99d2dcd5ccdefe2543 Mon Sep 17 00:00:00 2001 From: zirain Date: Fri, 24 Apr 2026 16:17:50 +0800 Subject: [PATCH 4/5] fix Signed-off-by: zirain --- internal/xds/translator/jsonpatch.go | 10 ++-------- .../in/xds-ir/jsonpatch-patch-multiple-resources.yaml | 3 ++- .../testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml | 3 ++- ...ch-patch-multiple-resources.envoypatchpolicies.yaml | 7 ++++--- .../jsonpatch-patch-multiple-resources.listeners.yaml | 1 - 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/internal/xds/translator/jsonpatch.go b/internal/xds/translator/jsonpatch.go index e726d74917..0e5ae5a210 100644 --- a/internal/xds/translator/jsonpatch.go +++ b/internal/xds/translator/jsonpatch.go @@ -120,7 +120,6 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* } var patchErrors error - var anyPatched bool for _, dest := range dests { var ( resourceJSON []byte @@ -170,15 +169,10 @@ func processJSONPatches(tCtx *types.ResourceVersionTable, envoyPatchPolicies []* patchErrors = errors.Join(patchErrors, tErr) continue } - - // Mark that at least one dest has been patched successfully, - // so that we can report partial success if there are multiple dests and some of them fail - anyPatched = true } - // If there are multiple dests and some of them fail, - // consider it as successful and ignore the failures to patch other dests. - if !anyPatched && patchErrors != nil { + // Report all patch errors, including partial failures + if patchErrors != nil { tErrs = errors.Join(tErrs, patchErrors) } } diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml index cd859d3db8..fce0ebf7c9 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-patch-multiple-resources.yaml @@ -11,7 +11,8 @@ envoyPatchPolicies: jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" name: - safeRegex: ".*" + # patch first-* listeners + prefix: "first" operation: op: add path: "/filter_chains/0/filters/0/typed_config/preserve_external_request_id" diff --git a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml index a98e4d5b21..cc48121967 100644 --- a/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/jsonpatch-with-jsonpath.yaml @@ -131,7 +131,8 @@ envoyPatchPolicies: from: "/endpoints/1/load_balancing_weight" path: "/endpoints/0/load_balancing_weight" # Patch route.typed_per_filter_config property. - - name: envoy-gateway/gateway-1/http + - name: + exact: "envoy-gateway/gateway-1/http" operation: jsonPath: ..routes[?(@.name=~"httproute/default/httproute-1")] op: add diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml index 61f71d060b..63c45bf2e3 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.envoypatchpolicies.yaml @@ -9,8 +9,9 @@ namespace: envoy-gateway conditions: - lastTransitionTime: null - message: Patches have been successfully applied. - reason: Programmed - status: "True" + message: 'Unable to unmarshal xds resource {"name":"second-listener","address":{"socket_address":{"address":"0.0.0.0","port_value":10080}},"default_filter_chain":{"filters":[{"name":"envoy.filters.network.http_connection_manager","typed_config":{"@type":"type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager","stat_prefix":"http-10080","rds":{"config_source":{"ads":{},"resource_api_version":"V3"},"route_config_name":"second-listener"},"http_filters":[{"name":"envoy.filters.http.router","typed_config":{"@type":"type.googleapis.com/envoy.extensions.filters.http.router.v3.Router","suppress_envoy_headers":true}}],"common_http_protocol_options":{"headers_with_underscores_action":"REJECT_REQUEST"},"http2_protocol_options":{"max_concurrent_streams":100,"initial_stream_window_size":65536,"initial_connection_window_size":1048576},"server_header_transformation":"PASS_THROUGH","use_remote_address":true,"normalize_path":true,"merge_slashes":true,"path_with_escaped_slashes_action":"UNESCAPE_AND_REDIRECT"}}],"name":"second-listener"},"per_connection_buffer_limit_bytes":32768,"max_connections_to_accept_per_socket_event":1,"filter_chains":[{"filters":[{"typed_config":{"http_filters":[{"name":"envoy.filters.http.ratelimit","typed_config":{"@type":"type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit","domain":"eg-ratelimit","failure_mode_deny":true,"rate_limit_service":{"grpc_service":{"envoy_grpc":{"cluster_name":"rate-limit-cluster"}},"transport_api_version":"V3"},"timeout":"1s"}}]}}]}]}, + err:proto: (line 1:1175): missing "@type" field.' + reason: Invalid + status: "False" type: Programmed controllerName: "" diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml index 5e27756c98..f2452ce968 100644 --- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-patch-multiple-resources.listeners.yaml @@ -32,7 +32,6 @@ typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router suppressEnvoyHeaders: true - preserveExternalRequestId: true routeConfig: name: ready_route virtualHosts: From 025b3a365309ec3549077027b1aa25d3b22a59c1 Mon Sep 17 00:00:00 2001 From: zirain Date: Fri, 26 Jun 2026 10:39:22 +0800 Subject: [PATCH 5/5] release notes Signed-off-by: zirain --- ...-nameselector-envoypatchpolicy-patching-multiple-resources.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 release-notes/current/new_features/8518-nameselector-envoypatchpolicy-patching-multiple-resources.md diff --git a/release-notes/current/new_features/8518-nameselector-envoypatchpolicy-patching-multiple-resources.md b/release-notes/current/new_features/8518-nameselector-envoypatchpolicy-patching-multiple-resources.md new file mode 100644 index 0000000000..d8386318bd --- /dev/null +++ b/release-notes/current/new_features/8518-nameselector-envoypatchpolicy-patching-multiple-resources.md @@ -0,0 +1 @@ +Added support for patching multiple resources with `nameSelector` in `EnvoyPatchPolicy`.