diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go
index d33f6544d3..ae09dacde7 100644
--- a/api/v1alpha1/shared_types.go
+++ b/api/v1alpha1/shared_types.go
@@ -819,11 +819,33 @@ type ResponseOverride struct {
}
// CustomResponseMatch defines the configuration for matching a user response to return a custom one.
+// When both statusCodes and responseHeaders are specified, both must match.
+// +kubebuilder:validation:XValidation:rule="has(self.statusCodes) || has(self.responseHeaders)",message="at least one of statusCodes or responseHeaders must be specified"
type CustomResponseMatch struct {
// Status code to match on. The match evaluates to true if any of the matches are successful.
+ //
+ // +optional
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=50
- StatusCodes []StatusCodeMatch `json:"statusCodes"`
+ StatusCodes []StatusCodeMatch `json:"statusCodes,omitempty"`
+
+ // Response headers to match on. The match evaluates to true if all matches are successful.
+ //
+ // +optional
+ // +kubebuilder:validation:MinItems=1
+ // +kubebuilder:validation:MaxItems=16
+ ResponseHeaders []ResponseOverrideHeaderMatch `json:"responseHeaders,omitempty"`
+}
+
+// ResponseOverrideHeaderMatch defines the configuration for matching a response header.
+type ResponseOverrideHeaderMatch struct {
+ // Name of the HTTP header.
+ // The header name is case-insensitive.
+ // For example, "Foo" and "foo" are considered the same header.
+ Name gwapiv1.HTTPHeaderName `json:"name"`
+
+ // Value within the HTTP header to match against.
+ Value StringMatch `json:"value"`
}
// StatusCodeValueType defines the types of values for the status code match supported by Envoy Gateway.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 7730ebed2b..2ce3ccb647 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2051,6 +2051,13 @@ func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ResponseHeaders != nil {
+ in, out := &in.ResponseHeaders, &out.ResponseHeaders
+ *out = make([]ResponseOverrideHeaderMatch, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponseMatch.
@@ -7938,6 +7945,22 @@ func (in *ResponseOverride) DeepCopy() *ResponseOverride {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResponseOverrideHeaderMatch) DeepCopyInto(out *ResponseOverrideHeaderMatch) {
+ *out = *in
+ in.Value.DeepCopyInto(&out.Value)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseOverrideHeaderMatch.
+func (in *ResponseOverrideHeaderMatch) DeepCopy() *ResponseOverrideHeaderMatch {
+ if in == nil {
+ return nil
+ }
+ out := new(ResponseOverrideHeaderMatch)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Retry) DeepCopyInto(out *Retry) {
*out = *in
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 2f2653c307..a407377cdc 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -2382,6 +2382,52 @@ spec:
match:
description: Match configuration.
properties:
+ responseHeaders:
+ description: Response headers to match on. The match evaluates
+ to true if all matches are successful.
+ items:
+ description: ResponseOverrideHeaderMatch defines the configuration
+ for matching a response header.
+ properties:
+ name:
+ description: |-
+ Name of the HTTP header.
+ The header name is case-insensitive.
+ For example, "Foo" and "foo" are considered the same header.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value within the HTTP header to match
+ against.
+ 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
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
statusCodes:
description: Status code to match on. The match evaluates
to true if any of the matches are successful.
@@ -2437,9 +2483,11 @@ spec:
maxItems: 50
minItems: 1
type: array
- required:
- - statusCodes
type: object
+ x-kubernetes-validations:
+ - message: at least one of statusCodes or responseHeaders must
+ be specified
+ rule: has(self.statusCodes) || has(self.responseHeaders)
redirect:
description: Redirect configuration
properties:
diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index 46e1d1ea12..616745119d 100644
--- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -2381,6 +2381,52 @@ spec:
match:
description: Match configuration.
properties:
+ responseHeaders:
+ description: Response headers to match on. The match evaluates
+ to true if all matches are successful.
+ items:
+ description: ResponseOverrideHeaderMatch defines the configuration
+ for matching a response header.
+ properties:
+ name:
+ description: |-
+ Name of the HTTP header.
+ The header name is case-insensitive.
+ For example, "Foo" and "foo" are considered the same header.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value within the HTTP header to match
+ against.
+ 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
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
statusCodes:
description: Status code to match on. The match evaluates
to true if any of the matches are successful.
@@ -2436,9 +2482,11 @@ spec:
maxItems: 50
minItems: 1
type: array
- required:
- - statusCodes
type: object
+ x-kubernetes-validations:
+ - message: at least one of statusCodes or responseHeaders must
+ be specified
+ rule: has(self.statusCodes) || has(self.responseHeaders)
redirect:
description: Redirect configuration
properties:
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 4f2378c5ce..c5d66e6f99 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -2382,6 +2382,10 @@ func (t *Translator) buildResponseOverride(policy *egv1a1.BackendTrafficPolicy,
}
}
+ for _, h := range ro.Match.ResponseHeaders {
+ match.ResponseHeaders = append(match.ResponseHeaders, *irStringMatch(string(h.Name), h.Value))
+ }
+
if ro.Redirect != nil {
redirect := &ir.Redirect{
Scheme: ro.Redirect.Scheme,
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
index 0e0246d2ef..fc8fb07777 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
@@ -204,6 +204,32 @@ backendTrafficPolicies:
- value: 403
response:
statusCode: 401
+ - match:
+ responseHeaders:
+ - name: X-Custom-Header
+ value:
+ type: Exact
+ value: custom-value
+ response:
+ contentType: text/plain
+ body:
+ inline: "matched response header"
+ - match:
+ statusCodes:
+ - value: 503
+ responseHeaders:
+ - name: X-Error-Type
+ value:
+ type: Prefix
+ value: "upstream-"
+ - name: X-Region
+ value:
+ type: RegularExpression
+ value: "^us-.*$"
+ response:
+ contentType: application/json
+ body:
+ inline: '{"error":"combined match"}'
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
index 429804c25b..5d8bd18b22 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
@@ -68,6 +68,35 @@ backendTrafficPolicies:
value: 403
response:
statusCode: 401
+ - match:
+ responseHeaders:
+ - name: X-Custom-Header
+ value:
+ type: Exact
+ value: custom-value
+ response:
+ body:
+ inline: matched response header
+ type: null
+ contentType: text/plain
+ - match:
+ responseHeaders:
+ - name: X-Error-Type
+ value:
+ type: Prefix
+ value: upstream-
+ - name: X-Region
+ value:
+ type: RegularExpression
+ value: ^us-.*$
+ statusCodes:
+ - type: null
+ value: 503
+ response:
+ body:
+ inline: '{"error":"combined match"}'
+ type: null
+ contentType: application/json
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
@@ -710,6 +739,29 @@ xdsIR:
name: backendtrafficpolicy/default/policy-for-route-2/responseoverride/rule/0
response:
statusCode: 401
+ - match:
+ responseHeaders:
+ - distinct: false
+ exact: custom-value
+ name: X-Custom-Header
+ name: backendtrafficpolicy/default/policy-for-route-2/responseoverride/rule/1
+ response:
+ body: bWF0Y2hlZCByZXNwb25zZSBoZWFkZXI=
+ contentType: text/plain
+ - match:
+ responseHeaders:
+ - distinct: false
+ name: X-Error-Type
+ prefix: upstream-
+ - distinct: false
+ name: X-Region
+ safeRegex: ^us-.*$
+ statusCodes:
+ - value: 503
+ name: backendtrafficpolicy/default/policy-for-route-2/responseoverride/rule/2
+ response:
+ body: eyJlcnJvciI6ImNvbWJpbmVkIG1hdGNoIn0=
+ contentType: application/json
- destination:
metadata:
kind: HTTPRoute
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 6d2fecc72d..50a8b087c1 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -827,10 +827,14 @@ type ResponseOverrideRule struct {
}
// CustomResponseMatch defines the configuration for matching a user response to return a custom one.
+// When both statusCodes and responseHeaders are specified, both must match.
// +k8s:deepcopy-gen=true
type CustomResponseMatch struct {
// Status code to match on. The match evaluates to true if any of the matches are successful.
- StatusCodes []StatusCodeMatch `json:"statusCodes"`
+ StatusCodes []StatusCodeMatch `json:"statusCodes,omitempty"`
+
+ // Response headers to match on. The match evaluates to true if all matches are successful.
+ ResponseHeaders []StringMatch `json:"responseHeaders,omitempty"`
}
// StatusCodeMatch defines the configuration for matching a status code.
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index d6f990c42c..d8a9be6be9 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -1285,6 +1285,13 @@ func (in *CustomResponseMatch) DeepCopyInto(out *CustomResponseMatch) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ResponseHeaders != nil {
+ in, out := &in.ResponseHeaders, &out.ResponseHeaders
+ *out = make([]StringMatch, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponseMatch.
diff --git a/internal/xds/translator/custom_response.go b/internal/xds/translator/custom_response.go
index 65977b6791..4373d712e7 100644
--- a/internal/xds/translator/custom_response.go
+++ b/internal/xds/translator/custom_response.go
@@ -77,7 +77,7 @@ func (c *customResponse) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *
return errs
}
-// buildHCMCustomResponseFilter returns an OAuth2 HTTP filter from the provided IR HTTPRoute.
+// buildHCMCustomResponseFilter returns an Custom Response HTTP filter from the provided IR ResponseOverride.
func (c *customResponse) buildHCMCustomResponseFilter(ro *ir.ResponseOverride) (*hcmv3.HttpFilter, error) {
config, err := c.customResponseConfig(ro)
if err != nil {
@@ -106,62 +106,48 @@ func (c *customResponse) customResponseConfig(ro *ir.ResponseOverride) (*respv3.
for _, r := range ro.Rules {
var (
- action *matcherv3.Matcher_OnMatch_Action
- predicate *matcherv3.Matcher_MatcherList_Predicate
- err error
+ action *matcherv3.Matcher_OnMatch_Action
+ predicates []*matcherv3.Matcher_MatcherList_Predicate
+ err error
)
- if action, err = c.buildAction(r); err != nil {
+ if action, err = c.buildAction(&r); err != nil {
return nil, err
}
- switch {
- case len(r.Match.StatusCodes) == 0:
- // This is just a sanity check, as the CRD validation should have caught this.
- return nil, fmt.Errorf("missing status code in response override rule")
- case len(r.Match.StatusCodes) == 1:
- if predicate, err = c.buildSinglePredicate(r.Match.StatusCodes[0]); err != nil {
+ if len(r.Match.StatusCodes) > 0 {
+ statusCodePredicate, err := c.buildStatusCodePredicate(r.Match.StatusCodes)
+ if err != nil {
return nil, err
}
- case len(r.Match.StatusCodes) > 1:
- var predicates []*matcherv3.Matcher_MatcherList_Predicate
-
- for _, codeMatch := range r.Match.StatusCodes {
- if predicate, err = c.buildSinglePredicate(codeMatch); err != nil {
- return nil, err
- }
+ predicates = append(predicates, statusCodePredicate)
+ }
- predicates = append(predicates, predicate)
+ if len(r.Match.ResponseHeaders) > 0 {
+ headerPredicate, err := c.buildResponseHeaderPredicate(r.Match.ResponseHeaders)
+ if err != nil {
+ return nil, err
}
+ predicates = append(predicates, headerPredicate)
+ }
- // Create a single predicate that ORs all the status code predicates together.
- predicate = &matcherv3.Matcher_MatcherList_Predicate{
- MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{
- OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
- Predicate: predicates,
- },
- },
- }
+ if len(predicates) == 0 {
+ // This is just a sanity check, as the CRD validation should have caught this.
+ return nil, fmt.Errorf("missing match criteria in response override rule")
}
- // For Local or Backend sources, AND the status code predicate with a
- // local_reply predicate so the rule only fires for the correct response origin.
+ // For Local or Backend sources, AND a local_reply predicate so the rule
+ // only fires for the correct response origin.
if r.Source == egv1a1.ResponseOverrideSourceLocal || r.Source == egv1a1.ResponseOverrideSourceBackend {
localReplyPredicate, err := c.buildLocalReplyPredicate(r.Source == egv1a1.ResponseOverrideSourceLocal)
if err != nil {
return nil, err
}
- predicate = &matcherv3.Matcher_MatcherList_Predicate{
- MatchType: &matcherv3.Matcher_MatcherList_Predicate_AndMatcher{
- AndMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
- Predicate: []*matcherv3.Matcher_MatcherList_Predicate{predicate, localReplyPredicate},
- },
- },
- }
+ predicates = append(predicates, localReplyPredicate)
}
matchers = append(matchers, &matcherv3.Matcher_MatcherList_FieldMatcher{
- Predicate: predicate,
+ Predicate: andPredicate(predicates),
OnMatch: &matcherv3.Matcher_OnMatch{
OnMatch: action,
},
@@ -183,7 +169,105 @@ func (c *customResponse) customResponseConfig(ro *ir.ResponseOverride) (*respv3.
return cr, nil
}
-func (c *customResponse) buildSinglePredicate(codeMatch ir.StatusCodeMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) {
+func andPredicate(predicates []*matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate {
+ if len(predicates) == 1 {
+ return predicates[0]
+ }
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_AndMatcher{
+ AndMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+ Predicate: predicates,
+ },
+ },
+ }
+}
+
+func (c *customResponse) buildStatusCodePredicate(codes []ir.StatusCodeMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) {
+ predicates := make([]*matcherv3.Matcher_MatcherList_Predicate, 0, len(codes))
+ for _, codeMatch := range codes {
+ predicate, err := c.buildSingleStatusCodePredicate(codeMatch)
+ if err != nil {
+ return nil, err
+ }
+ predicates = append(predicates, predicate)
+ }
+
+ if len(predicates) == 1 {
+ return predicates[0], nil
+ }
+
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{
+ OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+ Predicate: predicates,
+ },
+ },
+ }, nil
+}
+
+func (c *customResponse) buildResponseHeaderPredicate(headers []ir.StringMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) {
+ predicates := make([]*matcherv3.Matcher_MatcherList_Predicate, 0, len(headers))
+ for _, header := range headers {
+ input, err := c.buildResponseHeaderInput(header.Name)
+ if err != nil {
+ return nil, err
+ }
+ valueMatcher, err := buildStringMatcher(header)
+ if err != nil {
+ return nil, err
+ }
+ predicates = append(predicates, &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{
+ SinglePredicate: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate{
+ Input: input,
+ Matcher: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_ValueMatch{
+ ValueMatch: valueMatcher,
+ },
+ },
+ },
+ })
+ }
+
+ if len(predicates) == 1 {
+ return predicates[0], nil
+ }
+
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_AndMatcher{
+ AndMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+ Predicate: predicates,
+ },
+ },
+ }, nil
+}
+
+func buildStringMatcher(irMatch ir.StringMatch) (*matcherv3.StringMatcher, error) {
+ switch {
+ case irMatch.Exact != nil:
+ return &matcherv3.StringMatcher{
+ MatchPattern: &matcherv3.StringMatcher_Exact{Exact: *irMatch.Exact},
+ }, nil
+ case irMatch.Prefix != nil:
+ return &matcherv3.StringMatcher{
+ MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: *irMatch.Prefix},
+ }, nil
+ case irMatch.Suffix != nil:
+ return &matcherv3.StringMatcher{
+ MatchPattern: &matcherv3.StringMatcher_Suffix{Suffix: *irMatch.Suffix},
+ }, nil
+ case irMatch.SafeRegex != nil:
+ return &matcherv3.StringMatcher{
+ MatchPattern: &matcherv3.StringMatcher_SafeRegex{SafeRegex: &matcherv3.RegexMatcher{
+ Regex: *irMatch.SafeRegex,
+ EngineType: &matcherv3.RegexMatcher_GoogleRe2{GoogleRe2: &matcherv3.RegexMatcher_GoogleRE2{}},
+ }},
+ }, nil
+ default:
+ return nil, fmt.Errorf("unsupported string matcher")
+ }
+}
+
+func (c *customResponse) buildSingleStatusCodePredicate(codeMatch ir.StatusCodeMatch) (*matcherv3.Matcher_MatcherList_Predicate, error) {
var (
httpAttributeCELInput *cncfv3.TypedExtensionConfig
statusCodeInput *cncfv3.TypedExtensionConfig
@@ -266,6 +350,22 @@ func (c *customResponse) buildStatusCodeInput() (*cncfv3.TypedExtensionConfig, e
}, nil
}
+func (c *customResponse) buildResponseHeaderInput(headerName string) (*cncfv3.TypedExtensionConfig, error) {
+ var (
+ pb *anypb.Any
+ err error
+ )
+
+ if pb, err = proto.ToAnyWithValidation(&envoymatcherv3.HttpResponseHeaderMatchInput{HeaderName: headerName}); err != nil {
+ return nil, err
+ }
+
+ return &cncfv3.TypedExtensionConfig{
+ Name: "http-response-header-match-input",
+ TypedConfig: pb,
+ }, nil
+}
+
func (c *customResponse) buildLocalReplyInput() (*cncfv3.TypedExtensionConfig, error) {
pb, err := proto.ToAnyWithValidation(&envoymatcherv3.HttpResponseLocalReplyMatchInput{})
if err != nil {
@@ -407,7 +507,7 @@ func (c *customResponse) buildStatusCodeCELMatcher(codeRange ir.StatusCodeRange)
}, nil
}
-func (c *customResponse) buildAction(r ir.ResponseOverrideRule) (*matcherv3.Matcher_OnMatch_Action, error) {
+func (c *customResponse) buildAction(r *ir.ResponseOverrideRule) (*matcherv3.Matcher_OnMatch_Action, error) {
var (
pb *anypb.Any
err error
@@ -430,7 +530,7 @@ func (c *customResponse) buildAction(r ir.ResponseOverrideRule) (*matcherv3.Matc
}, nil
}
-func (c *customResponse) buildRedirectAction(r ir.ResponseOverrideRule) (*anypb.Any, error) {
+func (c *customResponse) buildRedirectAction(r *ir.ResponseOverrideRule) (*anypb.Any, error) {
redirectAction := &routev3.RedirectAction{}
if r.Redirect.Scheme != nil {
redirectAction.SchemeRewriteSpecifier = &routev3.RedirectAction_SchemeRedirect{
@@ -458,7 +558,7 @@ func (c *customResponse) buildRedirectAction(r ir.ResponseOverrideRule) (*anypb.
return proto.ToAnyWithValidation(redirect)
}
-func (c *customResponse) buildResponseAction(r ir.ResponseOverrideRule) (*anypb.Any, error) {
+func (c *customResponse) buildResponseAction(r *ir.ResponseOverrideRule) (*anypb.Any, error) {
response := &policyv3.LocalResponsePolicy{}
if len(r.Response.Body) > 0 {
diff --git a/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
index 61e41caf85..a3487ffdf4 100644
--- a/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
@@ -98,3 +98,23 @@ http:
name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0
response:
statusCode: 404
+ - match:
+ responseHeaders:
+ - name: X-Custom-Header
+ exact: custom-value
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/1
+ response:
+ body: bWF0Y2hlZCByZXNwb25zZSBoZWFkZXI=
+ contentType: text/plain
+ - match:
+ statusCodes:
+ - value: 503
+ responseHeaders:
+ - name: X-Error-Type
+ prefix: upstream-
+ - name: X-Region
+ safeRegex: "^us-.*$"
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/2
+ response:
+ body: eyJlcnJvciI6ImNvbWJpbmVkIG1hdGNoIn0=
+ contentType: application/json
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml
index 6d6c7d1e13..79262595d8 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.listeners.yaml
@@ -157,6 +157,69 @@
'@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput
valueMatch:
exact: "403"
+ - onMatch:
+ action:
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/1
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy
+ bodyFormat:
+ textFormat: matched response header
+ responseHeadersToAdd:
+ - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+ header:
+ key: Content-Type
+ value: text/plain
+ predicate:
+ singlePredicate:
+ input:
+ name: http-response-header-match-input
+ typedConfig:
+ '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput
+ headerName: X-Custom-Header
+ valueMatch:
+ exact: custom-value
+ - onMatch:
+ action:
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/2
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy
+ bodyFormat:
+ textFormat: '{"error":"combined match"}'
+ responseHeadersToAdd:
+ - appendAction: OVERWRITE_IF_EXISTS_OR_ADD
+ header:
+ key: Content-Type
+ value: application/json
+ predicate:
+ andMatcher:
+ predicate:
+ - singlePredicate:
+ input:
+ name: http-response-status-code-match-input
+ typedConfig:
+ '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput
+ valueMatch:
+ exact: "503"
+ - andMatcher:
+ predicate:
+ - singlePredicate:
+ input:
+ name: http-response-header-match-input
+ typedConfig:
+ '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput
+ headerName: X-Error-Type
+ valueMatch:
+ prefix: upstream-
+ - singlePredicate:
+ input:
+ name: http-response-header-match-input
+ typedConfig:
+ '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseHeaderMatchInput
+ headerName: X-Region
+ valueMatch:
+ safeRegex:
+ googleRe2: {}
+ regex: ^us-.*$
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
diff --git a/release-notes/current/new_features/9354-support-response-header-in-response-override-with-backendtrafficpolicy.md b/release-notes/current/new_features/9354-support-response-header-in-response-override-with-backendtrafficpolicy.md
new file mode 100644
index 0000000000..16f6b8b302
--- /dev/null
+++ b/release-notes/current/new_features/9354-support-response-header-in-response-override-with-backendtrafficpolicy.md
@@ -0,0 +1 @@
+Added support for matching on response headers in `BackendTrafficPolicy` `responseOverride` via the `match.responseHeaders` field.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 20d3157df6..1c45c12c9b 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -1384,13 +1384,15 @@ _Appears in:_
CustomResponseMatch defines the configuration for matching a user response to return a custom one.
+When both statusCodes and responseHeaders are specified, both must match.
_Appears in:_
- [ResponseOverride](#responseoverride)
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
-| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ | true | | Status code to match on. The match evaluates to true if any of the matches are successful. |
+| `statusCodes` | _[StatusCodeMatch](#statuscodematch) array_ | false | | Status code to match on. The match evaluates to true if any of the matches are successful. |
+| `responseHeaders` | _[ResponseOverrideHeaderMatch](#responseoverrideheadermatch) array_ | false | | Response headers to match on. The match evaluates to true if all matches are successful. |
#### CustomTag
@@ -5782,6 +5784,21 @@ _Appears in:_
| `source` | _[ResponseOverrideSource](#responseoverridesource)_ | false | | Source specifies which responses this rule applies to.
Local overrides only Envoy-generated responses (e.g. auth failures).
Backend overrides only upstream responses.
All (default) overrides both. |
+#### ResponseOverrideHeaderMatch
+
+
+
+ResponseOverrideHeaderMatch defines the configuration for matching a response header.
+
+_Appears in:_
+- [CustomResponseMatch](#customresponsematch)
+
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `name` | _[HTTPHeaderName](#httpheadername)_ | true | | Name of the HTTP header.
The header name is case-insensitive.
For example, "Foo" and "foo" are considered the same header. |
+| `value` | _[StringMatch](#stringmatch)_ | true | | Value within the HTTP header to match against. |
+
+
#### ResponseOverrideSource
_Underlying type:_ _string_
@@ -6248,6 +6265,7 @@ _Appears in:_
- [OIDCDenyRedirectHeader](#oidcdenyredirectheader)
- [OtherSANMatch](#othersanmatch)
- [ProxyMetrics](#proxymetrics)
+- [ResponseOverrideHeaderMatch](#responseoverrideheadermatch)
- [SubjectAltNames](#subjectaltnames)
| Field | Type | Required | Default | Description |
diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go
index 422396efff..72db58e4f2 100644
--- a/test/cel-validation/backendtrafficpolicy_test.go
+++ b/test/cel-validation/backendtrafficpolicy_test.go
@@ -3021,6 +3021,114 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
"inline must be set for type Inline",
},
},
+ {
+ desc: "response header match in response override",
+ mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+ btp.Spec = egv1a1.BackendTrafficPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("Gateway"),
+ Name: gwapiv1.ObjectName("eg"),
+ },
+ },
+ },
+ ResponseOverride: []*egv1a1.ResponseOverride{
+ {
+ Match: egv1a1.CustomResponseMatch{
+ ResponseHeaders: []egv1a1.ResponseOverrideHeaderMatch{
+ {
+ Name: "X-Custom-Header",
+ Value: egv1a1.StringMatch{
+ Type: new(egv1a1.StringMatchExact),
+ Value: "custom-value",
+ },
+ },
+ },
+ },
+ Response: &egv1a1.CustomResponse{
+ Body: &egv1a1.CustomResponseBody{
+ Inline: new("foo"),
+ },
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "status code and response header match in response override",
+ mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+ btp.Spec = egv1a1.BackendTrafficPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("Gateway"),
+ Name: gwapiv1.ObjectName("eg"),
+ },
+ },
+ },
+ ResponseOverride: []*egv1a1.ResponseOverride{
+ {
+ Match: egv1a1.CustomResponseMatch{
+ StatusCodes: []egv1a1.StatusCodeMatch{
+ {
+ Value: new(503),
+ },
+ },
+ ResponseHeaders: []egv1a1.ResponseOverrideHeaderMatch{
+ {
+ Name: "X-Error-Type",
+ Value: egv1a1.StringMatch{
+ Type: new(egv1a1.StringMatchPrefix),
+ Value: "upstream-",
+ },
+ },
+ },
+ },
+ Response: &egv1a1.CustomResponse{
+ Body: &egv1a1.CustomResponseBody{
+ Inline: new("foo"),
+ },
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "neither status code nor response header in response override",
+ mutate: func(btp *egv1a1.BackendTrafficPolicy) {
+ btp.Spec = egv1a1.BackendTrafficPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("Gateway"),
+ Name: gwapiv1.ObjectName("eg"),
+ },
+ },
+ },
+ ResponseOverride: []*egv1a1.ResponseOverride{
+ {
+ Match: egv1a1.CustomResponseMatch{},
+ Response: &egv1a1.CustomResponse{
+ Body: &egv1a1.CustomResponseBody{
+ Inline: new("foo"),
+ },
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{
+ "at least one of statusCodes or responseHeaders must be specified",
+ },
+ },
{
desc: "both targetref and targetrefs specified",
mutate: func(btp *egv1a1.BackendTrafficPolicy) {
diff --git a/test/e2e/testdata/response-override.yaml b/test/e2e/testdata/response-override.yaml
index 2bcebcf76a..6c8499ab3f 100644
--- a/test/e2e/testdata/response-override.yaml
+++ b/test/e2e/testdata/response-override.yaml
@@ -103,3 +103,18 @@ spec:
path:
type: ReplaceFullPath
replaceFullPath: /status/301
+ - match:
+ responseHeaders:
+ - name: X-Custom-Header
+ value:
+ type: Exact
+ value: custom-value
+ response:
+ contentType: text/plain
+ body:
+ type: Inline
+ inline: "matched on response header"
+ header:
+ set:
+ - name: X-Override-Matched
+ value: "true"
diff --git a/test/e2e/tests/direct-response.go b/test/e2e/tests/direct-response.go
index 30f10db58b..feb6c32d65 100644
--- a/test/e2e/tests/direct-response.go
+++ b/test/e2e/tests/direct-response.go
@@ -34,23 +34,39 @@ var DirectResponseTest = suite.ConformanceTest{
kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
// Test inline response with add and set headers
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/inline", "text/plain", "GET Oops! Your request is not found.", 200, map[string]string{
- "X-Add-Header": "added-value",
- "X-Set-Header": "set-value",
- "X-Response-Type": "direct",
- "Cache-Control": "no-cache",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/inline",
+ contentType: "text/plain",
+ body: "GET Oops! Your request is not found.",
+ statusCode: 200,
+ headers: map[string]string{
+ "X-Add-Header": "added-value",
+ "X-Set-Header": "set-value",
+ "X-Response-Type": "direct",
+ "Cache-Control": "no-cache",
+ },
})
// Test value-ref response with add and set headers
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/value-ref", "application/json", `{"error": "Internal Server Error"}`, 200, map[string]string{
- "X-Add-Header": "added-json",
- "X-Set-Header": "set-json",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/value-ref",
+ contentType: "application/json",
+ body: `{"error": "Internal Server Error"}`,
+ statusCode: 200,
+ headers: map[string]string{
+ "X-Add-Header": "added-json",
+ "X-Set-Header": "set-json",
+ },
})
// Test status-only response with add and set headers
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/401", "", ``, 401, map[string]string{
- "X-Add-Header": "added-401",
- "X-Set-Header": "set-401",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/401",
+ statusCode: 401,
+ headers: map[string]string{
+ "X-Add-Header": "added-401",
+ "X-Set-Header": "set-401",
+ },
})
})
},
diff --git a/test/e2e/tests/response-override.go b/test/e2e/tests/response-override.go
index e6d8eb4048..57e2e6f488 100644
--- a/test/e2e/tests/response-override.go
+++ b/test/e2e/tests/response-override.go
@@ -50,32 +50,89 @@ var ResponseOverrideTest = suite.ConformanceTest{
BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "response-override", Namespace: ns}, suite.ControllerName, ancestorRef)
// Test 404 response override with add and set headers
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/status/404", "text/plain", "404 Oops! Your request is not found.", 404, map[string]string{
- "X-Add-Header": "added-404",
- "X-Set-Header": "set-404",
- "X-Error-Type": "not-found",
- "Cache-Control": "no-cache",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/status/404",
+ contentType: "text/plain",
+ body: "404 Oops! Your request is not found.",
+ statusCode: 404,
+ headers: map[string]string{
+ "X-Add-Header": "added-404",
+ "X-Set-Header": "set-404",
+ "X-Error-Type": "not-found",
+ "Cache-Control": "no-cache",
+ },
})
// Test 500 response override with add and set headers
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/status/500", "application/json", `{"error": "Internal Server Error"}`, 500, map[string]string{
- "X-Add-Header": "added-500",
- "X-Set-Header": "set-500",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/status/500",
+ contentType: "application/json",
+ body: `{"error": "Internal Server Error"}`,
+ statusCode: 500,
+ headers: map[string]string{
+ "X-Add-Header": "added-500",
+ "X-Set-Header": "set-500",
+ },
})
// Test 403 response override with add and set headers (status override to 404)
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/status/403", "", "", 404, map[string]string{
- "X-Add-Header": "added-403",
- "X-Set-Header": "set-403",
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/status/403",
+ statusCode: 404,
+ headers: map[string]string{
+ "X-Add-Header": "added-403",
+ "X-Set-Header": "set-403",
+ },
+ })
+
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/status/401",
+ statusCode: 301,
+ })
+
+ // Test header match response override and add X-Override-Matched header (body is also overridden)
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/response-override-header-match",
+ requestHeaders: map[string]string{"X-Echo-Set-Header": "X-Custom-Header: custom-value"},
+ contentType: "text/plain",
+ body: "matched on response header",
+ statusCode: 200,
+ headers: map[string]string{"X-Override-Matched": "true"},
+ })
+
+ // Test header match response override NOT doing anything because the header does not match
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/response-override-header-match",
+ requestHeaders: map[string]string{"X-Echo-Set-Header": "X-Custom-Header: other-value"},
+ contentType: "application/json",
+ body: "matched on response header",
+ bodyNotEqual: true,
+ statusCode: 200,
+ })
+
+ // Test header match response override NOT doing anything because the header is never set
+ verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, &expectedResponse{
+ path: "/response-override-header-match",
+ contentType: "application/json",
+ body: "matched on response header",
+ bodyNotEqual: true,
+ statusCode: 200,
})
- verifyCustomResponse(t, &suite.TimeoutConfig, gwAddr, "/status/401", "", "", 301)
})
},
}
-func verifyCustomResponse(t *testing.T, timeoutConfig *config.TimeoutConfig, gwAddr,
- path, expectedContentType, expectedBody string, expectedStatusCode int, expectedHeaders ...map[string]string,
-) {
+type expectedResponse struct {
+ path string
+ requestHeaders map[string]string
+ contentType string
+ body string
+ bodyNotEqual bool
+ statusCode int
+ headers map[string]string
+}
+
+func verifyCustomResponse(t *testing.T, timeoutConfig *config.TimeoutConfig, gwAddr string, expected *expectedResponse) {
if timeoutConfig == nil {
t.Fatalf("timeoutConfig cannot be nil")
}
@@ -83,48 +140,59 @@ func verifyCustomResponse(t *testing.T, timeoutConfig *config.TimeoutConfig, gwA
reqURL := url.URL{
Scheme: "http",
Host: httputils.CalculateHost(t, gwAddr, "http"),
- Path: path,
+ Path: expected.path,
}
httputils.AwaitConvergence(t, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, func(_ time.Duration) bool {
- rsp, err := http.Get(reqURL.String())
+ req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil)
if err != nil {
- tlog.Logf(t, "failed to get response: %v", err)
+ tlog.Logf(t, "failed to create request: %v", err)
return false
}
+ for k, v := range expected.requestHeaders {
+ req.Header.Set(k, v)
+ }
- // Verify that the response body is overridden
- defer rsp.Body.Close()
- body, err := io.ReadAll(rsp.Body)
+ rsp, err := http.DefaultClient.Do(req)
if err != nil {
- tlog.Logf(t, "failed to read response body: %v", err)
+ tlog.Logf(t, "failed to get response: %v", err)
return false
}
- if string(body) != expectedBody {
- tlog.Logf(t, "expected response body to be %s but got %s", expectedBody, string(body))
+
+ if expected.statusCode != rsp.StatusCode {
+ tlog.Logf(t, "expected status code to be %d but got %d", expected.statusCode, rsp.StatusCode)
return false
}
// Verify that the content type is overridden
- contentType := rsp.Header.Get("Content-Type")
- if contentType != expectedContentType {
- tlog.Logf(t, "expected content type to be %s but got %s", expectedContentType, contentType)
+ if contentType := rsp.Header.Get("Content-Type"); contentType != expected.contentType {
+ tlog.Logf(t, "expected content type to be %s but got %s", expected.contentType, contentType)
+ return false
+ }
+
+ // Verify that the response body is (or, when bodyNotEqual is set, is not) overridden
+ defer rsp.Body.Close()
+ body, err := io.ReadAll(rsp.Body)
+ if err != nil {
+ tlog.Logf(t, "failed to read response body: %v", err)
return false
}
- if expectedStatusCode != rsp.StatusCode {
- tlog.Logf(t, "expected status code to be %d but got %d", expectedStatusCode, rsp.StatusCode)
+ if expected.bodyNotEqual {
+ if string(body) == expected.body {
+ tlog.Logf(t, "expected response body NOT to be %s", expected.body)
+ return false
+ }
+ } else if string(body) != expected.body {
+ tlog.Logf(t, "expected response body to be %s but got %s", expected.body, string(body))
return false
}
// Verify expected headers if provided
- if len(expectedHeaders) > 0 {
- for headerName, expectedValue := range expectedHeaders[0] {
- actualValue := rsp.Header.Get(headerName)
- if actualValue != expectedValue {
- tlog.Logf(t, "expected header %s to be %s but got %s", headerName, expectedValue, actualValue)
- return false
- }
+ for headerName, expectedValue := range expected.headers {
+ if actualValue := rsp.Header.Get(headerName); actualValue != expectedValue {
+ tlog.Logf(t, "expected header %s to be %s but got %s", headerName, expectedValue, actualValue)
+ return false
}
}
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index e8ae71f7e4..4e5ee99f20 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -26999,6 +26999,52 @@ spec:
match:
description: Match configuration.
properties:
+ responseHeaders:
+ description: Response headers to match on. The match evaluates
+ to true if all matches are successful.
+ items:
+ description: ResponseOverrideHeaderMatch defines the configuration
+ for matching a response header.
+ properties:
+ name:
+ description: |-
+ Name of the HTTP header.
+ The header name is case-insensitive.
+ For example, "Foo" and "foo" are considered the same header.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value within the HTTP header to match
+ against.
+ 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
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
statusCodes:
description: Status code to match on. The match evaluates
to true if any of the matches are successful.
@@ -27054,9 +27100,11 @@ spec:
maxItems: 50
minItems: 1
type: array
- required:
- - statusCodes
type: object
+ x-kubernetes-validations:
+ - message: at least one of statusCodes or responseHeaders must
+ be specified
+ rule: has(self.statusCodes) || has(self.responseHeaders)
redirect:
description: Redirect configuration
properties:
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index 8d51ce19a8..e91a01cb22 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -2937,6 +2937,52 @@ spec:
match:
description: Match configuration.
properties:
+ responseHeaders:
+ description: Response headers to match on. The match evaluates
+ to true if all matches are successful.
+ items:
+ description: ResponseOverrideHeaderMatch defines the configuration
+ for matching a response header.
+ properties:
+ name:
+ description: |-
+ Name of the HTTP header.
+ The header name is case-insensitive.
+ For example, "Foo" and "foo" are considered the same header.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value within the HTTP header to match
+ against.
+ 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
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
statusCodes:
description: Status code to match on. The match evaluates
to true if any of the matches are successful.
@@ -2992,9 +3038,11 @@ spec:
maxItems: 50
minItems: 1
type: array
- required:
- - statusCodes
type: object
+ x-kubernetes-validations:
+ - message: at least one of statusCodes or responseHeaders must
+ be specified
+ rule: has(self.statusCodes) || has(self.responseHeaders)
redirect:
description: Redirect configuration
properties:
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 a8dccebf69..7341d49608 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -2937,6 +2937,52 @@ spec:
match:
description: Match configuration.
properties:
+ responseHeaders:
+ description: Response headers to match on. The match evaluates
+ to true if all matches are successful.
+ items:
+ description: ResponseOverrideHeaderMatch defines the configuration
+ for matching a response header.
+ properties:
+ name:
+ description: |-
+ Name of the HTTP header.
+ The header name is case-insensitive.
+ For example, "Foo" and "foo" are considered the same header.
+ maxLength: 256
+ minLength: 1
+ pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
+ type: string
+ value:
+ description: Value within the HTTP header to match
+ against.
+ 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
+ required:
+ - name
+ - value
+ type: object
+ maxItems: 16
+ minItems: 1
+ type: array
statusCodes:
description: Status code to match on. The match evaluates
to true if any of the matches are successful.
@@ -2992,9 +3038,11 @@ spec:
maxItems: 50
minItems: 1
type: array
- required:
- - statusCodes
type: object
+ x-kubernetes-validations:
+ - message: at least one of statusCodes or responseHeaders must
+ be specified
+ rule: has(self.statusCodes) || has(self.responseHeaders)
redirect:
description: Redirect configuration
properties: