diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go index a642b37a0a..c3bed70d3a 100644 --- a/internal/gatewayapi/filters.go +++ b/internal/gatewayapi/filters.go @@ -19,6 +19,7 @@ import ( "github.com/envoyproxy/gateway/internal/gatewayapi/resource" "github.com/envoyproxy/gateway/internal/gatewayapi/status" "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/http" ) type FiltersTranslator interface { @@ -417,7 +418,7 @@ func (t *Translator) processRedirectFilter( if redirect.StatusCode != nil { redirectCode := int32(*redirect.StatusCode) // Envoy supports 302, 303, 307, and 308, but gateway API only includes 301 and 302 - if redirectCode == 301 || redirectCode == 302 { + if http.SupportedRedirectCodes.Has(redirectCode) { redir.StatusCode = &redirectCode } else { return status.NewRouteStatusError( diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.in.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.in.yaml index 149f4163f8..ebffb236c6 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.in.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.in.yaml @@ -1,39 +1,127 @@ gateways: -- apiVersion: gateway.networking.k8s.io/v1 - kind: Gateway - metadata: - namespace: envoy-gateway - name: gateway-1 - spec: - gatewayClassName: envoy-gateway-class - listeners: - - name: http - protocol: HTTP - port: 80 - hostname: "*.envoyproxy.io" - allowedRoutes: - namespaces: - from: All -httpRoutes: -- apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - namespace: default - name: httproute-1 - spec: - hostnames: - - gateway.envoyproxy.io - parentRefs: - - namespace: envoy-gateway + - apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway name: gateway-1 - sectionName: http - rules: - - matches: - - path: - value: "/" - filters: - - type: RequestRedirect - requestRedirect: - scheme: https - statusCode: 301 - hostname: "redirected.com" + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: "*.envoyproxy.io" + allowedRoutes: + namespaces: + from: All +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-301 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/status/301" + filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 + hostname: "redirected.com" + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-302 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/status/302" + filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 302 + hostname: "redirected.com" + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-303 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/status/303" + filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 303 + hostname: "redirected.com" + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-307 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/status/307" + filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 307 + hostname: "redirected.com" + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-308 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/status/308" + filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 308 + hostname: "redirected.com" diff --git a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml index ba77d82ea1..fc2cd428b0 100644 --- a/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-redirect-filter-hostname.out.yaml @@ -16,7 +16,7 @@ gateways: protocol: HTTP status: listeners: - - attachedRoutes: 1 + - attachedRoutes: 5 conditions: - lastTransitionTime: null message: Sending translated listener configuration to the data plane @@ -43,7 +43,7 @@ httpRoutes: - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: httproute-1 + name: httproute-301 namespace: default spec: hostnames: @@ -61,7 +61,167 @@ httpRoutes: type: RequestRedirect matches: - path: - value: / + value: /status/301 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-302 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - filters: + - requestRedirect: + hostname: redirected.com + scheme: https + statusCode: 302 + type: RequestRedirect + matches: + - path: + value: /status/302 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-303 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - filters: + - requestRedirect: + hostname: redirected.com + scheme: https + statusCode: 303 + type: RequestRedirect + matches: + - path: + value: /status/303 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-307 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - filters: + - requestRedirect: + hostname: redirected.com + scheme: https + statusCode: 307 + type: RequestRedirect + matches: + - path: + value: /status/307 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: httproute-308 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - filters: + - requestRedirect: + hostname: redirected.com + scheme: https + statusCode: 308 + type: RequestRedirect + matches: + - path: + value: /status/308 status: parents: - conditions: @@ -147,18 +307,82 @@ xdsIR: isHTTP2: false metadata: kind: HTTPRoute - name: httproute-1 + name: httproute-301 namespace: default - name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + name: httproute/default/httproute-301/rule/0/match/0/gateway_envoyproxy_io pathMatch: distinct: false name: "" - prefix: / + prefix: /status/301 redirect: hostname: redirected.com port: 443 scheme: https statusCode: 301 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-302 + namespace: default + name: httproute/default/httproute-302/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/302 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 302 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-303 + namespace: default + name: httproute/default/httproute-303/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/303 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 303 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-307 + namespace: default + name: httproute/default/httproute-307/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/307 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 307 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-308 + namespace: default + name: httproute/default/httproute-308/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/308 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 308 readyListener: address: 0.0.0.0 ipFamily: IPv4 diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d15b55bd35..5551191c7a 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/yaml" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + httputils "github.com/envoyproxy/gateway/internal/utils/http" ) const ( @@ -51,7 +52,7 @@ var ( ErrStringMatchInvertDistinctInvalid = errors.New("only one of the Invert or Distinct fields can be set") ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") ErrDirectResponseStatusInvalid = errors.New("only HTTP status codes 100 - 599 are supported for DirectResponse") - ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301 and 302 are supported for redirect filters") + ErrRedirectUnsupportedStatus = errors.New("only HTTP status codes 301, 302, 303, 307 and 308 are supported for redirect filters") ErrRedirectUnsupportedScheme = errors.New("only http and https are supported for the scheme in redirect filters") ErrHTTPPathModifierDoubleReplace = errors.New("redirect filter cannot have a path modifier that supplies more than one of fullPathReplace, prefixMatchReplace and regexMatchReplace") ErrHTTPPathModifierNoReplace = errors.New("redirect filter cannot have a path modifier that does not supply either fullPathReplace, prefixMatchReplace or regexMatchReplace") @@ -1990,7 +1991,7 @@ func (r Redirect) Validate() error { } if r.StatusCode != nil { - if *r.StatusCode != 301 && *r.StatusCode != 302 { + if !httputils.SupportedRedirectCodes.Has(*r.StatusCode) { errs = errors.Join(errs, ErrRedirectUnsupportedStatus) } } diff --git a/internal/utils/http/http.go b/internal/utils/http/http.go new file mode 100644 index 0000000000..9e3a781e44 --- /dev/null +++ b/internal/utils/http/http.go @@ -0,0 +1,10 @@ +// 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. + +package http + +import "k8s.io/apimachinery/pkg/util/sets" + +var SupportedRedirectCodes = sets.New[int32](301, 302, 303, 307, 308) diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index fd5c64648e..10d3fae79b 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -474,9 +474,18 @@ func buildXdsRedirectAction(httpRoute *ir.HTTPRoute) *routev3.RedirectAction { routeAction.PortRedirect = *redirection.Port } if redirection.StatusCode != nil { - if *redirection.StatusCode == 302 { + switch *redirection.StatusCode { + case 302: routeAction.ResponseCode = routev3.RedirectAction_FOUND - } // no need to check for 301 since Envoy will use 301 as the default if the field is not configured + case 303: + routeAction.ResponseCode = routev3.RedirectAction_SEE_OTHER + case 307: + routeAction.ResponseCode = routev3.RedirectAction_TEMPORARY_REDIRECT + case 308: + routeAction.ResponseCode = routev3.RedirectAction_PERMANENT_REDIRECT + default: + // Envoy will use 301 as the default if the field is not configured + } } return routeAction diff --git a/internal/xds/translator/testdata/in/xds-ir/httproute-redirect.yaml b/internal/xds/translator/testdata/in/xds-ir/httproute-redirect.yaml new file mode 100644 index 0000000000..336147d1c0 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/httproute-redirect.yaml @@ -0,0 +1,97 @@ +http: + - address: 0.0.0.0 + externalPort: 80 + hostnames: + - "*.envoyproxy.io" + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-301 + namespace: default + name: httproute/default/httproute-301/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/301 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 301 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-302 + namespace: default + name: httproute/default/httproute-302/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/302 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 302 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-303 + namespace: default + name: httproute/default/httproute-303/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/303 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 303 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-307 + namespace: default + name: httproute/default/httproute-307/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/307 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 307 + - hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-308 + namespace: default + name: httproute/default/httproute-308/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: /status/308 + redirect: + hostname: redirected.com + port: 443 + scheme: https + statusCode: 308 diff --git a/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.clusters.yaml new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.clusters.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.endpoints.yaml new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.endpoints.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.listeners.yaml new file mode 100644 index 0000000000..4d0fe90c54 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.listeners.yaml @@ -0,0 +1,35 @@ +- 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: envoy-gateway/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: envoy-gateway/gateway-1/http + maxConnectionsToAcceptPerSocketEvent: 1 + name: envoy-gateway/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.routes.yaml new file mode 100644 index 0000000000..0d92f3f335 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/httproute-redirect.routes.yaml @@ -0,0 +1,84 @@ +- ignorePortInHostMatching: true + name: envoy-gateway/gateway-1/http + virtualHosts: + - domains: + - gateway.envoyproxy.io + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Gateway + name: gateway-1 + namespace: envoy-gateway + sectionName: http + name: envoy-gateway/gateway-1/http/gateway_envoyproxy_io + routes: + - match: + pathSeparatedPrefix: /status/301 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-301 + namespace: default + name: httproute/default/httproute-301/rule/0/match/0/gateway_envoyproxy_io + redirect: + hostRedirect: redirected.com + schemeRedirect: https + - match: + pathSeparatedPrefix: /status/302 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-302 + namespace: default + name: httproute/default/httproute-302/rule/0/match/0/gateway_envoyproxy_io + redirect: + hostRedirect: redirected.com + responseCode: FOUND + schemeRedirect: https + - match: + pathSeparatedPrefix: /status/303 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-303 + namespace: default + name: httproute/default/httproute-303/rule/0/match/0/gateway_envoyproxy_io + redirect: + hostRedirect: redirected.com + responseCode: SEE_OTHER + schemeRedirect: https + - match: + pathSeparatedPrefix: /status/307 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-307 + namespace: default + name: httproute/default/httproute-307/rule/0/match/0/gateway_envoyproxy_io + redirect: + hostRedirect: redirected.com + responseCode: TEMPORARY_REDIRECT + schemeRedirect: https + - match: + pathSeparatedPrefix: /status/308 + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-308 + namespace: default + name: httproute/default/httproute-308/rule/0/match/0/gateway_envoyproxy_io + redirect: + hostRedirect: redirected.com + responseCode: PERMANENT_REDIRECT + schemeRedirect: https