From c8e00359d190457ff54035ad37588950d3deef77 Mon Sep 17 00:00:00 2001 From: bitliu Date: Thu, 22 Dec 2022 17:46:44 +0800 Subject: [PATCH] feat: add support for urlRewrite filter Signed-off-by: bitliu --- docs/latest/user/http-urlrewrite.md | 284 +++ docs/latest/user_docs.rst | 2 + internal/gatewayapi/filters.go | 635 ++++++ internal/gatewayapi/helpers.go | 119 +- internal/gatewayapi/listener.go | 131 ++ internal/gatewayapi/resource.go | 75 + internal/gatewayapi/route.go | 630 ++++++ ...rite-filter-full-path-replace-http.in.yaml | 42 + ...ite-filter-full-path-replace-http.out.yaml | 104 + ...ite-filter-hostname-prefix-replace.in.yaml | 43 + ...te-filter-hostname-prefix-replace.out.yaml | 106 + ...te-with-urlrewrite-filter-hostname.in.yaml | 40 + ...e-with-urlrewrite-filter-hostname.out.yaml | 101 + ...ewrite-filter-invalid-filter-type.out.yaml | 15 +- ...urlrewrite-filter-invalid-hostname.in.yaml | 43 + ...rlrewrite-filter-invalid-hostname.out.yaml | 102 + ...te-filter-invalid-multiple-filters.in.yaml | 48 + ...e-filter-invalid-multiple-filters.out.yaml | 111 + ...rlrewrite-filter-invalid-path-type.in.yaml | 43 + ...lrewrite-filter-invalid-path-type.out.yaml | 102 + ...ith-urlrewrite-filter-invalid-path.in.yaml | 43 + ...th-urlrewrite-filter-invalid-path.out.yaml | 102 + ...ith-urlrewrite-filter-missing-path.in.yaml | 41 + ...th-urlrewrite-filter-missing-path.out.yaml | 100 + ...rewrite-filter-prefix-replace-http.in.yaml | 42 + ...ewrite-filter-prefix-replace-http.out.yaml | 104 + internal/gatewayapi/translator.go | 1832 +---------------- internal/gatewayapi/translator_test.go | 24 +- internal/gatewayapi/validate.go | 579 ++++++ internal/ir/xds.go | 29 + internal/ir/xds_test.go | 37 + internal/ir/zz_generated.deepcopy.go | 30 + .../provider/kubernetes/kubernetes_test.go | 54 + internal/xds/translator/route.go | 33 + .../http-route-rewrite-url-fullpath.yaml | 20 + .../xds-ir/http-route-rewrite-url-host.yaml | 21 + .../xds-ir/http-route-rewrite-url-prefix.yaml | 20 + ...p-route-rewrite-url-fullpath.clusters.yaml | 18 + ...-route-rewrite-url-fullpath.listeners.yaml | 40 + ...ttp-route-rewrite-url-fullpath.routes.yaml | 18 + .../http-route-rewrite-url-host.clusters.yaml | 18 + ...http-route-rewrite-url-host.listeners.yaml | 40 + .../http-route-rewrite-url-host.routes.yaml | 17 + ...ttp-route-rewrite-url-prefix.clusters.yaml | 18 + ...tp-route-rewrite-url-prefix.listeners.yaml | 40 + .../http-route-rewrite-url-prefix.routes.yaml | 15 + internal/xds/translator/translator_test.go | 9 + tools/make/kube.mk | 2 +- 48 files changed, 4257 insertions(+), 1865 deletions(-) create mode 100644 docs/latest/user/http-urlrewrite.md create mode 100644 internal/gatewayapi/filters.go create mode 100644 internal/gatewayapi/listener.go create mode 100644 internal/gatewayapi/resource.go create mode 100644 internal/gatewayapi/route.go create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.out.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.in.yaml create mode 100644 internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml create mode 100644 internal/gatewayapi/validate.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-fullpath.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml diff --git a/docs/latest/user/http-urlrewrite.md b/docs/latest/user/http-urlrewrite.md new file mode 100644 index 0000000000..c2fa79e6aa --- /dev/null +++ b/docs/latest/user/http-urlrewrite.md @@ -0,0 +1,284 @@ +# HTTP URL Rewrite + +[HTTPURLRewriteFilter](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPURLRewriteFilter) defines a filter that modifies a request during forwarding. At most one of these filters may be used on a Route rule. This MUST NOT be used on the same Route rule as a HTTPRequestRedirect filter. + +Follow the steps from the [Quickstart](quickstart.md) to install Envoy Gateway and the example manifest. Do +not proceed until you can curl the example backend from the Quickstart guide using HTTP. + +## Rewrite URL Prefix Path + +You can configure to rewrite the prefix in the url like below. In this example, any curls to `http://${GATEWAY_HOST}:8080/get/xxx` will be +rewrite to `http://${GATEWAY_HOST}:8080/replace/xxx`. + +```shell +cat < GET /get/origin/path HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> + +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:03:28 GMT +< content-length: 503 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/replace/origin/path", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "fd84b842-9937-4fb5-83c7-61470d854b90" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path`, but the actual path is `/replace/origin/path`. + +## Rewrite URL Full Path + +You can configure to rewrite the fullpath in the url like below. In this example, any request sent to `http://${GATEWAY_HOST}:8080/get/origin/path/xxxx` will be +rewritten to `http://${GATEWAY_HOST}:8080/force/replace/fullpath`. + +```shell +cat < GET /get/origin/path/extra HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:09:31 GMT +< content-length: 512 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/force/replace/fullpath", + "host": "path.rewrite.example", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Envoy-Original-Path": [ + "/get/origin/path/extra" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "8ab774d6-9ffa-4faa-abbb-f45b0db00895" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Envoy-Original-Path` is `/get/origin/path/extra`, but the actual path is `/force/replace/fullpath`. + +## Rewrite Host Name + +You can configure to rewrite the hostname like below. In this example, any requests sent to `http://${GATEWAY_HOST}:8080/get` with `--header "Host: path.rewrite.example"` will rewrite host into `envoygateway.io`. + +```shell +cat < GET /get HTTP/1.1 +> Host: path.rewrite.example +> User-Agent: curl/7.85.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-type: application/json +< x-content-type-options: nosniff +< date: Wed, 21 Dec 2022 11:15:15 GMT +< content-length: 481 +< x-envoy-upstream-service-time: 0 +< server: envoy +< +{ + "path": "/get", + "host": "envoygateway.io", + "method": "GET", + "proto": "HTTP/1.1", + "headers": { + "Accept": [ + "*/*" + ], + "User-Agent": [ + "curl/7.85.0" + ], + "X-Envoy-Expected-Rq-Timeout-Ms": [ + "15000" + ], + "X-Forwarded-Host": [ + "path.rewrite.example" + ], + "X-Forwarded-Proto": [ + "http" + ], + "X-Request-Id": [ + "39aa447c-97b9-45a3-a675-9fb266ab1af0" + ] + }, + "namespace": "default", + "ingress": "", + "service": "", + "pod": "backend-6fdd4b9bd8-8vlc5" +... +``` + +You can see that the `X-Forwarded-Host` is `path.rewrite.example`, but the actual host is `envoygateway.io`. diff --git a/docs/latest/user_docs.rst b/docs/latest/user_docs.rst index 5a2f83e312..a1bec495a2 100644 --- a/docs/latest/user_docs.rst +++ b/docs/latest/user_docs.rst @@ -9,7 +9,9 @@ Learn how to deploy, use, and operate Envoy Gateway. user/quickstart user/http-routing user/http-redirect + user/http-urlrewrite user/http-traffic-splitting user/http-request-headers + user/http-response-headers user/secure-gateways user/tls-passthrough diff --git a/internal/gatewayapi/filters.go b/internal/gatewayapi/filters.go new file mode 100644 index 0000000000..a8bf5753c9 --- /dev/null +++ b/internal/gatewayapi/filters.go @@ -0,0 +1,635 @@ +// 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 gatewayapi + +import ( + "fmt" + "strings" + + "github.com/envoyproxy/gateway/internal/ir" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +type FiltersTranslator interface { + HTTPFiltersTranslator +} + +var _ FiltersTranslator = (*Translator)(nil) + +type HTTPFiltersTranslator interface { + processURLRewriteFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) + processRedirectFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) + processRequestHeaderModifierFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) + processResponseHeaderModifierFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) + processUnknownHTTPFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) + processUnsupportedHTTPFilter(filter v1beta1.HTTPRouteFilter, filterContext *HTTPFiltersContext) +} + +// HTTPFiltersContext is the context of http filters processing. +type HTTPFiltersContext struct { + *HTTPFilterChains + + ParentRef *RouteParentContext + HTTPRoute *HTTPRouteContext +} + +// HTTPFilterChains contains the ir processing results. +type HTTPFilterChains struct { + DirectResponse *ir.DirectResponse + RedirectResponse *ir.Redirect + + URLRewrite *ir.URLRewrite + + AddRequestHeaders []ir.AddHeader + RemoveRequestHeaders []string + + AddResponseHeaders []ir.AddHeader + RemoveResponseHeaders []string +} + +// ProcessHTTPFilters translate gateway api http filters to IRs. +func (t *Translator) ProcessHTTPFilters(parentRef *RouteParentContext, httpRoute *HTTPRouteContext, filters []v1beta1.HTTPRouteFilter) *HTTPFiltersContext { + httpFiltersContext := &HTTPFiltersContext{ + ParentRef: parentRef, + HTTPRoute: httpRoute, + + HTTPFilterChains: &HTTPFilterChains{}, + } + + for _, filter := range filters { + // If an invalid filter type has been configured then skip processing any more filters + if httpFiltersContext.DirectResponse != nil { + break + } + + switch filter.Type { + case v1beta1.HTTPRouteFilterURLRewrite: + t.processURLRewriteFilter(filter, httpFiltersContext) + case v1beta1.HTTPRouteFilterRequestRedirect: + t.processRedirectFilter(filter, httpFiltersContext) + case v1beta1.HTTPRouteFilterRequestHeaderModifier: + t.processRequestHeaderModifierFilter(filter, httpFiltersContext) + case v1beta1.HTTPRouteFilterResponseHeaderModifier: + t.processResponseHeaderModifierFilter(filter, httpFiltersContext) + case v1beta1.HTTPRouteFilterExtensionRef: + t.processUnknownHTTPFilter(filter, httpFiltersContext) + default: + t.processUnsupportedHTTPFilter(filter, httpFiltersContext) + } + } + + return httpFiltersContext +} + +func (t *Translator) processURLRewriteFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + if filterContext.URLRewrite != nil { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "Cannot configure multiple urlRewrite filters for a single HTTPRouteRule", + ) + return + } + + rewrite := filter.URLRewrite + if rewrite == nil { + return + } + + newURLRewrite := &ir.URLRewrite{} + + if rewrite.Hostname != nil { + if err := t.validateHostname(string(*rewrite.Hostname)); err != nil { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + err.Error(), + ) + return + } + redirectHost := string(*rewrite.Hostname) + newURLRewrite.Hostname = &redirectHost + } + + if rewrite.Path != nil { + switch rewrite.Path.Type { + case v1beta1.FullPathHTTPPathModifier: + if rewrite.Path.ReplacePrefixMatch != nil { + errMsg := "ReplacePrefixMatch cannot be set when rewrite path type is \"ReplaceFullPath\"" + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + if rewrite.Path.ReplaceFullPath == nil { + errMsg := "ReplaceFullPath must be set when rewrite path type is \"ReplaceFullPath\"" + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + if rewrite.Path.ReplaceFullPath != nil { + newURLRewrite.Path = &ir.HTTPPathModifier{ + FullReplace: rewrite.Path.ReplaceFullPath, + } + } + case v1beta1.PrefixMatchHTTPPathModifier: + if rewrite.Path.ReplaceFullPath != nil { + errMsg := "ReplaceFullPath cannot be set when rewrite path type is \"ReplacePrefixMatch\"" + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + if rewrite.Path.ReplacePrefixMatch == nil { + errMsg := "ReplacePrefixMatch must be set when rewrite path type is \"ReplacePrefixMatch\"" + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + if rewrite.Path.ReplacePrefixMatch != nil { + newURLRewrite.Path = &ir.HTTPPathModifier{ + PrefixMatchReplace: rewrite.Path.ReplacePrefixMatch, + } + } + default: + errMsg := fmt.Sprintf("Rewrite path type: %s is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported", rewrite.Path.Type) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + } + + filterContext.URLRewrite = newURLRewrite +} + +func (t *Translator) processRedirectFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + // Can't have two redirects for the same route + if filterContext.RedirectResponse != nil { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "Cannot configure multiple requestRedirect filters for a single HTTPRouteRule", + ) + return + } + + redirect := filter.RequestRedirect + if redirect == nil { + return + } + + redir := &ir.Redirect{} + if redirect.Scheme != nil { + // Note that gateway API may support additional schemes in the future, but unknown values + // must result in an UnsupportedValue status + if *redirect.Scheme == "http" || *redirect.Scheme == "https" { + redir.Scheme = redirect.Scheme + } else { + errMsg := fmt.Sprintf("Scheme: %s is unsupported, only 'https' and 'http' are supported", *redirect.Scheme) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + } + + if redirect.Hostname != nil { + if err := t.validateHostname(string(*redirect.Hostname)); err != nil { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + err.Error(), + ) + } else { + redirectHost := string(*redirect.Hostname) + redir.Hostname = &redirectHost + } + } + + if redirect.Path != nil { + switch redirect.Path.Type { + case v1beta1.FullPathHTTPPathModifier: + if redirect.Path.ReplaceFullPath != nil { + redir.Path = &ir.HTTPPathModifier{ + FullReplace: redirect.Path.ReplaceFullPath, + } + } + case v1beta1.PrefixMatchHTTPPathModifier: + if redirect.Path.ReplacePrefixMatch != nil { + redir.Path = &ir.HTTPPathModifier{ + PrefixMatchReplace: redirect.Path.ReplacePrefixMatch, + } + } + default: + errMsg := fmt.Sprintf("Redirect path type: %s is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported", redirect.Path.Type) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + } + + 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 { + redir.StatusCode = &redirectCode + } else { + errMsg := fmt.Sprintf("Status code %d is invalid, only 302 and 301 are supported", redirectCode) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + return + } + } + + if redirect.Port != nil { + redirectPort := uint32(*redirect.Port) + redir.Port = &redirectPort + } + + filterContext.RedirectResponse = redir +} + +func (t *Translator) processRequestHeaderModifierFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + // Make sure the header modifier config actually exists + headerModifier := filter.RequestHeaderModifier + if headerModifier == nil { + return + } + emptyFilterConfig := true // keep track of whether the provided config is empty or not + + // Add request headers + if headersToAdd := headerModifier.Add; headersToAdd != nil { + if len(headersToAdd) > 0 { + emptyFilterConfig = false + } + for _, addHeader := range headersToAdd { + emptyFilterConfig = false + if addHeader.Name == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "RequestHeaderModifier Filter cannot add a header with an empty name", + ) + // try to process the rest of the headers and produce a valid config. + continue + } + // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names + if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)), + ) + continue + } + // Check if the header is a duplicate + headerKey := string(addHeader.Name) + canAddHeader := true + for _, h := range filterContext.AddRequestHeaders { + if strings.EqualFold(h.Name, headerKey) { + canAddHeader = false + break + } + } + + if !canAddHeader { + continue + } + + newHeader := ir.AddHeader{ + Name: headerKey, + Append: true, + Value: addHeader.Value, + } + + filterContext.AddRequestHeaders = append(filterContext.AddRequestHeaders, newHeader) + } + } + + // Set headers + if headersToSet := headerModifier.Set; headersToSet != nil { + if len(headersToSet) > 0 { + emptyFilterConfig = false + } + for _, setHeader := range headersToSet { + + if setHeader.Name == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "RequestHeaderModifier Filter cannot set a header with an empty name", + ) + continue + } + // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names + if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)), + ) + continue + } + + // Check if the header to be set has already been configured + headerKey := string(setHeader.Name) + canAddHeader := true + for _, h := range filterContext.AddRequestHeaders { + if strings.EqualFold(h.Name, headerKey) { + canAddHeader = false + break + } + } + if !canAddHeader { + continue + } + newHeader := ir.AddHeader{ + Name: string(setHeader.Name), + Append: false, + Value: setHeader.Value, + } + + filterContext.AddRequestHeaders = append(filterContext.AddRequestHeaders, newHeader) + } + } + + // Remove request headers + // As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of + // headers to remove. It will remove the original header if present and then add/set the header after. + if headersToRemove := headerModifier.Remove; headersToRemove != nil { + if len(headersToRemove) > 0 { + emptyFilterConfig = false + } + for _, removedHeader := range headersToRemove { + if removedHeader == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "RequestHeaderModifier Filter cannot remove a header with an empty name", + ) + continue + } + + canRemHeader := true + for _, h := range filterContext.RemoveRequestHeaders { + if strings.EqualFold(h, removedHeader) { + canRemHeader = false + break + } + } + if !canRemHeader { + continue + } + + filterContext.RemoveRequestHeaders = append(filterContext.RemoveRequestHeaders, removedHeader) + } + } + + // Update the status if the filter failed to configure any valid headers to add/remove + if len(filterContext.AddRequestHeaders) == 0 && len(filterContext.RemoveRequestHeaders) == 0 && !emptyFilterConfig { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "RequestHeaderModifier Filter did not provide valid configuration to add/set/remove any headers", + ) + } +} + +func (t *Translator) processResponseHeaderModifierFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + // Make sure the header modifier config actually exists + headerModifier := filter.ResponseHeaderModifier + if headerModifier == nil { + return + } + emptyFilterConfig := true // keep track of whether the provided config is empty or not + + // Add response headers + if headersToAdd := headerModifier.Add; headersToAdd != nil { + if len(headersToAdd) > 0 { + emptyFilterConfig = false + } + for _, addHeader := range headersToAdd { + emptyFilterConfig = false + if addHeader.Name == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "ResponseHeaderModifier Filter cannot add a header with an empty name", + ) + // try to process the rest of the headers and produce a valid config. + continue + } + // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names + if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + fmt.Sprintf("ResponseHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)), + ) + continue + } + // Check if the header is a duplicate + headerKey := string(addHeader.Name) + canAddHeader := true + for _, h := range filterContext.AddResponseHeaders { + if strings.EqualFold(h.Name, headerKey) { + canAddHeader = false + break + } + } + + if !canAddHeader { + continue + } + + newHeader := ir.AddHeader{ + Name: headerKey, + Append: true, + Value: addHeader.Value, + } + + filterContext.AddResponseHeaders = append(filterContext.AddResponseHeaders, newHeader) + } + } + + // Set headers + if headersToSet := headerModifier.Set; headersToSet != nil { + if len(headersToSet) > 0 { + emptyFilterConfig = false + } + for _, setHeader := range headersToSet { + + if setHeader.Name == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "ResponseHeaderModifier Filter cannot set a header with an empty name", + ) + continue + } + // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names + if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + fmt.Sprintf("ResponseHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)), + ) + continue + } + + // Check if the header to be set has already been configured + headerKey := string(setHeader.Name) + canAddHeader := true + for _, h := range filterContext.AddResponseHeaders { + if strings.EqualFold(h.Name, headerKey) { + canAddHeader = false + break + } + } + if !canAddHeader { + continue + } + newHeader := ir.AddHeader{ + Name: string(setHeader.Name), + Append: false, + Value: setHeader.Value, + } + + filterContext.AddResponseHeaders = append(filterContext.AddResponseHeaders, newHeader) + } + } + + // Remove response headers + // As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of + // headers to remove. It will remove the original header if present and then add/set the header after. + if headersToRemove := headerModifier.Remove; headersToRemove != nil { + if len(headersToRemove) > 0 { + emptyFilterConfig = false + } + for _, removedHeader := range headersToRemove { + if removedHeader == "" { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "ResponseHeaderModifier Filter cannot remove a header with an empty name", + ) + continue + } + + canRemHeader := true + for _, h := range filterContext.RemoveResponseHeaders { + if strings.EqualFold(h, removedHeader) { + canRemHeader = false + break + } + } + if !canRemHeader { + continue + } + + filterContext.RemoveResponseHeaders = append(filterContext.RemoveResponseHeaders, removedHeader) + + } + } + + // Update the status if the filter failed to configure any valid headers to add/remove + if len(filterContext.AddResponseHeaders) == 0 && len(filterContext.RemoveResponseHeaders) == 0 && !emptyFilterConfig { + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "ResponseHeaderModifier Filter did not provide valid configuration to add/set/remove any headers", + ) + } +} + +func (t *Translator) processUnknownHTTPFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + // "If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. + // Instead, requests that would have been processed by that filter MUST receive a HTTP error response." + errMsg := fmt.Sprintf("Unknown custom filter type: %s", filter.Type) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + filterContext.DirectResponse = &ir.DirectResponse{ + Body: &errMsg, + StatusCode: 500, + } +} + +func (t *Translator) processUnsupportedHTTPFilter( + filter v1beta1.HTTPRouteFilter, + filterContext *HTTPFiltersContext) { + // Unsupported filters. + errMsg := fmt.Sprintf("Unsupported filter type: %s", filter.Type) + filterContext.ParentRef.SetCondition(filterContext.HTTPRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + errMsg, + ) + filterContext.DirectResponse = &ir.DirectResponse{ + Body: &errMsg, + StatusCode: 500, + } +} diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 30e7e1c52b..ddddb5c6e9 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -10,6 +10,8 @@ import ( "fmt" "strings" + "github.com/envoyproxy/gateway/internal/ir" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -22,6 +24,11 @@ const ( UDPProtocol = "UDP" ) +type protocolPort struct { + protocol v1beta1.ProtocolType + port int32 +} + func GroupPtr(name string) *v1beta1.Group { group := v1beta1.Group(name) return &group @@ -169,6 +176,54 @@ func HasReadyListener(listeners []*ListenerContext) bool { return false } +// ValidateHTTPRouteFilter validates the provided filter. +func ValidateHTTPRouteFilter(filter *v1beta1.HTTPRouteFilter) error { + switch { + case filter == nil: + return errors.New("filter is nil") + case filter.Type == v1beta1.HTTPRouteFilterRequestMirror || + filter.Type == v1beta1.HTTPRouteFilterURLRewrite || + filter.Type == v1beta1.HTTPRouteFilterRequestRedirect || + filter.Type == v1beta1.HTTPRouteFilterRequestHeaderModifier: + return nil + case filter.Type == v1beta1.HTTPRouteFilterExtensionRef: + switch { + case filter.ExtensionRef == nil: + return errors.New("extensionRef field must be specified for an extended filter") + case string(filter.ExtensionRef.Group) != egv1a1.GroupVersion.Group: + return fmt.Errorf("invalid group; must be %s", egv1a1.GroupVersion.Group) + case string(filter.ExtensionRef.Kind) != egv1a1.AuthenticationFilterKind: + return fmt.Errorf("invalid kind; must be %s", egv1a1.AuthenticationFilterKind) + default: + return nil + } + } + + return fmt.Errorf("unsupported filter type: %v", filter.Type) +} + +// GatewayOwnerLabels returns the Gateway Owner labels using +// the provided namespace and name as the values. +func GatewayOwnerLabels(namespace, name string) map[string]string { + return map[string]string{ + OwningGatewayNamespaceLabel: namespace, + OwningGatewayNameLabel: name, + } +} + +// servicePortToContainerPort translates a service port into an ephemeral +// container port. +func servicePortToContainerPort(servicePort int32) int32 { + // If the service port is a privileged port (1-1023) + // add a constant to the value converting it into an ephemeral port. + // This allows the container to bind to the port without needing a + // CAP_NET_BIND_SERVICE capability. + if servicePort < minEphemeralPort { + return servicePort + wellKnownPortShift + } + return servicePort +} + // computeHosts returns a list of the intersecting hostnames between the route // and the listener. func computeHosts(routeHostnames []string, listenerHostname *v1beta1.Hostname) []string { @@ -233,7 +288,7 @@ func hostnameMatchesWildcardHostname(hostname, wildcardHostname string) bool { return len(wildcardMatch) > 0 } -func containsPort(ports []*ProtocolPort, port *ProtocolPort) bool { +func containsPort(ports []*protocolPort, port *protocolPort) bool { for _, protocolPort := range ports { if protocolPort.port == port.port && layer4Protocol(protocolPort) == layer4Protocol(port) { return true @@ -242,7 +297,7 @@ func containsPort(ports []*ProtocolPort, port *ProtocolPort) bool { return false } -func layer4Protocol(protocolPort *ProtocolPort) string { +func layer4Protocol(protocolPort *protocolPort) string { switch protocolPort.protocol { case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.TLSProtocolType, v1beta1.TCPProtocolType: return TCPProtocol @@ -251,28 +306,46 @@ func layer4Protocol(protocolPort *ProtocolPort) string { } } -// ValidateHTTPRouteFilter validates the provided filter. -func ValidateHTTPRouteFilter(filter *v1beta1.HTTPRouteFilter) error { - switch { - case filter == nil: - return errors.New("filter is nil") - case filter.Type == v1beta1.HTTPRouteFilterRequestMirror || - filter.Type == v1beta1.HTTPRouteFilterURLRewrite || - filter.Type == v1beta1.HTTPRouteFilterRequestRedirect || - filter.Type == v1beta1.HTTPRouteFilterRequestHeaderModifier: +type crossNamespaceFrom struct { + group string + kind string + namespace string +} + +type crossNamespaceTo struct { + group string + kind string + namespace string + name string +} + +func irStringKey(gateway *v1beta1.Gateway) string { + return fmt.Sprintf("%s-%s", gateway.Namespace, gateway.Name) +} + +func irHTTPListenerName(listener *ListenerContext) string { + return fmt.Sprintf("%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name) +} + +func irTCPListenerName(listener *ListenerContext, tlsRoute *TLSRouteContext) string { + return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, tlsRoute.Name) +} + +func irUDPListenerName(listener *ListenerContext, udpRoute *UDPRouteContext) string { + return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, udpRoute.Name) +} + +func routeName(route RouteContext, ruleIdx, matchIdx int) string { + return fmt.Sprintf("%s-%s-rule-%d-match-%d", route.GetNamespace(), route.GetName(), ruleIdx, matchIdx) +} + +func irTLSConfig(tlsSecret *v1.Secret) *ir.TLSListenerConfig { + if tlsSecret == nil { return nil - case filter.Type == v1beta1.HTTPRouteFilterExtensionRef: - switch { - case filter.ExtensionRef == nil: - return errors.New("extensionRef field must be specified for an extended filter") - case string(filter.ExtensionRef.Group) != egv1a1.GroupVersion.Group: - return fmt.Errorf("invalid group; must be %s", egv1a1.GroupVersion.Group) - case string(filter.ExtensionRef.Kind) != egv1a1.AuthenticationFilterKind: - return fmt.Errorf("invalid kind; must be %s", egv1a1.AuthenticationFilterKind) - default: - return nil - } } - return fmt.Errorf("unsupported filter type: %v", filter.Type) + return &ir.TLSListenerConfig{ + ServerCertificate: tlsSecret.Data[v1.TLSCertKey], + PrivateKey: tlsSecret.Data[v1.TLSPrivateKeyKey], + } } diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go new file mode 100644 index 0000000000..31415031ba --- /dev/null +++ b/internal/gatewayapi/listener.go @@ -0,0 +1,131 @@ +// 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 gatewayapi + +import ( + "fmt" + + "github.com/envoyproxy/gateway/internal/ir" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var _ ListenersTranslator = (*Translator)(nil) + +type ListenersTranslator interface { + ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap, infraIR InfraIRMap, resources *Resources) +} + +func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap, infraIR InfraIRMap, resources *Resources) { + + t.validateConflictedLayer7Listeners(gateways) + t.validateConflictedLayer4Listeners(gateways, v1beta1.TCPProtocolType) + t.validateConflictedLayer4Listeners(gateways, v1beta1.UDPProtocolType) + + // Iterate through all listeners to validate spec + // and compute status for each, and add valid ones + // to the Xds IR. + for _, gateway := range gateways { + // init IR per gateway + irKey := irStringKey(gateway.Gateway) + gwXdsIR := &ir.Xds{} + gwInfraIR := ir.NewInfra() + gwInfraIR.Proxy.Name = irKey + gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayOwnerLabels(gateway.Namespace, gateway.Name) + if len(t.ProxyImage) > 0 { + gwInfraIR.Proxy.Image = t.ProxyImage + } + + // save the IR references in the map before the translation starts + xdsIR[irKey] = gwXdsIR + infraIR[irKey] = gwInfraIR + + // Infra IR proxy ports must be unique. + var foundPorts []*protocolPort + + for _, listener := range gateway.listeners { + // Process protocol & supported kinds + switch listener.Protocol { + case v1beta1.TLSProtocolType: + t.validateAllowedRoutes(listener, KindTLSRoute) + case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: + t.validateAllowedRoutes(listener, KindHTTPRoute) + case v1beta1.UDPProtocolType: + t.validateAllowedRoutes(listener, KindUDPRoute) + default: + listener.SetCondition( + v1beta1.ListenerConditionAccepted, + metav1.ConditionFalse, + v1beta1.ListenerReasonUnsupportedProtocol, + fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, or %s.", listener.Protocol, + v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.UDPProtocolType), + ) + } + + // Validate allowed namespaces + t.validateAllowedNamespaces(listener) + + // Process TLS configuration + t.validateTLSConfiguration(listener, resources) + + // Process Hostname configuration + t.validateHostName(listener) + + // Process conditions and check if the listener is ready + isReady := t.validateListenerConditions(listener) + if !isReady { + continue + } + + // Add the listener to the Xds IR + servicePort := &protocolPort{protocol: listener.Protocol, port: int32(listener.Port)} + containerPort := servicePortToContainerPort(servicePort.port) + switch listener.Protocol { + case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: + irListener := &ir.HTTPListener{ + Name: irHTTPListenerName(listener), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: irTLSConfig(listener.tlsSecret), + } + if listener.Hostname != nil { + irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) + } else { + // Hostname specifies the virtual hostname to match for protocol types that define this concept. + // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. + // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. + irListener.Hostnames = append(irListener.Hostnames, "*") + } + gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) + } + + // Add the listener to the Infra IR. Infra IR ports must have a unique port number per layer-4 protocol + // (TCP or UDP). + if !containsPort(foundPorts, servicePort) { + foundPorts = append(foundPorts, servicePort) + var proto ir.ProtocolType + switch listener.Protocol { + case v1beta1.HTTPProtocolType: + proto = ir.HTTPProtocolType + case v1beta1.HTTPSProtocolType: + proto = ir.HTTPSProtocolType + case v1beta1.TLSProtocolType: + proto = ir.TLSProtocolType + case v1beta1.UDPProtocolType: + proto = ir.UDPProtocolType + } + infraPort := ir.ListenerPort{ + Name: string(listener.Name), + Protocol: proto, + ServicePort: servicePort.port, + ContainerPort: containerPort, + } + // Only 1 listener is supported. + gwInfraIR.Proxy.Listeners[0].Ports = append(gwInfraIR.Proxy.Listeners[0].Ports, infraPort) + } + } + } +} diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go new file mode 100644 index 0000000000..2c4ca738da --- /dev/null +++ b/internal/gatewayapi/resource.go @@ -0,0 +1,75 @@ +// 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 gatewayapi + +import ( + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +type XdsIRMap map[string]*ir.Xds +type InfraIRMap map[string]*ir.Infra + +// Resources holds the Gateway API and related +// resources that the translators needs as inputs. +// +k8s:deepcopy-gen=true +type Resources struct { + Gateways []*v1beta1.Gateway + HTTPRoutes []*v1beta1.HTTPRoute + TLSRoutes []*v1alpha2.TLSRoute + UDPRoutes []*v1alpha2.UDPRoute + ReferenceGrants []*v1alpha2.ReferenceGrant + Namespaces []*v1.Namespace + Services []*v1.Service + Secrets []*v1.Secret + AuthenFilters []*egv1a1.AuthenticationFilter +} + +func NewResources() *Resources { + return &Resources{ + Gateways: []*v1beta1.Gateway{}, + HTTPRoutes: []*v1beta1.HTTPRoute{}, + TLSRoutes: []*v1alpha2.TLSRoute{}, + Services: []*v1.Service{}, + Secrets: []*v1.Secret{}, + ReferenceGrants: []*v1alpha2.ReferenceGrant{}, + Namespaces: []*v1.Namespace{}, + AuthenFilters: []*egv1a1.AuthenticationFilter{}, + } +} + +func (r *Resources) GetNamespace(name string) *v1.Namespace { + for _, ns := range r.Namespaces { + if ns.Name == name { + return ns + } + } + + return nil +} + +func (r *Resources) GetService(namespace, name string) *v1.Service { + for _, svc := range r.Services { + if svc.Namespace == namespace && svc.Name == name { + return svc + } + } + + return nil +} + +func (r *Resources) GetSecret(namespace, name string) *v1.Secret { + for _, secret := range r.Secrets { + if secret.Namespace == namespace && secret.Name == name { + return secret + } + } + + return nil +} diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go new file mode 100644 index 0000000000..6cc2d47a1a --- /dev/null +++ b/internal/gatewayapi/route.go @@ -0,0 +1,630 @@ +// 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 gatewayapi + +import ( + "fmt" + "strings" + + "github.com/envoyproxy/gateway/internal/ir" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var _ RoutesTranslator = (*Translator)(nil) + +type RoutesTranslator interface { + ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*HTTPRouteContext + ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext + ProcessUDPRoutes(udpRoutes []*v1alpha2.UDPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*UDPRouteContext +} + +func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*HTTPRouteContext { + var relevantHTTPRoutes []*HTTPRouteContext + + for _, h := range httpRoutes { + if h == nil { + panic("received nil httproute") + } + httpRoute := &HTTPRouteContext{HTTPRoute: h} + + // Find out if this route attaches to one of our Gateway's listeners, + // and if so, get the list of listeners that allow it to attach for each + // parentRef. + relevantRoute := t.processAllowedListenersForParentRefs(httpRoute, gateways, resources) + if !relevantRoute { + continue + } + + relevantHTTPRoutes = append(relevantHTTPRoutes, httpRoute) + + t.processHTTPRouteParentRefs(httpRoute, resources, xdsIR) + } + + return relevantHTTPRoutes +} + +func (t *Translator) processHTTPRouteParentRefs(httpRoute *HTTPRouteContext, resources *Resources, xdsIR XdsIRMap) { + for _, parentRef := range httpRoute.parentRefs { + // Skip parent refs that did not accept the route + if !parentRef.IsAccepted(httpRoute) { + continue + } + + // Need to compute Route rules within the parentRef loop because + // any conditions that come out of it have to go on each RouteParentStatus, + // not on the Route as a whole. + routeRoutes := t.processHTTPRouteRules(httpRoute, parentRef, resources) + + var hasHostnameIntersection = t.processHTTPRouteParentRefListener(httpRoute, routeRoutes, parentRef, xdsIR) + if !hasHostnameIntersection { + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNoMatchingListenerHostname, + "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", + ) + } + + // If no negative conditions have been set, the route is considered "Accepted=True". + if parentRef.httpRoute != nil && + len(parentRef.httpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { + parentRef.SetCondition(httpRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + } +} + +func (t *Translator) processHTTPRouteRules(httpRoute *HTTPRouteContext, parentRef *RouteParentContext, resources *Resources) []*ir.HTTPRoute { + var routeRoutes []*ir.HTTPRoute + + // compute matches, filters, backends + for ruleIdx, rule := range httpRoute.Spec.Rules { + httpFiltersContext := t.ProcessHTTPFilters(parentRef, httpRoute, rule.Filters) + + // A rule is matched if any one of its matches + // is satisfied (i.e. a logical "OR"), so generate + // a unique Xds IR HTTPRoute per match. + var ruleRoutes []*ir.HTTPRoute = t.processHTTPRouteRule(httpRoute, ruleIdx, httpFiltersContext, rule) + + for _, backendRef := range rule.BackendRefs { + destination, backendWeight := t.processRuleRouteDestination(backendRef, parentRef, httpRoute, resources) + for _, route := range ruleRoutes { + // If the route already has a direct response or redirect configured, then it was from a filter so skip + // processing any destinations for this route. + if route.DirectResponse == nil && route.Redirect == nil { + if destination != nil { + route.Destinations = append(route.Destinations, destination) + route.BackendWeights.Valid += backendWeight + + } else { + route.BackendWeights.Invalid += backendWeight + } + } + } + } + + // If the route has no valid backends then just use a direct response and don't fuss with weighted responses + for _, ruleRoute := range ruleRoutes { + if ruleRoute.BackendWeights.Invalid > 0 && len(ruleRoute.Destinations) == 0 { + ruleRoute.DirectResponse = &ir.DirectResponse{ + StatusCode: 500, + } + } + } + + // TODO handle: + // - sum of weights for valid backend refs is 0 + // - etc. + + routeRoutes = append(routeRoutes, ruleRoutes...) + } + + return routeRoutes +} + +func (t *Translator) processHTTPRouteRule(httpRoute *HTTPRouteContext, ruleIdx int, httpFiltersContext *HTTPFiltersContext, rule v1beta1.HTTPRouteRule) []*ir.HTTPRoute { + var ruleRoutes []*ir.HTTPRoute + + // A rule is matched if any one of its matches + // is satisfied (i.e. a logical "OR"), so generate + // a unique Xds IR HTTPRoute per match. + for matchIdx, match := range rule.Matches { + irRoute := &ir.HTTPRoute{ + Name: routeName(httpRoute, ruleIdx, matchIdx), + } + + if match.Path != nil { + switch PathMatchTypeDerefOr(match.Path.Type, v1beta1.PathMatchPathPrefix) { + case v1beta1.PathMatchPathPrefix: + irRoute.PathMatch = &ir.StringMatch{ + Prefix: match.Path.Value, + } + case v1beta1.PathMatchExact: + irRoute.PathMatch = &ir.StringMatch{ + Exact: match.Path.Value, + } + case v1beta1.PathMatchRegularExpression: + irRoute.PathMatch = &ir.StringMatch{ + SafeRegex: match.Path.Value, + } + } + } + for _, headerMatch := range match.Headers { + switch HeaderMatchTypeDerefOr(headerMatch.Type, v1beta1.HeaderMatchExact) { + case v1beta1.HeaderMatchExact: + irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ + Name: string(headerMatch.Name), + Exact: StringPtr(headerMatch.Value), + }) + case v1beta1.HeaderMatchRegularExpression: + irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ + Name: string(headerMatch.Name), + SafeRegex: StringPtr(headerMatch.Value), + }) + } + } + for _, queryParamMatch := range match.QueryParams { + switch QueryParamMatchTypeDerefOr(queryParamMatch.Type, v1beta1.QueryParamMatchExact) { + case v1beta1.QueryParamMatchExact: + irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ + Name: queryParamMatch.Name, + Exact: StringPtr(queryParamMatch.Value), + }) + case v1beta1.QueryParamMatchRegularExpression: + irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ + Name: queryParamMatch.Name, + SafeRegex: StringPtr(queryParamMatch.Value), + }) + } + } + + if match.Method != nil { + irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ + Name: ":method", + Exact: StringPtr(string(*match.Method)), + }) + } + + // Add the redirect filter or direct response that were created earlier to all the irRoutes + if httpFiltersContext.RedirectResponse != nil { + irRoute.Redirect = httpFiltersContext.RedirectResponse + } + if httpFiltersContext.DirectResponse != nil { + irRoute.DirectResponse = httpFiltersContext.DirectResponse + } + if httpFiltersContext.URLRewrite != nil { + irRoute.URLRewrite = httpFiltersContext.URLRewrite + } + if len(httpFiltersContext.AddRequestHeaders) > 0 { + irRoute.AddRequestHeaders = httpFiltersContext.AddRequestHeaders + } + if len(httpFiltersContext.RemoveRequestHeaders) > 0 { + irRoute.RemoveRequestHeaders = httpFiltersContext.RemoveRequestHeaders + } + if len(httpFiltersContext.AddResponseHeaders) > 0 { + irRoute.AddResponseHeaders = httpFiltersContext.AddResponseHeaders + } + if len(httpFiltersContext.RemoveResponseHeaders) > 0 { + irRoute.RemoveResponseHeaders = httpFiltersContext.RemoveResponseHeaders + } + ruleRoutes = append(ruleRoutes, irRoute) + } + + return ruleRoutes +} + +func (t *Translator) processHTTPRouteParentRefListener(httpRoute *HTTPRouteContext, routeRoutes []*ir.HTTPRoute, parentRef *RouteParentContext, xdsIR XdsIRMap) bool { + var hasHostnameIntersection bool + + for _, listener := range parentRef.listeners { + hosts := computeHosts(httpRoute.GetHostnames(), listener.Hostname) + if len(hosts) == 0 { + continue + } + hasHostnameIntersection = true + + var perHostRoutes []*ir.HTTPRoute + for _, host := range hosts { + var headerMatches []*ir.StringMatch + + // If the intersecting host is more specific than the Listener's hostname, + // add an additional header match to all of the routes for it + if host != "*" && (listener.Hostname == nil || string(*listener.Hostname) != host) { + // Hostnames that are prefixed with a wildcard label (*.) + // are interpreted as a suffix match. + if strings.HasPrefix(host, "*.") { + headerMatches = append(headerMatches, &ir.StringMatch{ + Name: ":authority", + Suffix: StringPtr(host[2:]), + }) + } else { + headerMatches = append(headerMatches, &ir.StringMatch{ + Name: ":authority", + Exact: StringPtr(host), + }) + } + } + + for _, routeRoute := range routeRoutes { + hostRoute := &ir.HTTPRoute{ + Name: fmt.Sprintf("%s-%s", routeRoute.Name, host), + PathMatch: routeRoute.PathMatch, + HeaderMatches: append(headerMatches, routeRoute.HeaderMatches...), + QueryParamMatches: routeRoute.QueryParamMatches, + AddRequestHeaders: routeRoute.AddRequestHeaders, + RemoveRequestHeaders: routeRoute.RemoveRequestHeaders, + AddResponseHeaders: routeRoute.AddResponseHeaders, + RemoveResponseHeaders: routeRoute.RemoveResponseHeaders, + Destinations: routeRoute.Destinations, + Redirect: routeRoute.Redirect, + DirectResponse: routeRoute.DirectResponse, + URLRewrite: routeRoute.URLRewrite, + } + // Don't bother copying over the weights unless the route has invalid backends. + if routeRoute.BackendWeights.Invalid > 0 { + hostRoute.BackendWeights = routeRoute.BackendWeights + } + perHostRoutes = append(perHostRoutes, hostRoute) + } + } + + irKey := irStringKey(listener.gateway) + irListener := xdsIR[irKey].GetHTTPListener(irHTTPListenerName(listener)) + if irListener != nil { + irListener.Routes = append(irListener.Routes, perHostRoutes...) + } + // Theoretically there should only be one parent ref per + // Route that attaches to a given Listener, so fine to just increment here, but we + // might want to check to ensure we're not double-counting. + if len(routeRoutes) > 0 { + listener.IncrementAttachedRoutes() + } + } + + return hasHostnameIntersection +} + +func (t *Translator) ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext { + var relevantTLSRoutes []*TLSRouteContext + + for _, tls := range tlsRoutes { + if tls == nil { + panic("received nil tlsroute") + } + tlsRoute := &TLSRouteContext{TLSRoute: tls} + + // Find out if this route attaches to one of our Gateway's listeners, + // and if so, get the list of listeners that allow it to attach for each + // parentRef. + relevantRoute := t.processAllowedListenersForParentRefs(tlsRoute, gateways, resources) + if !relevantRoute { + continue + } + + relevantTLSRoutes = append(relevantTLSRoutes, tlsRoute) + + t.processTLSRouteParentRefs(tlsRoute, resources, xdsIR) + } + + return relevantTLSRoutes +} + +func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resources *Resources, xdsIR XdsIRMap) { + for _, parentRef := range tlsRoute.parentRefs { + // Skip parent refs that did not accept the route + if !parentRef.IsAccepted(tlsRoute) { + continue + } + + // Need to compute Route rules within the parentRef loop because + // any conditions that come out of it have to go on each RouteParentStatus, + // not on the Route as a whole. + var routeDestinations []*ir.RouteDestination + + // compute backends + for _, rule := range tlsRoute.Spec.Rules { + for _, backendRef := range rule.BackendRefs { + backendRef := backendRef + // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when TLSRoute graduates to v1beta1. + serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, tlsRoute.Namespace) + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + + if !t.validateBackendRef(&backendRef, parentRef, tlsRoute, resources, serviceNamespace, KindTLSRoute) { + continue + } + + weight := uint32(1) + if backendRef.Weight != nil { + weight = uint32(*backendRef.Weight) + } + + routeDestinations = append(routeDestinations, &ir.RouteDestination{ + Host: service.Spec.ClusterIP, + Port: uint32(*backendRef.Port), + Weight: weight, + }) + } + + // TODO handle: + // - no valid backend refs + // - sum of weights for valid backend refs is 0 + // - returning 500's for invalid backend refs + // - etc. + } + + var hasHostnameIntersection bool + for _, listener := range parentRef.listeners { + hosts := computeHosts(tlsRoute.GetHostnames(), listener.Hostname) + if len(hosts) == 0 { + continue + } + + hasHostnameIntersection = true + + irKey := irStringKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) + // Create the TCP Listener while parsing the TLSRoute since + // the listener directly links to a routeDestination. + irListener := &ir.TCPListener{ + Name: irTCPListenerName(listener, tlsRoute), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: &ir.TLSInspectorConfig{ + SNIs: hosts, + }, + Destinations: routeDestinations, + } + gwXdsIR := xdsIR[irKey] + gwXdsIR.TCP = append(gwXdsIR.TCP, irListener) + + // Theoretically there should only be one parent ref per + // Route that attaches to a given Listener, so fine to just increment here, but we + // might want to check to ensure we're not double-counting. + if len(routeDestinations) > 0 { + listener.IncrementAttachedRoutes() + } + } + + if !hasHostnameIntersection { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNoMatchingListenerHostname, + "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", + ) + } + + // If no negative conditions have been set, the route is considered "Accepted=True". + if parentRef.tlsRoute != nil && + len(parentRef.tlsRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + } +} + +func (t *Translator) ProcessUDPRoutes(udpRoutes []*v1alpha2.UDPRoute, gateways []*GatewayContext, resources *Resources, + xdsIR XdsIRMap) []*UDPRouteContext { + var relevantUDPRoutes []*UDPRouteContext + + for _, u := range udpRoutes { + if u == nil { + panic("received nil udproute") + } + udpRoute := &UDPRouteContext{UDPRoute: u} + + // Find out if this route attaches to one of our Gateway's listeners, + // and if so, get the list of listeners that allow it to attach for each + // parentRef. + relevantRoute := t.processAllowedListenersForParentRefs(udpRoute, gateways, resources) + if !relevantRoute { + continue + } + + relevantUDPRoutes = append(relevantUDPRoutes, udpRoute) + + t.processUDPRouteParentRefs(udpRoute, resources, xdsIR) + } + + return relevantUDPRoutes +} + +func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resources *Resources, xdsIR XdsIRMap) { + for _, parentRef := range udpRoute.parentRefs { + // Skip parent refs that did not accept the route + if !parentRef.IsAccepted(udpRoute) { + continue + } + + // Need to compute Route rules within the parentRef loop because + // any conditions that come out of it have to go on each RouteParentStatus, + // not on the Route as a whole. + var routeDestinations []*ir.RouteDestination + + // compute backends + if len(udpRoute.Spec.Rules) != 1 { + parentRef.SetCondition(udpRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "InvalidRule", + "One and only one rule is supported", + ) + continue + } + if len(udpRoute.Spec.Rules[0].BackendRefs) != 1 { + parentRef.SetCondition(udpRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "InvalidBackend", + "One and only one backend is supported", + ) + continue + } + + backendRef := udpRoute.Spec.Rules[0].BackendRefs[0] + // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when UDPRoute graduates to v1beta1. + serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, udpRoute.Namespace) + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + + if !t.validateBackendRef(&backendRef, parentRef, udpRoute, resources, serviceNamespace, KindUDPRoute) { + continue + } + + // weight is not used in udp route destinations + routeDestinations = append(routeDestinations, &ir.RouteDestination{ + Host: service.Spec.ClusterIP, + Port: uint32(*backendRef.Port), + }) + + accepted := false + for _, listener := range parentRef.listeners { + // only one route is allowed for a UDP listener + if listener.AttachedRoutes() > 0 { + continue + } + if !listener.IsReady() { + continue + } + accepted = true + irKey := irStringKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) + // Create the UDP Listener while parsing the UDPRoute since + // the listener directly links to a routeDestination. + irListener := &ir.UDPListener{ + Name: irUDPListenerName(listener, udpRoute), + Address: "0.0.0.0", + Port: uint32(containerPort), + Destinations: routeDestinations, + } + gwXdsIR := xdsIR[irKey] + gwXdsIR.UDP = append(gwXdsIR.UDP, irListener) + + // Theoretically there should only be one parent ref per + // Route that attaches to a given Listener, so fine to just increment here, but we + // might want to check to ensure we're not double-counting. + if len(routeDestinations) > 0 { + listener.IncrementAttachedRoutes() + } + } + + // If no negative conditions have been set, the route is considered "Accepted=True". + if accepted && parentRef.udpRoute != nil && + len(parentRef.udpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { + parentRef.SetCondition(udpRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + if !accepted { + parentRef.SetCondition(udpRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonUnsupportedValue, + "Multiple routes on the same UDP listener", + ) + } + } +} + +// processRuleRouteDestination takes a backendRef and translates it into a destination or sets error statuses and +// returns the weight for the backend so that 500 error responses can be returned for invalid backends in +// the same proportion as the backend would have otherwise received +func (t *Translator) processRuleRouteDestination(backendRef v1beta1.HTTPBackendRef, + parentRef *RouteParentContext, + httpRoute *HTTPRouteContext, + resources *Resources) (destination *ir.RouteDestination, backendWeight uint32) { + + weight := uint32(1) + if backendRef.Weight != nil { + weight = uint32(*backendRef.Weight) + } + + serviceNamespace := NamespaceDerefOr(backendRef.Namespace, httpRoute.Namespace) + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + + if !t.validateBackendRef(&backendRef.BackendRef, parentRef, httpRoute, resources, serviceNamespace, KindHTTPRoute) { + return nil, weight + } + + return &ir.RouteDestination{ + Host: service.Spec.ClusterIP, + Port: uint32(*backendRef.Port), + Weight: weight, + }, weight + +} + +// processAllowedListenersForParentRefs finds out if the route attaches to one of our +// Gateways' listeners, and if so, gets the list of listeners that allow it to +// attach for each parentRef. +func (t *Translator) processAllowedListenersForParentRefs(routeContext RouteContext, gateways []*GatewayContext, resources *Resources) bool { + var relevantRoute bool + + for _, parentRef := range routeContext.GetParentReferences() { + isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) + + // Parent ref is not to a Gateway that we control: skip it + if !isRelevantParentRef { + continue + } + relevantRoute = true + + parentRefCtx := routeContext.GetRouteParentContext(parentRef) + // Reset conditions since they will be recomputed during translation + parentRefCtx.ResetConditions(routeContext) + + if !HasReadyListener(selectedListeners) { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + "NoReadyListeners", + "There are no ready listeners for this parent ref", + ) + continue + } + + var allowedListeners []*ListenerContext + for _, listener := range selectedListeners { + acceptedKind := routeContext.GetRouteType() + if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: v1beta1.Kind(acceptedKind)}) && + listener.AllowsNamespace(resources.GetNamespace(routeContext.GetNamespace())) { + allowedListeners = append(allowedListeners, listener) + } + } + + if len(allowedListeners) == 0 { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNotAllowedByListeners, + "No listeners included by this parent ref allowed this attachment.", + ) + continue + } + + parentRefCtx.SetListeners(allowedListeners...) + + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + return relevantRoute +} diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.in.yaml new file mode 100644 index 0000000000..a9d0eed0a4 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.in.yaml @@ -0,0 +1,42 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml new file mode 100644 index 0000000000..5f18aa87b9 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-full-path-replace-http.out.yaml @@ -0,0 +1,104 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + path: + fullReplace: /rewrite +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.in.yaml new file mode 100644 index 0000000000..81b7145ebc --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.in.yaml @@ -0,0 +1,43 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml new file mode 100644 index 0000000000..ba15853a4e --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname-prefix-replace.out.yaml @@ -0,0 +1,106 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + hostname: "rewrite.com" + path: + prefixMatchReplace: /rewrite +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.in.yaml new file mode 100644 index 0000000000..20c01dad05 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.in.yaml @@ -0,0 +1,40 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.out.yaml new file mode 100644 index 0000000000..48b82c0e19 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-hostname.out.yaml @@ -0,0 +1,101 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + hostname: "rewrite.com" +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml index 92d47b4659..a6761ca0b8 100644 --- a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-filter-type.out.yaml @@ -59,9 +59,9 @@ httpRoutes: controllerName: gateway.envoyproxy.io/gatewayclass-controller conditions: - type: Accepted - status: "False" - reason: UnsupportedValue - message: "Unsupported filter type: URLRewrite" + status: "True" + reason: Accepted + message: "Route is accepted" xdsIR: envoy-gateway-gateway-1: http: @@ -79,9 +79,12 @@ xdsIR: exact: gateway.envoyproxy.io # I believe the correct way to handle an invalid filter should be to allow the HTTPRoute to function # normally but leave out the filter config and set the status, but this behaviour can be changed. - directResponse: - body: "Unsupported filter type: URLRewrite" - statusCode: 500 + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + hostname: "urlrewrite.envoyproxy.io" infraIR: envoy-gateway-gateway-1: proxy: diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.in.yaml new file mode 100644 index 0000000000..9aef23666f --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.in.yaml @@ -0,0 +1,43 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "-rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.out.yaml new file mode 100644 index 0000000000..de126bd1e0 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-hostname.out.yaml @@ -0,0 +1,102 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "-rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: "hostname \"-rewrite.com\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]" +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.in.yaml new file mode 100644 index 0000000000..4a80b8390a --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.in.yaml @@ -0,0 +1,48 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.out.yaml new file mode 100644 index 0000000000..6791b5a660 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-multiple-filters.out.yaml @@ -0,0 +1,111 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: Cannot configure multiple urlRewrite filters for a single HTTPRouteRule +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + hostname: "rewrite.com" + path: + prefixMatchReplace: /rewrite +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.in.yaml new file mode 100644 index 0000000000..8feeaffc60 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.in.yaml @@ -0,0 +1,43 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatches + replacePrefixMatch: /rewrites diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.out.yaml new file mode 100644 index 0000000000..ece8af8466 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path-type.out.yaml @@ -0,0 +1,102 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + hostname: "rewrite.com" + path: + type: ReplacePrefixMatches + replacePrefixMatch: /rewrites + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: "Rewrite path type: ReplacePrefixMatches is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported" +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.in.yaml new file mode 100644 index 0000000000..fb33211a08 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.in.yaml @@ -0,0 +1,43 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + replaceFullPath: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.out.yaml new file mode 100644 index 0000000000..136dde98f4 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-invalid-path.out.yaml @@ -0,0 +1,102 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replaceFullPath: /rewrite + replacePrefixMatch: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: ReplaceFullPath cannot be set when rewrite path type is "ReplacePrefixMatch" +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.in.yaml new file mode 100644 index 0000000000..99c0c5f6b8 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.in.yaml @@ -0,0 +1,41 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.out.yaml new file mode 100644 index 0000000000..d131ff4c85 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-missing-path.out.yaml @@ -0,0 +1,100 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: UnsupportedValue + message: ReplacePrefixMatch must be set when rewrite path type is "ReplacePrefixMatch" +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.in.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.in.yaml new file mode 100644 index 0000000000..a11190c319 --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.in.yaml @@ -0,0 +1,42 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite diff --git a/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml new file mode 100644 index 0000000000..ba954d033d --- /dev/null +++ b/internal/gatewayapi/testdata/httproute-with-urlrewrite-filter-prefix-replace-http.out.yaml @@ -0,0 +1,104 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + 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 + status: + listeners: + - name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Programmed + status: "True" + reason: Programmed + message: Listener is ready +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /rewrite + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + sectionName: http + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-http + address: 0.0.0.0 + port: 10080 + hostnames: + - "*.envoyproxy.io" + routes: + - name: default-httproute-1-rule-0-match-0-gateway.envoyproxy.io + pathMatch: + prefix: "/" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + urlRewrite: + path: + prefixMatchReplace: /rewrite +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:translator-tests + listeners: + - address: "" + ports: + - name: http + protocol: "HTTP" + containerPort: 10080 + servicePort: 80 diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index 96cce48188..bf46afdf26 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -6,19 +6,8 @@ package gatewayapi import ( - "fmt" - "net/netip" - "strings" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" - - egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/envoyproxy/gateway/internal/ir" ) const ( @@ -44,65 +33,15 @@ const ( wellKnownPortShift = 10000 ) -type XdsIRMap map[string]*ir.Xds -type InfraIRMap map[string]*ir.Infra - -// Resources holds the Gateway API and related -// resources that the translators needs as inputs. -// +k8s:deepcopy-gen=true -type Resources struct { - Gateways []*v1beta1.Gateway - HTTPRoutes []*v1beta1.HTTPRoute - TLSRoutes []*v1alpha2.TLSRoute - UDPRoutes []*v1alpha2.UDPRoute - ReferenceGrants []*v1alpha2.ReferenceGrant - Namespaces []*v1.Namespace - Services []*v1.Service - Secrets []*v1.Secret - AuthenFilters []*egv1a1.AuthenticationFilter -} - -func NewResources() *Resources { - return &Resources{ - Gateways: []*v1beta1.Gateway{}, - HTTPRoutes: []*v1beta1.HTTPRoute{}, - TLSRoutes: []*v1alpha2.TLSRoute{}, - Services: []*v1.Service{}, - Secrets: []*v1.Secret{}, - ReferenceGrants: []*v1alpha2.ReferenceGrant{}, - Namespaces: []*v1.Namespace{}, - AuthenFilters: []*egv1a1.AuthenticationFilter{}, - } -} +var _ TranslatorManager = (*Translator)(nil) -func (r *Resources) GetNamespace(name string) *v1.Namespace { - for _, ns := range r.Namespaces { - if ns.Name == name { - return ns - } - } +type TranslatorManager interface { + Translate(resources *Resources) *TranslateResult + GetRelevantGateways(gateways []*v1beta1.Gateway) []*GatewayContext - return nil -} - -func (r *Resources) GetService(namespace, name string) *v1.Service { - for _, svc := range r.Services { - if svc.Namespace == namespace && svc.Name == name { - return svc - } - } - - return nil -} - -func (r *Resources) GetSecret(namespace, name string) *v1.Secret { - for _, secret := range r.Secrets { - if secret.Namespace == namespace && secret.Name == name { - return secret - } - } - - return nil + RoutesTranslator + ListenersTranslator + FiltersTranslator } // Translator translates Gateway API resources to IRs and computes status @@ -127,11 +66,6 @@ type TranslateResult struct { InfraIR InfraIRMap } -type ProtocolPort struct { - protocol v1beta1.ProtocolType - port int32 -} - func newTranslateResult(gateways []*GatewayContext, httpRoutes []*HTTPRouteContext, tlsRoutes []*TLSRouteContext, udpRoutes []*UDPRouteContext, xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { @@ -208,1755 +142,3 @@ func (t *Translator) GetRelevantGateways(gateways []*v1beta1.Gateway) []*Gateway return relevant } - -type portListeners struct { - listeners []*ListenerContext - protocols sets.String - hostnames map[string]int -} - -func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap, infraIR InfraIRMap, resources *Resources) { - - t.checkConflictedLayer7Listeners(gateways) - t.checkConflictedLayer4Listeners(gateways, v1beta1.TCPProtocolType) - t.checkConflictedLayer4Listeners(gateways, v1beta1.UDPProtocolType) - - // Iterate through all listeners to validate spec - // and compute status for each, and add valid ones - // to the Xds IR. - for _, gateway := range gateways { - // init IR per gateway - irKey := irStringKey(gateway.Gateway) - gwXdsIR := &ir.Xds{} - gwInfraIR := ir.NewInfra() - gwInfraIR.Proxy.Name = irKey - gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayOwnerLabels(gateway.Namespace, gateway.Name) - if len(t.ProxyImage) > 0 { - gwInfraIR.Proxy.Image = t.ProxyImage - } - - // save the IR references in the map before the translation starts - xdsIR[irKey] = gwXdsIR - infraIR[irKey] = gwInfraIR - - // Infra IR proxy ports must be unique. - var foundPorts []*ProtocolPort - - for _, listener := range gateway.listeners { - // Process protocol & supported kinds - switch listener.Protocol { - case v1beta1.TLSProtocolType: - t.checkAllowedRoutes(listener, KindTLSRoute) - case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: - t.checkAllowedRoutes(listener, KindHTTPRoute) - case v1beta1.UDPProtocolType: - t.checkAllowedRoutes(listener, KindUDPRoute) - default: - listener.SetCondition( - v1beta1.ListenerConditionAccepted, - metav1.ConditionFalse, - v1beta1.ListenerReasonUnsupportedProtocol, - fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, or %s.", listener.Protocol, - v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.UDPProtocolType), - ) - } - - // Validate allowed namespaces - t.validateAllowedNamespaces(listener) - - // Process TLS configuration - t.checkTLS(listener, resources) - - // Process Hostname configuration - t.checkHostName(listener) - - // Process conditions and check if the listener is ready - isReady := t.checkListenerConditions(listener) - if !isReady { - continue - } - - // Add the listener to the Xds IR - servicePort := &ProtocolPort{protocol: listener.Protocol, port: int32(listener.Port)} - containerPort := servicePortToContainerPort(servicePort.port) - switch listener.Protocol { - case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: - irListener := &ir.HTTPListener{ - Name: irHTTPListenerName(listener), - Address: "0.0.0.0", - Port: uint32(containerPort), - TLS: irTLSConfig(listener.tlsSecret), - } - if listener.Hostname != nil { - irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) - } else { - // Hostname specifies the virtual hostname to match for protocol types that define this concept. - // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. - // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. - irListener.Hostnames = append(irListener.Hostnames, "*") - } - gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) - } - - // Add the listener to the Infra IR. Infra IR ports must have a unique port number per layer-4 protocol - // (TCP or UDP). - if !containsPort(foundPorts, servicePort) { - foundPorts = append(foundPorts, servicePort) - var proto ir.ProtocolType - switch listener.Protocol { - case v1beta1.HTTPProtocolType: - proto = ir.HTTPProtocolType - case v1beta1.HTTPSProtocolType: - proto = ir.HTTPSProtocolType - case v1beta1.TLSProtocolType: - proto = ir.TLSProtocolType - case v1beta1.UDPProtocolType: - proto = ir.UDPProtocolType - } - infraPort := ir.ListenerPort{ - Name: string(listener.Name), - Protocol: proto, - ServicePort: servicePort.port, - ContainerPort: containerPort, - } - // Only 1 listener is supported. - gwInfraIR.Proxy.Listeners[0].Ports = append(gwInfraIR.Proxy.Listeners[0].Ports, infraPort) - } - } - } -} - -func (t *Translator) checkListenerConditions(listener *ListenerContext) (isReady bool) { - lConditions := listener.GetConditions() - if len(lConditions) == 0 { - listener.SetCondition(v1beta1.ListenerConditionProgrammed, metav1.ConditionTrue, v1beta1.ListenerReasonProgrammed, - "Listener is ready") - return true - - } - // Any condition on the listener apart from Ready=true indicates an error. - if !(lConditions[0].Type == string(v1beta1.ListenerConditionProgrammed) && lConditions[0].Status == metav1.ConditionTrue) { - // set "Ready: false" if it's not set already. - var hasReadyCond bool - for _, existing := range lConditions { - if existing.Type == string(v1beta1.ListenerConditionProgrammed) { - hasReadyCond = true - break - } - } - if !hasReadyCond { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - "Listener is invalid, see other Conditions for details.", - ) - } - // skip computing IR - return false - } - return true -} - -func (t *Translator) validateAllowedNamespaces(listener *ListenerContext) { - if listener.AllowedRoutes != nil && - listener.AllowedRoutes.Namespaces != nil && - listener.AllowedRoutes.Namespaces.From != nil && - *listener.AllowedRoutes.Namespaces.From == v1beta1.NamespacesFromSelector { - if listener.AllowedRoutes.Namespaces.Selector == nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - "The allowedRoutes.namespaces.selector field must be specified when allowedRoutes.namespaces.from is set to \"Selector\".", - ) - } else { - selector, err := metav1.LabelSelectorAsSelector(listener.AllowedRoutes.Namespaces.Selector) - if err != nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - fmt.Sprintf("The allowedRoutes.namespaces.selector could not be parsed: %v.", err), - ) - } - - listener.namespaceSelector = selector - } - } -} - -func (t *Translator) checkTLS(listener *ListenerContext, resources *Resources) { - switch listener.Protocol { - case v1beta1.HTTPProtocolType, v1beta1.UDPProtocolType, v1beta1.TCPProtocolType: - if listener.TLS != nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - fmt.Sprintf("Listener must not have TLS set when protocol is %s.", listener.Protocol), - ) - } - case v1beta1.HTTPSProtocolType: - if listener.TLS == nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), - ) - break - } - - if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModeTerminate { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - "UnsupportedTLSMode", - fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Terminate.", *listener.TLS.Mode), - ) - break - } - - if len(listener.TLS.CertificateRefs) != 1 { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - "Listener must have exactly 1 TLS certificate ref", - ) - break - } - - certificateRef := listener.TLS.CertificateRefs[0] - - if certificateRef.Group != nil && string(*certificateRef.Group) != "" { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, - "Listener's TLS certificate ref group must be unspecified/empty.", - ) - break - } - - if certificateRef.Kind != nil && string(*certificateRef.Kind) != KindSecret { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, - fmt.Sprintf("Listener's TLS certificate ref kind must be %s.", KindSecret), - ) - break - } - - secretNamespace := listener.gateway.Namespace - - if certificateRef.Namespace != nil && string(*certificateRef.Namespace) != "" && string(*certificateRef.Namespace) != listener.gateway.Namespace { - if !isValidCrossNamespaceRef( - crossNamespaceFrom{ - group: string(v1beta1.GroupName), - kind: KindGateway, - namespace: listener.gateway.Namespace, - }, - crossNamespaceTo{ - group: "", - kind: KindSecret, - namespace: string(*certificateRef.Namespace), - name: string(certificateRef.Name), - }, - resources.ReferenceGrants, - ) { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonRefNotPermitted, - fmt.Sprintf("Certificate ref to secret %s/%s not permitted by any ReferenceGrant", *certificateRef.Namespace, certificateRef.Name), - ) - break - } - - secretNamespace = string(*certificateRef.Namespace) - } - - secret := resources.GetSecret(secretNamespace, string(certificateRef.Name)) - - if secret == nil { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, - fmt.Sprintf("Secret %s/%s does not exist.", listener.gateway.Namespace, certificateRef.Name), - ) - break - } - - if secret.Type != v1.SecretTypeTLS { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, - fmt.Sprintf("Secret %s/%s must be of type %s.", listener.gateway.Namespace, certificateRef.Name, v1.SecretTypeTLS), - ) - break - } - - if len(secret.Data[v1.TLSCertKey]) == 0 || len(secret.Data[v1.TLSPrivateKeyKey]) == 0 { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidCertificateRef, - fmt.Sprintf("Secret %s/%s must contain %s and %s.", listener.gateway.Namespace, certificateRef.Name, v1.TLSCertKey, v1.TLSPrivateKeyKey), - ) - break - } - - listener.SetTLSSecret(secret) - case v1beta1.TLSProtocolType: - if listener.TLS == nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), - ) - break - } - - if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModePassthrough { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - "UnsupportedTLSMode", - fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Passthrough.", *listener.TLS.Mode), - ) - break - } - - if len(listener.TLS.CertificateRefs) > 0 { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - "Listener must not have TLS certificate refs set for TLS mode Passthrough", - ) - break - } - } -} - -func (t *Translator) checkHostName(listener *ListenerContext) { - if listener.Protocol == v1beta1.UDPProtocolType || listener.Protocol == v1beta1.TCPProtocolType { - if listener.Hostname != nil { - listener.SetCondition( - v1beta1.ListenerConditionProgrammed, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalid, - fmt.Sprintf("Listener must not have hostname set when protocol is %s.", listener.Protocol), - ) - } - } -} - -func (t *Translator) checkAllowedRoutes(listener *ListenerContext, routeKind v1beta1.Kind) { - if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { - listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: routeKind}) - } else { - for _, kind := range listener.AllowedRoutes.Kinds { - if kind.Group != nil && string(*kind.Group) != v1beta1.GroupName { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidRouteKinds, - fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), - ) - continue - } - - if kind.Kind != routeKind { - listener.SetCondition( - v1beta1.ListenerConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.ListenerReasonInvalidRouteKinds, - fmt.Sprintf("Kind is not supported, kind must be %s", routeKind), - ) - continue - } - listener.SetSupportedKinds(kind) - } - } -} - -func (t *Translator) checkConflictedLayer7Listeners(gateways []*GatewayContext) { - // Iterate through all layer-7 (HTTP, HTTPS, TLS) listeners and collect info about protocols - // and hostnames per port. - for _, gateway := range gateways { - portListenerInfo := map[v1beta1.PortNumber]*portListeners{} - for _, listener := range gateway.listeners { - if listener.Protocol == v1beta1.UDPProtocolType { - continue - } - if portListenerInfo[listener.Port] == nil { - portListenerInfo[listener.Port] = &portListeners{ - protocols: sets.NewString(), - hostnames: map[string]int{}, - } - } - - portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) - - var protocol string - switch listener.Protocol { - // HTTPS and TLS can co-exist on the same port - case v1beta1.HTTPSProtocolType, v1beta1.TLSProtocolType: - protocol = "https/tls" - default: - protocol = string(listener.Protocol) - } - portListenerInfo[listener.Port].protocols.Insert(protocol) - - var hostname string - if listener.Hostname != nil { - hostname = string(*listener.Hostname) - } - - portListenerInfo[listener.Port].hostnames[hostname]++ - } - - // Set Conflicted conditions for any listeners with conflicting specs. - for _, info := range portListenerInfo { - for _, listener := range info.listeners { - if len(info.protocols) > 1 { - listener.SetCondition( - v1beta1.ListenerConditionConflicted, - metav1.ConditionTrue, - v1beta1.ListenerReasonProtocolConflict, - "All listeners for a given port must use a compatible protocol", - ) - } - - var hostname string - if listener.Hostname != nil { - hostname = string(*listener.Hostname) - } - - if info.hostnames[hostname] > 1 { - listener.SetCondition( - v1beta1.ListenerConditionConflicted, - metav1.ConditionTrue, - v1beta1.ListenerReasonHostnameConflict, - "All listeners for a given port must use a unique hostname", - ) - } - } - } - } -} - -func (t *Translator) checkConflictedLayer4Listeners(gateways []*GatewayContext, protocol v1beta1.ProtocolType) { - // Iterate through all layer-4(TCP UDP) listeners and check if there are more than one listener on the same port - for _, gateway := range gateways { - portListenerInfo := map[v1beta1.PortNumber]*portListeners{} - for _, listener := range gateway.listeners { - if listener.Protocol == protocol { - if portListenerInfo[listener.Port] == nil { - portListenerInfo[listener.Port] = &portListeners{} - } - portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) - } - } - - // Leave the first one and set Conflicted conditions for all other listeners with conflicting specs. - for _, info := range portListenerInfo { - if len(info.listeners) > 1 { - for i := 1; i < len(info.listeners); i++ { - info.listeners[i].SetCondition( - v1beta1.ListenerConditionConflicted, - metav1.ConditionTrue, - v1beta1.ListenerReasonProtocolConflict, - fmt.Sprintf("Only one %s listener is allowed in a given port", protocol), - ) - } - } - } - } -} - -// servicePortToContainerPort translates a service port into an ephemeral -// container port. -func servicePortToContainerPort(servicePort int32) int32 { - // If the service port is a privileged port (1-1023) - // add a constant to the value converting it into an ephemeral port. - // This allows the container to bind to the port without needing a - // CAP_NET_BIND_SERVICE capability. - if servicePort < minEphemeralPort { - return servicePort + wellKnownPortShift - } - return servicePort -} - -// buildRuleRouteDest takes a backendRef and translates it into a destination or sets error statuses and -// returns the weight for the backend so that 500 error responses can be returned for invalid backends in -// the same proportion as the backend would have otherwise received -func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, - parentRef *RouteParentContext, - httpRoute *HTTPRouteContext, - resources *Resources) (destination *ir.RouteDestination, backendWeight uint32) { - - weight := uint32(1) - if backendRef.Weight != nil { - weight = uint32(*backendRef.Weight) - } - - serviceNamespace := NamespaceDerefOr(backendRef.Namespace, httpRoute.Namespace) - service := resources.GetService(serviceNamespace, string(backendRef.Name)) - - if !checkBackendRef(&backendRef.BackendRef, parentRef, httpRoute, resources, serviceNamespace, KindHTTPRoute) { - return nil, weight - } - - return &ir.RouteDestination{ - Host: service.Spec.ClusterIP, - Port: uint32(*backendRef.Port), - Weight: weight, - }, weight - -} - -func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*HTTPRouteContext { - var relevantHTTPRoutes []*HTTPRouteContext - - for _, h := range httpRoutes { - if h == nil { - panic("received nil httproute") - } - httpRoute := &HTTPRouteContext{HTTPRoute: h} - - // Find out if this route attaches to one of our Gateway's listeners, - // and if so, get the list of listeners that allow it to attach for each - // parentRef. - relevantRoute := processAllowedListenersForParentRefs(httpRoute, gateways, resources) - if !relevantRoute { - continue - } - - relevantHTTPRoutes = append(relevantHTTPRoutes, httpRoute) - - for _, parentRef := range httpRoute.parentRefs { - // Skip parent refs that did not accept the route - if !parentRef.IsAccepted(httpRoute) { - continue - } - - // Need to compute Route rules within the parentRef loop because - // any conditions that come out of it have to go on each RouteParentStatus, - // not on the Route as a whole. - var routeRoutes []*ir.HTTPRoute - - // compute matches, filters, backends - for ruleIdx, rule := range httpRoute.Spec.Rules { - var ruleRoutes []*ir.HTTPRoute - - // First see if there are any filters in the rules. Then apply those filters to any irRoutes. - var directResponse *ir.DirectResponse - var redirectResponse *ir.Redirect - - addRequestHeaders := []ir.AddHeader{} - removeRequestHeaders := []string{} - - addResponseHeaders := []ir.AddHeader{} - removeResponseHeaders := []string{} - - // Process the filters for this route rule - for _, filter := range rule.Filters { - if directResponse != nil { - break // If an invalid filter type has been configured then skip processing any more filters - } - switch filter.Type { - case v1beta1.HTTPRouteFilterRequestRedirect: - // Can't have two redirects for the same route - if redirectResponse != nil { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "Cannot configure multiple requestRedirect filters for a single HTTPRouteRule", - ) - continue - } - - redirect := filter.RequestRedirect - if redirect == nil { - break - } - - redir := &ir.Redirect{} - if redirect.Scheme != nil { - // Note that gateway API may support additional schemes in the future, but unknown values - // must result in an UnsupportedValue status - if *redirect.Scheme == "http" || *redirect.Scheme == "https" { - redir.Scheme = redirect.Scheme - } else { - errMsg := fmt.Sprintf("Scheme: %s is unsupported, only 'https' and 'http' are supported", *redirect.Scheme) - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - errMsg, - ) - continue - } - } - - if redirect.Hostname != nil { - if err := isValidHostname(string(*redirect.Hostname)); err != nil { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - err.Error(), - ) - continue - } else { - redirectHost := string(*redirect.Hostname) - redir.Hostname = &redirectHost - } - } - - if redirect.Path != nil { - switch redirect.Path.Type { - case v1beta1.FullPathHTTPPathModifier: - if redirect.Path.ReplaceFullPath != nil { - redir.Path = &ir.HTTPPathModifier{ - FullReplace: redirect.Path.ReplaceFullPath, - } - } - case v1beta1.PrefixMatchHTTPPathModifier: - if redirect.Path.ReplacePrefixMatch != nil { - redir.Path = &ir.HTTPPathModifier{ - PrefixMatchReplace: redirect.Path.ReplacePrefixMatch, - } - } - default: - errMsg := fmt.Sprintf("Redirect path type: %s is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported", redirect.Path.Type) - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - errMsg, - ) - continue - } - } - - 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 { - redir.StatusCode = &redirectCode - } else { - errMsg := fmt.Sprintf("Status code %d is invalid, only 302 and 301 are supported", redirectCode) - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - errMsg, - ) - continue - } - } - - if redirect.Port != nil { - redirectPort := uint32(*redirect.Port) - redir.Port = &redirectPort - } - - redirectResponse = redir - case v1beta1.HTTPRouteFilterRequestHeaderModifier: - // Make sure the header modifier config actually exists - headerModifier := filter.RequestHeaderModifier - if headerModifier == nil { - break - } - emptyFilterConfig := true // keep track of whether the provided config is empty or not - - // Add request headers - if headersToAdd := headerModifier.Add; headersToAdd != nil { - if len(headersToAdd) > 0 { - emptyFilterConfig = false - } - for _, addHeader := range headersToAdd { - emptyFilterConfig = false - if addHeader.Name == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "RequestHeaderModifier Filter cannot add a header with an empty name", - ) - // try to process the rest of the headers and produce a valid config. - continue - } - // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names - if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)), - ) - continue - } - // Check if the header is a duplicate - headerKey := string(addHeader.Name) - canAddHeader := true - for _, h := range addRequestHeaders { - if strings.EqualFold(h.Name, headerKey) { - canAddHeader = false - break - } - } - - if !canAddHeader { - continue - } - - newHeader := ir.AddHeader{ - Name: headerKey, - Append: true, - Value: addHeader.Value, - } - - addRequestHeaders = append(addRequestHeaders, newHeader) - } - } - - // Set headers - if headersToSet := headerModifier.Set; headersToSet != nil { - if len(headersToSet) > 0 { - emptyFilterConfig = false - } - for _, setHeader := range headersToSet { - - if setHeader.Name == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "RequestHeaderModifier Filter cannot set a header with an empty name", - ) - continue - } - // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names - if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("RequestHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)), - ) - continue - } - - // Check if the header to be set has already been configured - headerKey := string(setHeader.Name) - canAddHeader := true - for _, h := range addRequestHeaders { - if strings.EqualFold(h.Name, headerKey) { - canAddHeader = false - break - } - } - if !canAddHeader { - continue - } - newHeader := ir.AddHeader{ - Name: string(setHeader.Name), - Append: false, - Value: setHeader.Value, - } - - addRequestHeaders = append(addRequestHeaders, newHeader) - } - } - - // Remove request headers - // As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of - // headers to remove. It will remove the original header if present and then add/set the header after. - if headersToRemove := headerModifier.Remove; headersToRemove != nil { - if len(headersToRemove) > 0 { - emptyFilterConfig = false - } - for _, removedHeader := range headersToRemove { - if removedHeader == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "RequestHeaderModifier Filter cannot remove a header with an empty name", - ) - continue - } - - canRemHeader := true - for _, h := range removeRequestHeaders { - if strings.EqualFold(h, removedHeader) { - canRemHeader = false - break - } - } - if !canRemHeader { - continue - } - - removeRequestHeaders = append(removeRequestHeaders, removedHeader) - - } - } - - // Update the status if the filter failed to configure any valid headers to add/remove - if len(addRequestHeaders) == 0 && len(removeRequestHeaders) == 0 && !emptyFilterConfig { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "RequestHeaderModifier Filter did not provide valid configuration to add/set/remove any headers", - ) - } - case v1beta1.HTTPRouteFilterResponseHeaderModifier: - // Make sure the header modifier config actually exists - headerModifier := filter.ResponseHeaderModifier - if headerModifier == nil { - break - } - emptyFilterConfig := true // keep track of whether the provided config is empty or not - - // Add response headers - if headersToAdd := headerModifier.Add; headersToAdd != nil { - if len(headersToAdd) > 0 { - emptyFilterConfig = false - } - for _, addHeader := range headersToAdd { - emptyFilterConfig = false - if addHeader.Name == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "ResponseHeaderModifier Filter cannot add a header with an empty name", - ) - // try to process the rest of the headers and produce a valid config. - continue - } - // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names - if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("ResponseHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)), - ) - continue - } - // Check if the header is a duplicate - headerKey := string(addHeader.Name) - canAddHeader := true - for _, h := range addResponseHeaders { - if strings.EqualFold(h.Name, headerKey) { - canAddHeader = false - break - } - } - - if !canAddHeader { - continue - } - - newHeader := ir.AddHeader{ - Name: headerKey, - Append: true, - Value: addHeader.Value, - } - - addResponseHeaders = append(addResponseHeaders, newHeader) - } - } - - // Set headers - if headersToSet := headerModifier.Set; headersToSet != nil { - if len(headersToSet) > 0 { - emptyFilterConfig = false - } - for _, setHeader := range headersToSet { - - if setHeader.Name == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "ResponseHeaderModifier Filter cannot set a header with an empty name", - ) - continue - } - // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names - if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - fmt.Sprintf("ResponseHeaderModifier Filter cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)), - ) - continue - } - - // Check if the header to be set has already been configured - headerKey := string(setHeader.Name) - canAddHeader := true - for _, h := range addResponseHeaders { - if strings.EqualFold(h.Name, headerKey) { - canAddHeader = false - break - } - } - if !canAddHeader { - continue - } - newHeader := ir.AddHeader{ - Name: string(setHeader.Name), - Append: false, - Value: setHeader.Value, - } - - addResponseHeaders = append(addResponseHeaders, newHeader) - } - } - - // Remove response headers - // As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of - // headers to remove. It will remove the original header if present and then add/set the header after. - if headersToRemove := headerModifier.Remove; headersToRemove != nil { - if len(headersToRemove) > 0 { - emptyFilterConfig = false - } - for _, removedHeader := range headersToRemove { - if removedHeader == "" { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "ResponseHeaderModifier Filter cannot remove a header with an empty name", - ) - continue - } - - canRemHeader := true - for _, h := range removeResponseHeaders { - if strings.EqualFold(h, removedHeader) { - canRemHeader = false - break - } - } - if !canRemHeader { - continue - } - - removeResponseHeaders = append(removeResponseHeaders, removedHeader) - - } - } - - // Update the status if the filter failed to configure any valid headers to add/remove - if len(addResponseHeaders) == 0 && len(removeResponseHeaders) == 0 && !emptyFilterConfig { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "ResponseHeaderModifier Filter did not provide valid configuration to add/set/remove any headers", - ) - } - case v1beta1.HTTPRouteFilterExtensionRef: - // "If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. - // Instead, requests that would have been processed by that filter MUST receive a HTTP error response." - errMsg := fmt.Sprintf("Unknown custom filter type: %s", filter.Type) - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - errMsg, - ) - directResponse = &ir.DirectResponse{ - Body: &errMsg, - StatusCode: 500, - } - default: - // Unsupported filters. - errMsg := fmt.Sprintf("Unsupported filter type: %s", filter.Type) - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - errMsg, - ) - directResponse = &ir.DirectResponse{ - Body: &errMsg, - StatusCode: 500, - } - } - } - - // A rule is matched if any one of its matches - // is satisfied (i.e. a logical "OR"), so generate - // a unique Xds IR HTTPRoute per match. - for matchIdx, match := range rule.Matches { - irRoute := &ir.HTTPRoute{ - Name: routeName(httpRoute, ruleIdx, matchIdx), - } - - if match.Path != nil { - switch PathMatchTypeDerefOr(match.Path.Type, v1beta1.PathMatchPathPrefix) { - case v1beta1.PathMatchPathPrefix: - irRoute.PathMatch = &ir.StringMatch{ - Prefix: match.Path.Value, - } - case v1beta1.PathMatchExact: - irRoute.PathMatch = &ir.StringMatch{ - Exact: match.Path.Value, - } - case v1beta1.PathMatchRegularExpression: - irRoute.PathMatch = &ir.StringMatch{ - SafeRegex: match.Path.Value, - } - } - } - for _, headerMatch := range match.Headers { - switch HeaderMatchTypeDerefOr(headerMatch.Type, v1beta1.HeaderMatchExact) { - case v1beta1.HeaderMatchExact: - irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ - Name: string(headerMatch.Name), - Exact: StringPtr(headerMatch.Value), - }) - case v1beta1.HeaderMatchRegularExpression: - irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ - Name: string(headerMatch.Name), - SafeRegex: StringPtr(headerMatch.Value), - }) - } - } - for _, queryParamMatch := range match.QueryParams { - switch QueryParamMatchTypeDerefOr(queryParamMatch.Type, v1beta1.QueryParamMatchExact) { - case v1beta1.QueryParamMatchExact: - irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ - Name: queryParamMatch.Name, - Exact: StringPtr(queryParamMatch.Value), - }) - case v1beta1.QueryParamMatchRegularExpression: - irRoute.QueryParamMatches = append(irRoute.QueryParamMatches, &ir.StringMatch{ - Name: queryParamMatch.Name, - SafeRegex: StringPtr(queryParamMatch.Value), - }) - } - } - - if match.Method != nil { - irRoute.HeaderMatches = append(irRoute.HeaderMatches, &ir.StringMatch{ - Name: ":method", - Exact: StringPtr(string(*match.Method)), - }) - } - - // Add the redirect filter or direct response that were created earlier to all the irRoutes - if redirectResponse != nil { - irRoute.Redirect = redirectResponse - } - if directResponse != nil { - irRoute.DirectResponse = directResponse - } - if len(addRequestHeaders) > 0 { - irRoute.AddRequestHeaders = addRequestHeaders - } - if len(removeRequestHeaders) > 0 { - irRoute.RemoveRequestHeaders = removeRequestHeaders - } - if len(addResponseHeaders) > 0 { - irRoute.AddResponseHeaders = addResponseHeaders - } - if len(removeResponseHeaders) > 0 { - irRoute.RemoveResponseHeaders = removeResponseHeaders - } - ruleRoutes = append(ruleRoutes, irRoute) - } - - for _, backendRef := range rule.BackendRefs { - destination, backendWeight := buildRuleRouteDest(backendRef, parentRef, httpRoute, resources) - for _, route := range ruleRoutes { - // If the route already has a direct response or redirect configured, then it was from a filter so skip - // processing any destinations for this route. - if route.DirectResponse == nil && route.Redirect == nil { - if destination != nil { - route.Destinations = append(route.Destinations, destination) - route.BackendWeights.Valid += backendWeight - - } else { - route.BackendWeights.Invalid += backendWeight - } - } - } - } - - // If the route has no valid backends then just use a direct response and don't fuss with weighted responses - for _, ruleRoute := range ruleRoutes { - if ruleRoute.BackendWeights.Invalid > 0 && len(ruleRoute.Destinations) == 0 { - ruleRoute.DirectResponse = &ir.DirectResponse{ - StatusCode: 500, - } - } - } - - // TODO handle: - // - sum of weights for valid backend refs is 0 - // - etc. - - routeRoutes = append(routeRoutes, ruleRoutes...) - } - - var hasHostnameIntersection bool - for _, listener := range parentRef.listeners { - hosts := computeHosts(httpRoute.GetHostnames(), listener.Hostname) - if len(hosts) == 0 { - continue - } - hasHostnameIntersection = true - - var perHostRoutes []*ir.HTTPRoute - for _, host := range hosts { - var headerMatches []*ir.StringMatch - - // If the intersecting host is more specific than the Listener's hostname, - // add an additional header match to all of the routes for it - if host != "*" && (listener.Hostname == nil || string(*listener.Hostname) != host) { - // Hostnames that are prefixed with a wildcard label (*.) - // are interpreted as a suffix match. - if strings.HasPrefix(host, "*.") { - headerMatches = append(headerMatches, &ir.StringMatch{ - Name: ":authority", - Suffix: StringPtr(host[2:]), - }) - } else { - headerMatches = append(headerMatches, &ir.StringMatch{ - Name: ":authority", - Exact: StringPtr(host), - }) - } - } - - for _, routeRoute := range routeRoutes { - hostRoute := &ir.HTTPRoute{ - Name: fmt.Sprintf("%s-%s", routeRoute.Name, host), - PathMatch: routeRoute.PathMatch, - HeaderMatches: append(headerMatches, routeRoute.HeaderMatches...), - QueryParamMatches: routeRoute.QueryParamMatches, - AddRequestHeaders: routeRoute.AddRequestHeaders, - RemoveRequestHeaders: routeRoute.RemoveRequestHeaders, - AddResponseHeaders: routeRoute.AddResponseHeaders, - RemoveResponseHeaders: routeRoute.RemoveResponseHeaders, - Destinations: routeRoute.Destinations, - Redirect: routeRoute.Redirect, - DirectResponse: routeRoute.DirectResponse, - } - // Don't bother copying over the weights unless the route has invalid backends. - if routeRoute.BackendWeights.Invalid > 0 { - hostRoute.BackendWeights = routeRoute.BackendWeights - } - perHostRoutes = append(perHostRoutes, hostRoute) - } - } - - irKey := irStringKey(listener.gateway) - irListener := xdsIR[irKey].GetHTTPListener(irHTTPListenerName(listener)) - if irListener != nil { - irListener.Routes = append(irListener.Routes, perHostRoutes...) - } - // Theoretically there should only be one parent ref per - // Route that attaches to a given Listener, so fine to just increment here, but we - // might want to check to ensure we're not double-counting. - if len(routeRoutes) > 0 { - listener.IncrementAttachedRoutes() - } - } - - if !hasHostnameIntersection { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonNoMatchingListenerHostname, - "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", - ) - } - - // If no negative conditions have been set, the route is considered "Accepted=True". - if parentRef.httpRoute != nil && - len(parentRef.httpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { - parentRef.SetCondition(httpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionTrue, - v1beta1.RouteReasonAccepted, - "Route is accepted", - ) - } - } - } - - return relevantHTTPRoutes -} - -func (t *Translator) ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext { - var relevantTLSRoutes []*TLSRouteContext - - for _, t := range tlsRoutes { - if t == nil { - panic("received nil tlsroute") - } - tlsRoute := &TLSRouteContext{TLSRoute: t} - - // Find out if this route attaches to one of our Gateway's listeners, - // and if so, get the list of listeners that allow it to attach for each - // parentRef. - relevantRoute := processAllowedListenersForParentRefs(tlsRoute, gateways, resources) - if !relevantRoute { - continue - } - - relevantTLSRoutes = append(relevantTLSRoutes, tlsRoute) - - for _, parentRef := range tlsRoute.parentRefs { - // Skip parent refs that did not accept the route - if !parentRef.IsAccepted(tlsRoute) { - continue - } - - // Need to compute Route rules within the parentRef loop because - // any conditions that come out of it have to go on each RouteParentStatus, - // not on the Route as a whole. - var routeDestinations []*ir.RouteDestination - - // compute backends - for _, rule := range tlsRoute.Spec.Rules { - for _, backendRef := range rule.BackendRefs { - backendRef := backendRef - // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when TLSRoute graduates to v1beta1. - serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, tlsRoute.Namespace) - service := resources.GetService(serviceNamespace, string(backendRef.Name)) - - if !checkBackendRef(&backendRef, parentRef, tlsRoute, resources, serviceNamespace, KindTLSRoute) { - continue - } - - weight := uint32(1) - if backendRef.Weight != nil { - weight = uint32(*backendRef.Weight) - } - - routeDestinations = append(routeDestinations, &ir.RouteDestination{ - Host: service.Spec.ClusterIP, - Port: uint32(*backendRef.Port), - Weight: weight, - }) - } - - // TODO handle: - // - no valid backend refs - // - sum of weights for valid backend refs is 0 - // - returning 500's for invalid backend refs - // - etc. - } - - var hasHostnameIntersection bool - for _, listener := range parentRef.listeners { - hosts := computeHosts(tlsRoute.GetHostnames(), listener.Hostname) - if len(hosts) == 0 { - continue - } - - hasHostnameIntersection = true - - irKey := irStringKey(listener.gateway) - containerPort := servicePortToContainerPort(int32(listener.Port)) - // Create the TCP Listener while parsing the TLSRoute since - // the listener directly links to a routeDestination. - irListener := &ir.TCPListener{ - Name: irTCPListenerName(listener, tlsRoute), - Address: "0.0.0.0", - Port: uint32(containerPort), - TLS: &ir.TLSInspectorConfig{ - SNIs: hosts, - }, - Destinations: routeDestinations, - } - gwXdsIR := xdsIR[irKey] - gwXdsIR.TCP = append(gwXdsIR.TCP, irListener) - - // Theoretically there should only be one parent ref per - // Route that attaches to a given Listener, so fine to just increment here, but we - // might want to check to ensure we're not double-counting. - if len(routeDestinations) > 0 { - listener.IncrementAttachedRoutes() - } - } - - if !hasHostnameIntersection { - parentRef.SetCondition(tlsRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonNoMatchingListenerHostname, - "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", - ) - } - - // If no negative conditions have been set, the route is considered "Accepted=True". - if parentRef.tlsRoute != nil && - len(parentRef.tlsRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { - parentRef.SetCondition(tlsRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionTrue, - v1beta1.RouteReasonAccepted, - "Route is accepted", - ) - } - } - } - - return relevantTLSRoutes -} - -func (t *Translator) ProcessUDPRoutes(udpRoutes []*v1alpha2.UDPRoute, gateways []*GatewayContext, resources *Resources, - xdsIR XdsIRMap) []*UDPRouteContext { - var relevantUDPRoutes []*UDPRouteContext - - for _, u := range udpRoutes { - if u == nil { - panic("received nil udproute") - } - udpRoute := &UDPRouteContext{UDPRoute: u} - - // Find out if this route attaches to one of our Gateway's listeners, - // and if so, get the list of listeners that allow it to attach for each - // parentRef. - relevantRoute := processAllowedListenersForParentRefs(udpRoute, gateways, resources) - if !relevantRoute { - continue - } - - relevantUDPRoutes = append(relevantUDPRoutes, udpRoute) - - for _, parentRef := range udpRoute.parentRefs { - // Skip parent refs that did not accept the route - if !parentRef.IsAccepted(udpRoute) { - continue - } - - // Need to compute Route rules within the parentRef loop because - // any conditions that come out of it have to go on each RouteParentStatus, - // not on the Route as a whole. - var routeDestinations []*ir.RouteDestination - - // compute backends - if len(udpRoute.Spec.Rules) != 1 { - parentRef.SetCondition(udpRoute, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - "InvalidRule", - "One and only one rule is supported", - ) - continue - } - if len(udpRoute.Spec.Rules[0].BackendRefs) != 1 { - parentRef.SetCondition(udpRoute, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - "InvalidBackend", - "One and only one backend is supported", - ) - continue - } - - backendRef := udpRoute.Spec.Rules[0].BackendRefs[0] - // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when UDPRoute graduates to v1beta1. - serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, udpRoute.Namespace) - service := resources.GetService(serviceNamespace, string(backendRef.Name)) - - if !checkBackendRef(&backendRef, parentRef, udpRoute, resources, serviceNamespace, KindUDPRoute) { - continue - } - - // weight is not used in udp route destinations - routeDestinations = append(routeDestinations, &ir.RouteDestination{ - Host: service.Spec.ClusterIP, - Port: uint32(*backendRef.Port), - }) - - accepted := false - for _, listener := range parentRef.listeners { - // only one route is allowed for a UDP listener - if listener.AttachedRoutes() > 0 { - continue - } - if !listener.IsReady() { - continue - } - accepted = true - irKey := irStringKey(listener.gateway) - containerPort := servicePortToContainerPort(int32(listener.Port)) - // Create the UDP Listener while parsing the UDPRoute since - // the listener directly links to a routeDestination. - irListener := &ir.UDPListener{ - Name: irUDPListenerName(listener, udpRoute), - Address: "0.0.0.0", - Port: uint32(containerPort), - Destinations: routeDestinations, - } - gwXdsIR := xdsIR[irKey] - gwXdsIR.UDP = append(gwXdsIR.UDP, irListener) - - // Theoretically there should only be one parent ref per - // Route that attaches to a given Listener, so fine to just increment here, but we - // might want to check to ensure we're not double-counting. - if len(routeDestinations) > 0 { - listener.IncrementAttachedRoutes() - } - } - - // If no negative conditions have been set, the route is considered "Accepted=True". - if accepted && parentRef.udpRoute != nil && - len(parentRef.udpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 { - parentRef.SetCondition(udpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionTrue, - v1beta1.RouteReasonAccepted, - "Route is accepted", - ) - } - if !accepted { - parentRef.SetCondition(udpRoute, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonUnsupportedValue, - "Multiple routes on the same UDP listener", - ) - } - } - } - - return relevantUDPRoutes -} - -// processAllowedListenersForParentRefs finds out if the route attaches to one of our -// Gateways' listeners, and if so, gets the list of listeners that allow it to -// attach for each parentRef. -func processAllowedListenersForParentRefs(routeContext RouteContext, gateways []*GatewayContext, resources *Resources) bool { - var relevantRoute bool - - for _, parentRef := range routeContext.GetParentReferences() { - isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) - - // Parent ref is not to a Gateway that we control: skip it - if !isRelevantParentRef { - continue - } - relevantRoute = true - - parentRefCtx := routeContext.GetRouteParentContext(parentRef) - // Reset conditions since they will be recomputed during translation - parentRefCtx.ResetConditions(routeContext) - - if !HasReadyListener(selectedListeners) { - parentRefCtx.SetCondition(routeContext, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - "NoReadyListeners", - "There are no ready listeners for this parent ref", - ) - continue - } - - var allowedListeners []*ListenerContext - for _, listener := range selectedListeners { - acceptedKind := routeContext.GetRouteType() - if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: v1beta1.Kind(acceptedKind)}) && - listener.AllowsNamespace(resources.GetNamespace(routeContext.GetNamespace())) { - allowedListeners = append(allowedListeners, listener) - } - } - - if len(allowedListeners) == 0 { - parentRefCtx.SetCondition(routeContext, - v1beta1.RouteConditionAccepted, - metav1.ConditionFalse, - v1beta1.RouteReasonNotAllowedByListeners, - "No listeners included by this parent ref allowed this attachment.", - ) - continue - } - - parentRefCtx.SetListeners(allowedListeners...) - - parentRefCtx.SetCondition(routeContext, - v1beta1.RouteConditionAccepted, - metav1.ConditionTrue, - v1beta1.RouteReasonAccepted, - "Route is accepted", - ) - } - return relevantRoute -} - -type crossNamespaceFrom struct { - group string - kind string - namespace string -} - -type crossNamespaceTo struct { - group string - kind string - namespace string - name string -} - -func isValidCrossNamespaceRef(from crossNamespaceFrom, to crossNamespaceTo, referenceGrants []*v1alpha2.ReferenceGrant) bool { - for _, referenceGrant := range referenceGrants { - // The ReferenceGrant must be defined in the namespace of - // the "to" (the referent). - if referenceGrant.Namespace != to.namespace { - continue - } - - // Check if the ReferenceGrant has a matching "from". - var fromAllowed bool - for _, refGrantFrom := range referenceGrant.Spec.From { - if string(refGrantFrom.Namespace) == from.namespace && string(refGrantFrom.Group) == from.group && string(refGrantFrom.Kind) == from.kind { - fromAllowed = true - break - } - } - if !fromAllowed { - continue - } - - // Check if the ReferenceGrant has a matching "to". - var toAllowed bool - for _, refGrantTo := range referenceGrant.Spec.To { - if string(refGrantTo.Group) == to.group && string(refGrantTo.Kind) == to.kind && (refGrantTo.Name == nil || *refGrantTo.Name == "" || string(*refGrantTo.Name) == to.name) { - toAllowed = true - break - } - } - if !toAllowed { - continue - } - - // If we got here, both the "from" and the "to" were allowed by this - // reference grant. - return true - } - - // If we got here, no reference policy or reference grant allowed both the "from" and "to". - return false -} - -// Checks if a hostname is valid according to RFC 1123 and gateway API's requirement that it not be an IP address -func isValidHostname(hostname string) error { - if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { - return fmt.Errorf("hostname %q is invalid for a redirect filter: %v", hostname, errs) - } - - // IP addresses are not allowed so parsing the hostname as an address needs to fail - if _, err := netip.ParseAddr(hostname); err == nil { - return fmt.Errorf("hostname: %q cannot be an ip address", hostname) - } - - labelIdx := 0 - for i := range hostname { - if hostname[i] == '.' { - - if i-labelIdx > 63 { - return fmt.Errorf("label: %q in hostname %q cannot exceed 63 characters", hostname[labelIdx:i], hostname) - } - labelIdx = i + 1 - } - } - // Check the last label - if len(hostname)-labelIdx > 63 { - return fmt.Errorf("label: %q in hostname %q cannot exceed 63 characters", hostname[labelIdx:], hostname) - } - - return nil -} - -func irStringKey(gateway *v1beta1.Gateway) string { - return fmt.Sprintf("%s-%s", gateway.Namespace, gateway.Name) -} - -func irHTTPListenerName(listener *ListenerContext) string { - return fmt.Sprintf("%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name) -} - -func irTCPListenerName(listener *ListenerContext, tlsRoute *TLSRouteContext) string { - return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, tlsRoute.Name) -} - -func irUDPListenerName(listener *ListenerContext, udpRoute *UDPRouteContext) string { - return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, udpRoute.Name) -} - -func routeName(route RouteContext, ruleIdx, matchIdx int) string { - return fmt.Sprintf("%s-%s-rule-%d-match-%d", route.GetNamespace(), route.GetName(), ruleIdx, matchIdx) -} - -func irTLSConfig(tlsSecret *v1.Secret) *ir.TLSListenerConfig { - if tlsSecret == nil { - return nil - } - - return &ir.TLSListenerConfig{ - ServerCertificate: tlsSecret.Data[v1.TLSCertKey], - PrivateKey: tlsSecret.Data[v1.TLSPrivateKeyKey], - } -} - -// GatewayOwnerLabels returns the Gateway Owner labels using -// the provided namespace and name as the values. -func GatewayOwnerLabels(namespace, name string) map[string]string { - return map[string]string{ - OwningGatewayNamespaceLabel: namespace, - OwningGatewayNameLabel: name, - } -} - -func checkBackendRef(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext, - resources *Resources, serviceNamespace string, routeKind v1beta1.Kind) bool { - if !checkBackendRefGroup(backendRef, parentRef, route) { - return false - } - if !checkBackendRefKind(backendRef, parentRef, route) { - return false - } - if !checkBackendNamespace(backendRef, parentRef, route, resources, routeKind) { - return false - } - if !checkBackendPort(backendRef, parentRef, route) { - return false - } - protocol := v1.ProtocolTCP - if routeKind == KindUDPRoute { - protocol = v1.ProtocolUDP - } - if !checkBackendService(backendRef, parentRef, resources, serviceNamespace, route, protocol) { - return false - } - return true -} - -func checkBackendRefGroup(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { - if backendRef.Group != nil && *backendRef.Group != "" { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonInvalidKind, - "Group is invalid, only the core API group (specified by omitting the group field or setting it to an empty string) is supported", - ) - return false - } - return true -} - -func checkBackendRefKind(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { - if backendRef.Kind != nil && *backendRef.Kind != KindService { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonInvalidKind, - "Kind is invalid, only Service is supported", - ) - return false - } - return true -} - -func checkBackendNamespace(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext, - resources *Resources, routeKind v1beta1.Kind) bool { - if backendRef.Namespace != nil && string(*backendRef.Namespace) != "" && string(*backendRef.Namespace) != route.GetNamespace() { - if !isValidCrossNamespaceRef( - crossNamespaceFrom{ - group: v1beta1.GroupName, - kind: string(routeKind), - namespace: route.GetNamespace(), - }, - crossNamespaceTo{ - group: "", - kind: KindService, - namespace: string(*backendRef.Namespace), - name: string(backendRef.Name), - }, - resources.ReferenceGrants, - ) { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonRefNotPermitted, - fmt.Sprintf("Backend ref to service %s/%s not permitted by any ReferenceGrant", *backendRef.Namespace, backendRef.Name), - ) - return false - } - } - return true -} - -func checkBackendPort(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { - if backendRef.Port == nil { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - "PortNotSpecified", - "A valid port number corresponding to a port on the Service must be specified", - ) - return false - } - return true -} -func checkBackendService(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, resources *Resources, - serviceNamespace string, route RouteContext, protocol v1.Protocol) bool { - service := resources.GetService(serviceNamespace, string(backendRef.Name)) - if service == nil { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - v1beta1.RouteReasonBackendNotFound, - fmt.Sprintf("Service %s/%s not found", NamespaceDerefOr(backendRef.Namespace, route.GetNamespace()), - string(backendRef.Name)), - ) - return false - } - var portFound bool - for _, port := range service.Spec.Ports { - portProtocol := port.Protocol - if port.Protocol == "" { // Default protocol is TCP - portProtocol = v1.ProtocolTCP - } - if port.Port == int32(*backendRef.Port) && portProtocol == protocol { - portFound = true - break - } - } - - if !portFound { - parentRef.SetCondition(route, - v1beta1.RouteConditionResolvedRefs, - metav1.ConditionFalse, - "PortNotFound", - fmt.Sprintf(string(protocol)+" Port %d not found on service %s/%s", *backendRef.Port, serviceNamespace, - string(backendRef.Name)), - ) - return false - } - return true -} diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go index 427fcc5dd9..0b0ca2b1dd 100644 --- a/internal/gatewayapi/translator_test.go +++ b/internal/gatewayapi/translator_test.go @@ -101,6 +101,8 @@ func TestIsValidHostname(t *testing.T) { err string } + translator := &Translator{} + // Setting up a hostname that is 256+ characters for a test case that does not also trip the max label size veryLongHostname := "a" label := 0 @@ -124,12 +126,12 @@ func TestIsValidHostname(t *testing.T) { { name: "dot-prefix", hostname: ".example.test.com", - err: "hostname \".example.test.com\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \".example.test.com\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "dot-suffix", hostname: "example.test.com.", - err: "hostname \"example.test.com.\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"example.test.com.\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "ip-address", @@ -139,17 +141,17 @@ func TestIsValidHostname(t *testing.T) { { name: "dash-prefix", hostname: "-example.test.com", - err: "hostname \"-example.test.com\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"-example.test.com\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "dash-suffix", hostname: "example.test.com-", - err: "hostname \"example.test.com-\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"example.test.com-\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "invalid-symbol", hostname: "examp!e.test.com", - err: "hostname \"examp!e.test.com\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"examp!e.test.com\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "long-label", @@ -164,24 +166,24 @@ func TestIsValidHostname(t *testing.T) { { name: "way-too-long-hostname", hostname: veryLongHostname, - err: fmt.Sprintf("hostname %q is invalid for a redirect filter: [must be no more than 253 characters]", veryLongHostname), + err: fmt.Sprintf("hostname %q is invalid: [must be no more than 253 characters]", veryLongHostname), }, { name: "empty-hostname", hostname: "", - err: "hostname \"\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, { name: "double-dot", hostname: "example..test.com", - err: "hostname \"example..test.com\" is invalid for a redirect filter: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", + err: "hostname \"example..test.com\" is invalid: [a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')]", }, } for _, tc := range testcases { tc := tc t.Run(tc.name, func(t *testing.T) { - err := isValidHostname(tc.hostname) + err := translator.validateHostname(tc.hostname) if tc.err == "" { assert.Nil(t, err) } else { @@ -200,6 +202,8 @@ func TestIsValidCrossNamespaceRef(t *testing.T) { want bool } + translator := &Translator{} + var testcases []*testcase baseCase := func() *testcase { @@ -304,7 +308,7 @@ func TestIsValidCrossNamespaceRef(t *testing.T) { referenceGrants = append(referenceGrants, tc.referenceGrant) } - assert.Equal(t, tc.want, isValidCrossNamespaceRef(tc.from, tc.to, referenceGrants)) + assert.Equal(t, tc.want, translator.validateCrossNamespaceRef(tc.from, tc.to, referenceGrants)) }) } } diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go new file mode 100644 index 0000000000..33da36607f --- /dev/null +++ b/internal/gatewayapi/validate.go @@ -0,0 +1,579 @@ +// 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 gatewayapi + +import ( + "fmt" + "net/netip" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func (t *Translator) validateBackendRef(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext, + resources *Resources, serviceNamespace string, routeKind v1beta1.Kind) bool { + if !t.validateBackendRefGroup(backendRef, parentRef, route) { + return false + } + if !t.validateBackendRefKind(backendRef, parentRef, route) { + return false + } + if !t.validateBackendNamespace(backendRef, parentRef, route, resources, routeKind) { + return false + } + if !t.validateBackendPort(backendRef, parentRef, route) { + return false + } + protocol := v1.ProtocolTCP + if routeKind == KindUDPRoute { + protocol = v1.ProtocolUDP + } + if !t.validateBackendService(backendRef, parentRef, resources, serviceNamespace, route, protocol) { + return false + } + return true +} + +func (t *Translator) validateBackendRefGroup(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { + if backendRef.Group != nil && *backendRef.Group != "" { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Group is invalid, only the core API group (specified by omitting the group field or setting it to an empty string) is supported", + ) + return false + } + return true +} + +func (t *Translator) validateBackendRefKind(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { + if backendRef.Kind != nil && *backendRef.Kind != KindService { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Kind is invalid, only Service is supported", + ) + return false + } + return true +} + +func (t *Translator) validateBackendNamespace(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext, + resources *Resources, routeKind v1beta1.Kind) bool { + if backendRef.Namespace != nil && string(*backendRef.Namespace) != "" && string(*backendRef.Namespace) != route.GetNamespace() { + if !t.validateCrossNamespaceRef( + crossNamespaceFrom{ + group: v1beta1.GroupName, + kind: string(routeKind), + namespace: route.GetNamespace(), + }, + crossNamespaceTo{ + group: "", + kind: KindService, + namespace: string(*backendRef.Namespace), + name: string(backendRef.Name), + }, + resources.ReferenceGrants, + ) { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonRefNotPermitted, + fmt.Sprintf("Backend ref to service %s/%s not permitted by any ReferenceGrant", *backendRef.Namespace, backendRef.Name), + ) + return false + } + } + return true +} + +func (t *Translator) validateBackendPort(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, route RouteContext) bool { + if backendRef.Port == nil { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotSpecified", + "A valid port number corresponding to a port on the Service must be specified", + ) + return false + } + return true +} +func (t *Translator) validateBackendService(backendRef *v1alpha2.BackendRef, parentRef *RouteParentContext, resources *Resources, + serviceNamespace string, route RouteContext, protocol v1.Protocol) bool { + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + if service == nil { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonBackendNotFound, + fmt.Sprintf("Service %s/%s not found", NamespaceDerefOr(backendRef.Namespace, route.GetNamespace()), + string(backendRef.Name)), + ) + return false + } + var portFound bool + for _, port := range service.Spec.Ports { + portProtocol := port.Protocol + if port.Protocol == "" { // Default protocol is TCP + portProtocol = v1.ProtocolTCP + } + if port.Port == int32(*backendRef.Port) && portProtocol == protocol { + portFound = true + break + } + } + + if !portFound { + parentRef.SetCondition(route, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotFound", + fmt.Sprintf(string(protocol)+" Port %d not found on service %s/%s", *backendRef.Port, serviceNamespace, + string(backendRef.Name)), + ) + return false + } + return true +} + +func (t *Translator) validateListenerConditions(listener *ListenerContext) (isReady bool) { + lConditions := listener.GetConditions() + if len(lConditions) == 0 { + listener.SetCondition(v1beta1.ListenerConditionProgrammed, metav1.ConditionTrue, v1beta1.ListenerReasonProgrammed, + "Listener is ready") + return true + + } + // Any condition on the listener apart from Ready=true indicates an error. + if !(lConditions[0].Type == string(v1beta1.ListenerConditionProgrammed) && lConditions[0].Status == metav1.ConditionTrue) { + // set "Ready: false" if it's not set already. + var hasReadyCond bool + for _, existing := range lConditions { + if existing.Type == string(v1beta1.ListenerConditionProgrammed) { + hasReadyCond = true + break + } + } + if !hasReadyCond { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener is invalid, see other Conditions for details.", + ) + } + // skip computing IR + return false + } + return true +} + +func (t *Translator) validateAllowedNamespaces(listener *ListenerContext) { + if listener.AllowedRoutes != nil && + listener.AllowedRoutes.Namespaces != nil && + listener.AllowedRoutes.Namespaces.From != nil && + *listener.AllowedRoutes.Namespaces.From == v1beta1.NamespacesFromSelector { + if listener.AllowedRoutes.Namespaces.Selector == nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "The allowedRoutes.namespaces.selector field must be specified when allowedRoutes.namespaces.from is set to \"Selector\".", + ) + } else { + selector, err := metav1.LabelSelectorAsSelector(listener.AllowedRoutes.Namespaces.Selector) + if err != nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("The allowedRoutes.namespaces.selector could not be parsed: %v.", err), + ) + } + + listener.namespaceSelector = selector + } + } +} + +func (t *Translator) validateTLSConfiguration(listener *ListenerContext, resources *Resources) { + switch listener.Protocol { + case v1beta1.HTTPProtocolType, v1beta1.UDPProtocolType, v1beta1.TCPProtocolType: + if listener.TLS != nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must not have TLS set when protocol is %s.", listener.Protocol), + ) + } + case v1beta1.HTTPSProtocolType: + if listener.TLS == nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), + ) + break + } + + if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModeTerminate { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + "UnsupportedTLSMode", + fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Terminate.", *listener.TLS.Mode), + ) + break + } + + if len(listener.TLS.CertificateRefs) != 1 { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener must have exactly 1 TLS certificate ref", + ) + break + } + + certificateRef := listener.TLS.CertificateRefs[0] + + if certificateRef.Group != nil && string(*certificateRef.Group) != "" { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidCertificateRef, + "Listener's TLS certificate ref group must be unspecified/empty.", + ) + break + } + + if certificateRef.Kind != nil && string(*certificateRef.Kind) != KindSecret { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidCertificateRef, + fmt.Sprintf("Listener's TLS certificate ref kind must be %s.", KindSecret), + ) + break + } + + secretNamespace := listener.gateway.Namespace + + if certificateRef.Namespace != nil && string(*certificateRef.Namespace) != "" && string(*certificateRef.Namespace) != listener.gateway.Namespace { + if !t.validateCrossNamespaceRef( + crossNamespaceFrom{ + group: string(v1beta1.GroupName), + kind: KindGateway, + namespace: listener.gateway.Namespace, + }, + crossNamespaceTo{ + group: "", + kind: KindSecret, + namespace: string(*certificateRef.Namespace), + name: string(certificateRef.Name), + }, + resources.ReferenceGrants, + ) { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonRefNotPermitted, + fmt.Sprintf("Certificate ref to secret %s/%s not permitted by any ReferenceGrant", *certificateRef.Namespace, certificateRef.Name), + ) + break + } + + secretNamespace = string(*certificateRef.Namespace) + } + + secret := resources.GetSecret(secretNamespace, string(certificateRef.Name)) + + if secret == nil { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidCertificateRef, + fmt.Sprintf("Secret %s/%s does not exist.", listener.gateway.Namespace, certificateRef.Name), + ) + break + } + + if secret.Type != v1.SecretTypeTLS { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidCertificateRef, + fmt.Sprintf("Secret %s/%s must be of type %s.", listener.gateway.Namespace, certificateRef.Name, v1.SecretTypeTLS), + ) + break + } + + if len(secret.Data[v1.TLSCertKey]) == 0 || len(secret.Data[v1.TLSPrivateKeyKey]) == 0 { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidCertificateRef, + fmt.Sprintf("Secret %s/%s must contain %s and %s.", listener.gateway.Namespace, certificateRef.Name, v1.TLSCertKey, v1.TLSPrivateKeyKey), + ) + break + } + + listener.SetTLSSecret(secret) + case v1beta1.TLSProtocolType: + if listener.TLS == nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), + ) + break + } + + if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModePassthrough { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + "UnsupportedTLSMode", + fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Passthrough.", *listener.TLS.Mode), + ) + break + } + + if len(listener.TLS.CertificateRefs) > 0 { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener must not have TLS certificate refs set for TLS mode Passthrough", + ) + break + } + } +} + +func (t *Translator) validateHostName(listener *ListenerContext) { + if listener.Protocol == v1beta1.UDPProtocolType || listener.Protocol == v1beta1.TCPProtocolType { + if listener.Hostname != nil { + listener.SetCondition( + v1beta1.ListenerConditionProgrammed, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must not have hostname set when protocol is %s.", listener.Protocol), + ) + } + } +} + +func (t *Translator) validateAllowedRoutes(listener *ListenerContext, routeKind v1beta1.Kind) { + if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { + listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: routeKind}) + } else { + for _, kind := range listener.AllowedRoutes.Kinds { + if kind.Group != nil && string(*kind.Group) != v1beta1.GroupName { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), + ) + continue + } + + if kind.Kind != routeKind { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Kind is not supported, kind must be %s", routeKind), + ) + continue + } + listener.SetSupportedKinds(kind) + } + } +} + +type portListeners struct { + listeners []*ListenerContext + protocols sets.Set[string] + hostnames map[string]int +} + +func (t *Translator) validateConflictedLayer7Listeners(gateways []*GatewayContext) { + // Iterate through all layer-7 (HTTP, HTTPS, TLS) listeners and collect info about protocols + // and hostnames per port. + for _, gateway := range gateways { + portListenerInfo := map[v1beta1.PortNumber]*portListeners{} + for _, listener := range gateway.listeners { + if listener.Protocol == v1beta1.UDPProtocolType { + continue + } + if portListenerInfo[listener.Port] == nil { + portListenerInfo[listener.Port] = &portListeners{ + protocols: sets.Set[string]{}, + hostnames: map[string]int{}, + } + } + + portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) + + var protocol string + switch listener.Protocol { + // HTTPS and TLS can co-exist on the same port + case v1beta1.HTTPSProtocolType, v1beta1.TLSProtocolType: + protocol = "https/tls" + default: + protocol = string(listener.Protocol) + } + portListenerInfo[listener.Port].protocols.Insert(protocol) + + var hostname string + if listener.Hostname != nil { + hostname = string(*listener.Hostname) + } + + portListenerInfo[listener.Port].hostnames[hostname]++ + } + + // Set Conflicted conditions for any listeners with conflicting specs. + for _, info := range portListenerInfo { + for _, listener := range info.listeners { + if len(info.protocols) > 1 { + listener.SetCondition( + v1beta1.ListenerConditionConflicted, + metav1.ConditionTrue, + v1beta1.ListenerReasonProtocolConflict, + "All listeners for a given port must use a compatible protocol", + ) + } + + var hostname string + if listener.Hostname != nil { + hostname = string(*listener.Hostname) + } + + if info.hostnames[hostname] > 1 { + listener.SetCondition( + v1beta1.ListenerConditionConflicted, + metav1.ConditionTrue, + v1beta1.ListenerReasonHostnameConflict, + "All listeners for a given port must use a unique hostname", + ) + } + } + } + } +} + +func (t *Translator) validateConflictedLayer4Listeners(gateways []*GatewayContext, protocol v1beta1.ProtocolType) { + // Iterate through all layer-4(TCP UDP) listeners and check if there are more than one listener on the same port + for _, gateway := range gateways { + portListenerInfo := map[v1beta1.PortNumber]*portListeners{} + for _, listener := range gateway.listeners { + if listener.Protocol == protocol { + if portListenerInfo[listener.Port] == nil { + portListenerInfo[listener.Port] = &portListeners{} + } + portListenerInfo[listener.Port].listeners = append(portListenerInfo[listener.Port].listeners, listener) + } + } + + // Leave the first one and set Conflicted conditions for all other listeners with conflicting specs. + for _, info := range portListenerInfo { + if len(info.listeners) > 1 { + for i := 1; i < len(info.listeners); i++ { + info.listeners[i].SetCondition( + v1beta1.ListenerConditionConflicted, + metav1.ConditionTrue, + v1beta1.ListenerReasonProtocolConflict, + fmt.Sprintf("Only one %s listener is allowed in a given port", protocol), + ) + } + } + } + } +} + +func (t *Translator) validateCrossNamespaceRef(from crossNamespaceFrom, to crossNamespaceTo, referenceGrants []*v1alpha2.ReferenceGrant) bool { + for _, referenceGrant := range referenceGrants { + // The ReferenceGrant must be defined in the namespace of + // the "to" (the referent). + if referenceGrant.Namespace != to.namespace { + continue + } + + // Check if the ReferenceGrant has a matching "from". + var fromAllowed bool + for _, refGrantFrom := range referenceGrant.Spec.From { + if string(refGrantFrom.Namespace) == from.namespace && string(refGrantFrom.Group) == from.group && string(refGrantFrom.Kind) == from.kind { + fromAllowed = true + break + } + } + if !fromAllowed { + continue + } + + // Check if the ReferenceGrant has a matching "to". + var toAllowed bool + for _, refGrantTo := range referenceGrant.Spec.To { + if string(refGrantTo.Group) == to.group && string(refGrantTo.Kind) == to.kind && (refGrantTo.Name == nil || *refGrantTo.Name == "" || string(*refGrantTo.Name) == to.name) { + toAllowed = true + break + } + } + if !toAllowed { + continue + } + + // If we got here, both the "from" and the "to" were allowed by this + // reference grant. + return true + } + + // If we got here, no reference policy or reference grant allowed both the "from" and "to". + return false +} + +// Checks if a hostname is valid according to RFC 1123 and gateway API's requirement that it not be an IP address +func (t *Translator) validateHostname(hostname string) error { + if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { + return fmt.Errorf("hostname %q is invalid: %v", hostname, errs) + } + + // IP addresses are not allowed so parsing the hostname as an address needs to fail + if _, err := netip.ParseAddr(hostname); err == nil { + return fmt.Errorf("hostname: %q cannot be an ip address", hostname) + } + + labelIdx := 0 + for i := range hostname { + if hostname[i] == '.' { + + if i-labelIdx > 63 { + return fmt.Errorf("label: %q in hostname %q cannot exceed 63 characters", hostname[labelIdx:i], hostname) + } + labelIdx = i + 1 + } + } + // Check the last label + if len(hostname)-labelIdx > 63 { + return fmt.Errorf("label: %q in hostname %q cannot exceed 63 characters", hostname[labelIdx:], hostname) + } + + return nil +} diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 8acf6a2b75..4f578a51b1 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -209,6 +209,8 @@ type HTTPRoute struct { Redirect *Redirect // Destinations associated with this matched route. Destinations []*RouteDestination + // Rewrite to be changed for this route. + URLRewrite *URLRewrite } // Validate the fields within the HTTPRoute structure @@ -250,6 +252,11 @@ func (h HTTPRoute) Validate() error { errs = multierror.Append(errs, err) } } + if h.URLRewrite != nil { + if err := h.URLRewrite.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } if len(h.AddRequestHeaders) > 0 { occurred := map[string]bool{} for _, header := range h.AddRequestHeaders { @@ -366,6 +373,28 @@ func (r DirectResponse) Validate() error { return errs } +// Re holds the details for how to rewrite a request +// +k8s:deepcopy-gen=true +type URLRewrite struct { + // Path contains config for rewriting the path of the request. + Path *HTTPPathModifier + // Hostname configures the replacement of the request's hostname. + Hostname *string +} + +// Validate the fields within the URLRewrite structure +func (r URLRewrite) Validate() error { + var errs error + + if r.Path != nil { + if err := r.Path.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + + return errs +} + // Redirect holds the details for how and where to redirect a request // +k8s:deepcopy-gen=true type Redirect struct { diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index 16201df8bb..7e10a7779f 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -214,6 +214,33 @@ var ( }, } + urlRewriteHTTPRoute = HTTPRoute{ + Name: "rewrite", + PathMatch: &StringMatch{ + Exact: ptrTo("rewrite"), + }, + URLRewrite: &URLRewrite{ + Hostname: ptrTo("rewrite.example.com"), + Path: &HTTPPathModifier{ + FullReplace: ptrTo("/rewrite"), + }, + }, + } + + urlRewriteFilterBadPath = HTTPRoute{ + Name: "rewrite", + PathMatch: &StringMatch{ + Exact: ptrTo("rewrite"), + }, + URLRewrite: &URLRewrite{ + Hostname: ptrTo("rewrite.example.com"), + Path: &HTTPPathModifier{ + FullReplace: ptrTo("/rewrite"), + PrefixMatchReplace: ptrTo("/rewrite"), + }, + }, + } + addRequestHeaderHTTPRoute = HTTPRoute{ Name: "addheader", PathMatch: &StringMatch{ @@ -681,6 +708,16 @@ func TestValidateHTTPRoute(t *testing.T) { input: directResponseBadStatus, want: []error{ErrDirectResponseStatusInvalid}, }, + { + name: "rewrite-httproute", + input: urlRewriteHTTPRoute, + want: nil, + }, + { + name: "rewrite-bad-path", + input: urlRewriteFilterBadPath, + want: []error{ErrHTTPPathModifierDoubleReplace}, + }, { name: "add-request-headers-httproute", input: addRequestHeaderHTTPRoute, diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 868eb71875..e948d8ff84 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -182,6 +182,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { } } } + if in.URLRewrite != nil { + in, out := &in.URLRewrite, &out.URLRewrite + *out = new(URLRewrite) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute. @@ -480,6 +485,31 @@ func (in *UDPListener) DeepCopy() *UDPListener { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *URLRewrite) DeepCopyInto(out *URLRewrite) { + *out = *in + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(HTTPPathModifier) + (*in).DeepCopyInto(*out) + } + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new URLRewrite. +func (in *URLRewrite) DeepCopy() *URLRewrite { + if in == nil { + return nil + } + out := new(URLRewrite) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Xds) DeepCopyInto(out *Xds) { *out = *in diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go index a7d838419f..64354bc8ea 100644 --- a/internal/provider/kubernetes/kubernetes_test.go +++ b/internal/provider/kubernetes/kubernetes_test.go @@ -472,6 +472,8 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour redirectPort := gwapiv1b1.PortNumber(8443) redirectStatus := 301 + rewriteHostname := gwapiv1b1.PreciseHostname("rewrite.hostname.local") + var testCases = []struct { name string route gwapiv1b1.HTTPRoute @@ -571,6 +573,58 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour }, }, }, + { + name: "rewrite-httproute", + route: gwapiv1b1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httproute-rewrite-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1b1.CommonRouteSpec{ + ParentRefs: []gwapiv1b1.ParentReference{ + { + Name: gwapiv1b1.ObjectName(gw.Name), + }, + }, + }, + Hostnames: []gwapiv1b1.Hostname{"test.hostname.local"}, + Rules: []gwapiv1b1.HTTPRouteRule{ + { + Matches: []gwapiv1b1.HTTPRouteMatch{ + { + Path: &gwapiv1b1.HTTPPathMatch{ + Type: gatewayapi.PathMatchTypePtr(gwapiv1b1.PathMatchPathPrefix), + Value: gatewayapi.StringPtr("/rewrite/"), + }, + }, + }, + BackendRefs: []gwapiv1b1.HTTPBackendRef{ + { + BackendRef: gwapiv1b1.BackendRef{ + BackendObjectReference: gwapiv1b1.BackendObjectReference{ + Name: "test", + }, + }, + }, + }, + Filters: []gwapiv1b1.HTTPRouteFilter{ + { + Type: gwapiv1b1.HTTPRouteFilterType("URLRewrite"), + URLRewrite: &gwapiv1b1.HTTPURLRewriteFilter{ + Hostname: &rewriteHostname, + Path: &gwapiv1b1.HTTPPathModifier{ + Type: gwapiv1b1.HTTPPathModifierType("ReplaceFullPath"), + ReplaceFullPath: gatewayapi.StringPtr("/newpath"), + }, + }, + }, + }, + }, + }, + }, + }, + }, { name: "add-request-header-httproute", route: gwapiv1b1.HTTPRoute{ diff --git a/internal/xds/translator/route.go b/internal/xds/translator/route.go index aebcc79f21..f57daeda4b 100644 --- a/internal/xds/translator/route.go +++ b/internal/xds/translator/route.go @@ -38,6 +38,8 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) *route.Route { ret.Action = &route.Route_DirectResponse{DirectResponse: buildXdsDirectResponseAction(httpRoute.DirectResponse)} case httpRoute.Redirect != nil: ret.Action = &route.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute.Redirect)} + case httpRoute.URLRewrite != nil: + ret.Action = &route.Route_Route{Route: buildXdsURLRewriteAction(httpRoute.Name, httpRoute.URLRewrite)} default: if httpRoute.BackendWeights.Invalid != 0 { // If there are invalid backends then a weighted cluster is required for the route @@ -214,6 +216,37 @@ func buildXdsRedirectAction(redirection *ir.Redirect) *route.RedirectAction { return ret } +func buildXdsURLRewriteAction(routeName string, urlRewrite *ir.URLRewrite) *route.RouteAction { + ret := &route.RouteAction{ + ClusterSpecifier: &route.RouteAction_Cluster{ + Cluster: routeName, + }, + } + + if urlRewrite.Path != nil { + if urlRewrite.Path.FullReplace != nil { + ret.RegexRewrite = &matcher.RegexMatchAndSubstitute{ + Pattern: &matcher.RegexMatcher{ + Regex: "/.+", + }, + Substitution: *urlRewrite.Path.FullReplace, + } + } else if urlRewrite.Path.PrefixMatchReplace != nil { + ret.PrefixRewrite = *urlRewrite.Path.PrefixMatchReplace + } + } + + if urlRewrite.Hostname != nil { + ret.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{ + HostRewriteLiteral: *urlRewrite.Hostname, + } + + ret.AppendXForwardedHost = true + } + + return ret +} + func buildXdsDirectResponseAction(res *ir.DirectResponse) *route.DirectResponseAction { ret := &route.DirectResponseAction{Status: res.StatusCode} diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-fullpath.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-fullpath.yaml new file mode 100644 index 0000000000..32c9114460 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-fullpath.yaml @@ -0,0 +1,20 @@ +name: "http-route" +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "rewrite-route" + pathMatch: + prefix: "/origin" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: "1.2.3.4" + port: 50000 + urlRewrite: + path: + fullReplace: /rewrite diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml new file mode 100644 index 0000000000..c63d100436 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-host.yaml @@ -0,0 +1,21 @@ +name: "http-route" +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "rewrite-route" + pathMatch: + prefix: "/origin" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: "1.2.3.4" + port: 50000 + urlRewrite: + hostname: "3.3.3.3" + path: + prefixMatchReplace: /rewrite diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix.yaml new file mode 100644 index 0000000000..8ce2437be2 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/http-route-rewrite-url-prefix.yaml @@ -0,0 +1,20 @@ +name: "http-route" +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "rewrite-route" + pathMatch: + prefix: "/origin" + headerMatches: + - name: ":authority" + exact: gateway.envoyproxy.io + destinations: + - host: "1.2.3.4" + port: 50000 + urlRewrite: + path: + prefixMatchReplace: /rewrite diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.clusters.yaml new file mode 100644 index 0000000000..cfc7233a5a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.clusters.yaml @@ -0,0 +1,18 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: rewrite-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: rewrite-route + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.listeners.yaml new file mode 100644 index 0000000000..6c55557397 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.listeners.yaml @@ -0,0 +1,40 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml new file mode 100644 index 0000000000..c0c348cb78 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.routes.yaml @@ -0,0 +1,18 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + headers: + - name: :authority + stringMatch: + exact: gateway.envoyproxy.io + prefix: /origin + route: + cluster: rewrite-route + regexRewrite: + pattern: + regex: /.+ + substitution: /rewrite diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.clusters.yaml new file mode 100644 index 0000000000..cfc7233a5a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.clusters.yaml @@ -0,0 +1,18 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: rewrite-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: rewrite-route + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.listeners.yaml new file mode 100644 index 0000000000..6c55557397 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.listeners.yaml @@ -0,0 +1,40 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml new file mode 100644 index 0000000000..65cac050e0 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.routes.yaml @@ -0,0 +1,17 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + headers: + - name: :authority + stringMatch: + exact: gateway.envoyproxy.io + prefix: /origin + route: + appendXForwardedHost: true + cluster: rewrite-route + hostRewriteLiteral: 3.3.3.3 + prefixRewrite: /rewrite diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.clusters.yaml new file mode 100644 index 0000000000..cfc7233a5a --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.clusters.yaml @@ -0,0 +1,18 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: rewrite-route + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + locality: {} + name: rewrite-route + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.listeners.yaml new file mode 100644 index 0000000000..6c55557397 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.listeners.yaml @@ -0,0 +1,40 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + 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 + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/stdout + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + rds: + configSource: + apiConfigSource: + apiType: DELTA_GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + setNodeOnFirstMessageOnly: true + transportApiVersion: V3 + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + name: first-listener diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml new file mode 100644 index 0000000000..755b58263b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.routes.yaml @@ -0,0 +1,15 @@ +- name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener + routes: + - match: + headers: + - name: :authority + stringMatch: + exact: gateway.envoyproxy.io + prefix: /origin + route: + cluster: rewrite-route + prefixRewrite: /rewrite diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index b662283a16..a785ee4e6b 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -77,6 +77,15 @@ func TestTranslate(t *testing.T) { { name: "http2-route", }, + { + name: "http-route-rewrite-url-prefix", + }, + { + name: "http-route-rewrite-url-fullpath", + }, + { + name: "http-route-rewrite-url-host", + }, } for _, tc := range testCases { diff --git a/tools/make/kube.mk b/tools/make/kube.mk index 234740e21a..2566656755 100644 --- a/tools/make/kube.mk +++ b/tools/make/kube.mk @@ -60,7 +60,7 @@ kube-demo: ## Deploy a demo backend service, gatewayclass, gateway and httproute kubectl apply -f examples/kubernetes/quickstart.yaml $(eval ENVOY_SERVICE := $(shell kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')) @echo -e "\nPort forward to the Envoy service using the command below" - @echo -e 'kubectl -n envoy-gateway-system port-forward service/$(ENVOY_SERVICE) 8888:8080 &' + @echo -e "kubectl -n envoy-gateway-system port-forward service/$(ENVOY_SERVICE) 8888:8080 &" @echo -e "\nCurl the app through Envoy proxy using the command below" @echo -e "curl --verbose --header \"Host: www.example.com\" http://localhost:8888/get\n"