diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go
index b79839a7dda..6bbcdf020a7 100644
--- a/api/v1alpha1/shared_types.go
+++ b/api/v1alpha1/shared_types.go
@@ -684,7 +684,15 @@ type CustomResponse struct {
ContentType *string `json:"contentType,omitempty"`
// Body of the Custom Response
- Body CustomResponseBody `json:"body"`
+ //
+ // +optional
+ Body *CustomResponseBody `json:"body,omitempty"`
+
+ // Status Code of the Custom Response
+ // If unset, does not override the status of response.
+ //
+ // +optional
+ StatusCode *int `json:"statusCode,omitempty"`
}
// ResponseValueType defines the types of values for the response body supported by Envoy Gateway.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 742ffed1b25..60accb57493 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1163,7 +1163,16 @@ func (in *CustomResponse) DeepCopyInto(out *CustomResponse) {
*out = new(string)
**out = **in
}
- in.Body.DeepCopyInto(&out.Body)
+ if in.Body != nil {
+ in, out := &in.Body, &out.Body
+ *out = new(CustomResponseBody)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.StatusCode != nil {
+ in, out := &in.StatusCode, &out.StatusCode
+ *out = new(int)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResponse.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
index f9fb0f329dd..74d0171fb6d 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml
@@ -1071,8 +1071,11 @@ spec:
description: Content Type of the response. This will be
set in the Content-Type header.
type: string
- required:
- - body
+ statusCode:
+ description: |-
+ Status Code of the Custom Response
+ If unset, does not override the status of response.
+ type: integer
type: object
required:
- match
diff --git a/internal/gatewayapi/backendtrafficpolicy.go b/internal/gatewayapi/backendtrafficpolicy.go
index 0934629428b..5f1f7b04bd7 100644
--- a/internal/gatewayapi/backendtrafficpolicy.go
+++ b/internal/gatewayapi/backendtrafficpolicy.go
@@ -878,6 +878,10 @@ func buildResponseOverride(policy *egv1a1.BackendTrafficPolicy, resources *resou
ContentType: ro.Response.ContentType,
}
+ if ro.Response.StatusCode != nil {
+ response.StatusCode = ptr.To(uint32(*ro.Response.StatusCode))
+ }
+
var err error
response.Body, err = getCustomResponseBody(ro.Response.Body, resources, policy.Namespace)
if err != nil {
@@ -896,8 +900,8 @@ func buildResponseOverride(policy *egv1a1.BackendTrafficPolicy, resources *resou
}, nil
}
-func getCustomResponseBody(body egv1a1.CustomResponseBody, resources *resource.Resources, policyNs string) (*string, error) {
- if body.Type != nil && *body.Type == egv1a1.ResponseValueTypeValueRef {
+func getCustomResponseBody(body *egv1a1.CustomResponseBody, resources *resource.Resources, policyNs string) (*string, error) {
+ if body != nil && body.Type != nil && *body.Type == egv1a1.ResponseValueTypeValueRef {
cm := resources.GetConfigMap(policyNs, string(body.ValueRef.Name))
if cm != nil {
b, dataOk := cm.Data["response.body"]
@@ -917,7 +921,7 @@ func getCustomResponseBody(body egv1a1.CustomResponseBody, resources *resource.R
} else {
return nil, fmt.Errorf("can't find the referenced configmap %s", body.ValueRef.Name)
}
- } else if body.Inline != nil {
+ } else if body != nil && body.Inline != nil {
return body.Inline, nil
}
diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go
index 7e1b5f0409a..06e1ce3cd1e 100644
--- a/internal/gatewayapi/filters.go
+++ b/internal/gatewayapi/filters.go
@@ -905,7 +905,7 @@ func (t *Translator) processExtensionRefHTTPFilter(extFilter *gwapiv1.LocalObjec
dr := &ir.CustomResponse{}
if hrf.Spec.DirectResponse.Body != nil {
var err error
- if dr.Body, err = getCustomResponseBody(*hrf.Spec.DirectResponse.Body, resources, filterNs); err != nil {
+ if dr.Body, err = getCustomResponseBody(hrf.Spec.DirectResponse.Body, resources, filterNs); err != nil {
t.processInvalidHTTPFilter(string(extFilter.Kind), filterContext, err)
return
}
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
index 51dd9fd7114..2337e7cc1b9 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.in.yaml
@@ -50,7 +50,26 @@ httpRoutes:
name: httproute-1
spec:
hostnames:
- - gateway.envoyproxy.io
+ - foo.envoyproxy.io
+ parentRefs:
+ - namespace: default
+ name: gateway-2
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/"
+ backendRefs:
+ - name: service-1
+ port: 8080
+ - apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-2
+ spec:
+ hostnames:
+ - bar.envoyproxy.io
parentRefs:
- namespace: default
name: gateway-2
@@ -114,7 +133,7 @@ backendTrafficPolicies:
kind: BackendTrafficPolicy
metadata:
namespace: default
- name: policy-for-route
+ name: policy-for-route-1
spec:
targetRef:
group: gateway.networking.k8s.io
@@ -143,3 +162,19 @@ backendTrafficPolicies:
group: ""
kind: ConfigMap
name: response-override-config
+ - apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: BackendTrafficPolicy
+ metadata:
+ namespace: default
+ name: policy-for-route-2
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-2
+ responseOverride:
+ - match:
+ statusCodes:
+ - value: 403
+ response:
+ statusCode: 401
diff --git a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
index 568a57af484..e0d6fb9e7e1 100644
--- a/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
+++ b/internal/gatewayapi/testdata/backendtrafficpolicy-with-response-override.out.yaml
@@ -3,7 +3,7 @@ backendTrafficPolicies:
kind: BackendTrafficPolicy
metadata:
creationTimestamp: null
- name: policy-for-route
+ name: policy-for-route-1
namespace: default
spec:
responseOverride:
@@ -51,6 +51,39 @@ backendTrafficPolicies:
status: "True"
type: Accepted
controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: BackendTrafficPolicy
+ metadata:
+ creationTimestamp: null
+ name: policy-for-route-2
+ namespace: default
+ spec:
+ responseOverride:
+ - match:
+ statusCodes:
+ - type: null
+ value: 403
+ response:
+ statusCode: 401
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-2
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-2
+ namespace: default
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
@@ -160,7 +193,7 @@ gateways:
protocol: HTTP
status:
listeners:
- - attachedRoutes: 1
+ - attachedRoutes: 2
conditions:
- lastTransitionTime: null
message: Sending translated listener configuration to the data plane
@@ -226,7 +259,45 @@ httpRoutes:
namespace: default
spec:
hostnames:
- - gateway.envoyproxy.io
+ - foo.envoyproxy.io
+ parentRefs:
+ - name: gateway-2
+ namespace: default
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ value: /
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-2
+ namespace: default
+ sectionName: http
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ creationTimestamp: null
+ name: httproute-2
+ namespace: default
+ spec:
+ hostnames:
+ - bar.envoyproxy.io
parentRefs:
- name: gateway-2
namespace: default
@@ -377,25 +448,25 @@ xdsIR:
port: 8080
protocol: HTTP
weight: 1
- hostname: gateway.envoyproxy.io
+ hostname: foo.envoyproxy.io
isHTTP2: false
metadata:
kind: HTTPRoute
name: httproute-1
namespace: default
- name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+ name: httproute/default/httproute-1/rule/0/match/0/foo_envoyproxy_io
pathMatch:
distinct: false
name: ""
prefix: /
traffic:
responseOverride:
- name: backendtrafficpolicy/default/policy-for-route
+ name: backendtrafficpolicy/default/policy-for-route-1
rules:
- match:
statusCodes:
- value: 404
- name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0
+ name: backendtrafficpolicy/default/policy-for-route-1/responseoverride/rule/0
response:
body: httproute-1 Not Found
contentType: text/plain
@@ -405,10 +476,40 @@ xdsIR:
- range:
end: 511
start: 501
- name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/1
+ name: backendtrafficpolicy/default/policy-for-route-1/responseoverride/rule/1
response:
body: |
{
"error": "Internal Server Error"
}
contentType: application/json
+ - destination:
+ name: httproute/default/httproute-2/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: bar.envoyproxy.io
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-2
+ namespace: default
+ name: httproute/default/httproute-2/rule/0/match/0/bar_envoyproxy_io
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /
+ traffic:
+ responseOverride:
+ name: backendtrafficpolicy/default/policy-for-route-2
+ rules:
+ - match:
+ statusCodes:
+ - value: 403
+ name: backendtrafficpolicy/default/policy-for-route-2/responseoverride/rule/0
+ response:
+ statusCode: 401
diff --git a/internal/xds/translator/custom_response.go b/internal/xds/translator/custom_response.go
index 6cca67982e9..8b7f320d71f 100644
--- a/internal/xds/translator/custom_response.go
+++ b/internal/xds/translator/custom_response.go
@@ -21,6 +21,7 @@ import (
envoymatcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/types/known/anypb"
+ "google.golang.org/protobuf/types/known/wrapperspb"
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/ir"
@@ -395,6 +396,10 @@ func (c *customResponse) buildAction(r ir.ResponseOverrideRule) (*matcherv3.Matc
})
}
+ if r.Response.StatusCode != nil {
+ response.StatusCode = &wrapperspb.UInt32Value{Value: *r.Response.StatusCode}
+ }
+
var (
pb *anypb.Any
err error
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 cb00ac65af9..88cfb99c810 100644
--- a/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/custom-response.yaml
@@ -54,3 +54,29 @@ http:
"error": "Internal Server Error"
}
contentType: application/json
+ - destination:
+ name: httproute/default/httproute-2/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ protocol: HTTP
+ weight: 1
+ hostname: "*"
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-2
+ namespace: default
+ name: httproute/default/httproute-2/rule/0/match/-1/*
+ traffic:
+ responseOverride:
+ name: backendtrafficpolicy/default/policy-for-route
+ rules:
+ - match:
+ statusCodes:
+ - value: 403
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0
+ response:
+ statusCode: 404
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml
index c24d059eeaa..ba27dfd9d28 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.clusters.yaml
@@ -15,3 +15,20 @@
name: httproute/default/httproute-1/rule/0
perConnectionBufferLimitBytes: 32768
type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig:
+ localityWeightedLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: httproute/default/httproute-2/rule/0
+ ignoreHealthOnHostRemoval: true
+ lbPolicy: LEAST_REQUEST
+ name: httproute/default/httproute-2/rule/0
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml
index 29bb6b4e444..05442a9a15b 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.endpoints.yaml
@@ -10,3 +10,15 @@
loadBalancingWeight: 1
locality:
region: httproute/default/httproute-1/rule/0/backend/0
+- clusterName: httproute/default/httproute-2/rule/0
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 7.7.7.7
+ portValue: 8080
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: httproute/default/httproute-2/rule/0/backend/0
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 19c56586960..455f453eda2 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
@@ -110,6 +110,27 @@
name: http-attributes-cel-match-input
typedConfig:
'@type': type.googleapis.com/xds.type.matcher.v3.HttpAttributesCelMatchInput
+ - disabled: true
+ name: envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse
+ customResponseMatcher:
+ matcherList:
+ matchers:
+ - onMatch:
+ action:
+ name: backendtrafficpolicy/default/policy-for-route/responseoverride/rule/0
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.http.custom_response.local_response_policy.v3.LocalResponsePolicy
+ statusCode: 404
+ predicate:
+ singlePredicate:
+ input:
+ name: http-response-status-code-match-input
+ typedConfig:
+ '@type': type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeMatchInput
+ valueMatch:
+ exact: "403"
- name: envoy.filters.http.router
typedConfig:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
diff --git a/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml
index 8262bb6f325..cddd1439572 100644
--- a/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/custom-response.routes.yaml
@@ -31,3 +31,21 @@
envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-gateway:
'@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
config: {}
+ - match:
+ prefix: /
+ metadata:
+ filterMetadata:
+ envoy-gateway:
+ resources:
+ - kind: HTTPRoute
+ name: httproute-2
+ namespace: default
+ name: httproute/default/httproute-2/rule/0/match/-1/*
+ route:
+ cluster: httproute/default/httproute-2/rule/0
+ upgradeConfigs:
+ - upgradeType: websocket
+ typedPerFilterConfig:
+ envoy.filters.http.custom_response/backendtrafficpolicy/default/policy-for-route:
+ '@type': type.googleapis.com/envoy.config.route.v3.FilterConfig
+ config: {}
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index c6a7121d7ca..7dc9abcdff0 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -850,7 +850,8 @@ _Appears in:_
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contentType` | _string_ | false | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ | true | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ | false | Body of the Custom Response |
+| `statusCode` | _integer_ | false | Status Code of the Custom Response
If unset, does not override the status of response. |
#### CustomResponseBody
diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md
index c6a7121d7ca..7dc9abcdff0 100644
--- a/site/content/zh/latest/api/extension_types.md
+++ b/site/content/zh/latest/api/extension_types.md
@@ -850,7 +850,8 @@ _Appears in:_
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contentType` | _string_ | false | Content Type of the response. This will be set in the Content-Type header. |
-| `body` | _[CustomResponseBody](#customresponsebody)_ | true | Body of the Custom Response |
+| `body` | _[CustomResponseBody](#customresponsebody)_ | false | Body of the Custom Response |
+| `statusCode` | _integer_ | false | Status Code of the Custom Response
If unset, does not override the status of response. |
#### CustomResponseBody
diff --git a/test/cel-validation/backendtrafficpolicy_test.go b/test/cel-validation/backendtrafficpolicy_test.go
index d5e6a1b2d1f..776342629bf 100644
--- a/test/cel-validation/backendtrafficpolicy_test.go
+++ b/test/cel-validation/backendtrafficpolicy_test.go
@@ -1412,7 +1412,7 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
},
},
Response: egv1a1.CustomResponse{
- Body: egv1a1.CustomResponseBody{
+ Body: &egv1a1.CustomResponseBody{
ValueRef: &gwapiv1a2.LocalObjectReference{
Kind: gwapiv1a2.Kind("ConfigMap"),
Name: gwapiv1a2.ObjectName("eg"),
@@ -1450,7 +1450,7 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
},
},
Response: egv1a1.CustomResponse{
- Body: egv1a1.CustomResponseBody{
+ Body: &egv1a1.CustomResponseBody{
Type: ptr.To(egv1a1.ResponseValueTypeValueRef),
Inline: ptr.To("foo"),
},
@@ -1486,7 +1486,7 @@ func TestBackendTrafficPolicyTarget(t *testing.T) {
},
},
Response: egv1a1.CustomResponse{
- Body: egv1a1.CustomResponseBody{
+ Body: &egv1a1.CustomResponseBody{
Type: ptr.To(egv1a1.ResponseValueTypeValueRef),
ValueRef: &gwapiv1a2.LocalObjectReference{
Kind: gwapiv1a2.Kind("Foo"),
diff --git a/test/e2e/testdata/response-override.yaml b/test/e2e/testdata/response-override.yaml
index 084747aaa6c..b0049edfe85 100644
--- a/test/e2e/testdata/response-override.yaml
+++ b/test/e2e/testdata/response-override.yaml
@@ -44,6 +44,12 @@ spec:
body:
type: Inline
inline: "Oops! Your request is not found."
+ - match:
+ statusCodes:
+ - type: Value
+ value: 403
+ response:
+ statusCode: 404
- match:
statusCodes:
- type: Value
diff --git a/test/e2e/tests/response-override.go b/test/e2e/tests/response-override.go
index 3f7a553fee6..3f611ed1d69 100644
--- a/test/e2e/tests/response-override.go
+++ b/test/e2e/tests/response-override.go
@@ -51,6 +51,7 @@ var ResponseOverrideTest = suite.ConformanceTest{
BackendTrafficPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "response-override", Namespace: ns}, suite.ControllerName, ancestorRef)
verifyCustomResponse(t, suite.TimeoutConfig, gwAddr, "/status/404", "text/plain", "Oops! Your request is not found.", 404)
verifyCustomResponse(t, suite.TimeoutConfig, gwAddr, "/status/500", "application/json", `{"error": "Internal Server Error"}`, 500)
+ verifyCustomResponse(t, suite.TimeoutConfig, gwAddr, "/status/403", "", "", 404)
})
},
}