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) }) }, }