From e928309fe5eee6c0efdc16b24cbb3de780413b60 Mon Sep 17 00:00:00 2001 From: Yi Tao Date: Fri, 5 May 2023 16:23:18 +0800 Subject: [PATCH 1/3] translate HTTPRoute to expression based routes --- internal/dataplane/parser/translate_errors.go | 2 + .../dataplane/parser/translate_httproute.go | 152 +++---------- .../parser/translate_httproute_test.go | 92 -------- .../dataplane/parser/translators/httproute.go | 125 ++++++++++ .../parser/translators/httproute_atc.go | 213 ++++++++++++++++++ .../parser/translators/httproute_test.go | 103 +++++++++ .../parser/translators/ingress_atc.go | 6 +- .../parser/translators/ingress_atc_test.go | 4 +- .../parser/translators/translator_errors.go | 9 + test/integration/examples_test.go | 1 - test/integration/httproute_test.go | 5 - test/integration/httproute_webhook_test.go | 1 - 12 files changed, 486 insertions(+), 227 deletions(-) create mode 100644 internal/dataplane/parser/translators/httproute_atc.go create mode 100644 internal/dataplane/parser/translators/httproute_test.go create mode 100644 internal/dataplane/parser/translators/translator_errors.go diff --git a/internal/dataplane/parser/translate_errors.go b/internal/dataplane/parser/translate_errors.go index 00411a4322..eb0570b69e 100644 --- a/internal/dataplane/parser/translate_errors.go +++ b/internal/dataplane/parser/translate_errors.go @@ -2,6 +2,8 @@ package parser import "errors" +// REVIEW: use error variables in translator package instead? + var ( errRouteValidationNoRules = errors.New("no rules provided") errRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported") diff --git a/internal/dataplane/parser/translate_httproute.go b/internal/dataplane/parser/translate_httproute.go index 6b09ee53c4..11d11821f7 100644 --- a/internal/dataplane/parser/translate_httproute.go +++ b/internal/dataplane/parser/translate_httproute.go @@ -2,7 +2,6 @@ package parser import ( "fmt" - pathlib "path" "strings" "github.com/kong/go-kong/kong" @@ -85,7 +84,7 @@ func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *g // generate the routes for the service and attach them to the service for _, kongRouteTranslation := range kongServiceTranslation.KongRoutes { - routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.featureFlags.RegexPathPrefix) + routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.featureFlags.RegexPathPrefix, p.featureFlags.ExpressionRoutes) if err != nil { return err } @@ -134,6 +133,17 @@ func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1bet // Translate HTTPRoute - Utils // ----------------------------------------------------------------------------- +// getHTTPRouteHostnamesAsSliceOfStrings translates the hostnames defined in an +// HTTPRoute specification into a []*string slice, which is the type required by translating to matchers +// in expression based routes. +func getHTTPRouteHostnamesAsSliceOfStrings(httproute *gatewayv1beta1.HTTPRoute) []string { + hostnames := make([]string, 0, len(httproute.Spec.Hostnames)) + for _, hostname := range httproute.Spec.Hostnames { + hostnames = append(hostnames, string(hostname)) + } + return hostnames +} + // getHTTPRouteHostnamesAsSliceOfStringPointers translates the hostnames defined // in an HTTPRoute specification into a []*string slice, which is the type required // by kong.Route{}. @@ -221,11 +231,26 @@ func generateKongRouteFromTranslation( httproute *gatewayv1beta1.HTTPRoute, translation translators.KongRouteTranslation, addRegexPrefix bool, + expressionRoutes bool, ) ([]kongstate.Route, error) { // gather the k8s object information and hostnames from the httproute objectInfo := util.FromK8sObject(httproute) tags := util.GenerateTagsForObject(httproute) + // translate to expression based routes when expressionRoutes is enabled. + if expressionRoutes { + // get the hostnames from the HTTPRoute + hostnames := getHTTPRouteHostnamesAsSliceOfStrings(httproute) + return translators.GenerateKongExpressionRoutesFromHTTPRouteMatches( + translation.Name, + translation.Matches, + translation.Filters, + objectInfo, + hostnames, + tags, + ) + } + // get the hostnames from the HTTPRoute hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) @@ -306,7 +331,7 @@ func generateKongRoutesFromHTTPRouteMatches( // if the redirect filter has not been set, we still need to set the route plugins if !hasRedirectFilter { - plugins := generatePluginsFromHTTPRouteFilters(filters, "", tags) + plugins := translators.GeneratePluginsFromHTTPRouteFilters(filters, "", tags) r.Plugins = append(r.Plugins, plugins...) routes = []kongstate.Route{r} } @@ -356,7 +381,7 @@ func getRoutesFromMatches(matches []gatewayv1beta1.HTTPRouteMatch, } // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteFilters(filters, path, tags) + plugins := translators.GeneratePluginsFromHTTPRouteFilters(filters, path, tags) matchRoute.Plugins = append(matchRoute.Plugins, plugins...) routes = append(routes, *route) @@ -435,125 +460,6 @@ func generateKongstateHTTPRoute(routeName string, ingressObjectInfo util.K8sObje return r } -// generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong plugins. -// path is the parameter to be used by the redirect plugin, to perform redirection. -func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter, path string, tags []*string) []kong.Plugin { - kongPlugins := make([]kong.Plugin, 0) - if len(filters) == 0 { - return kongPlugins - } - - for _, filter := range filters { - switch filter.Type { - case gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier: - kongPlugins = append(kongPlugins, generateRequestHeaderModifierKongPlugin(filter.RequestHeaderModifier)) - - case gatewayv1beta1.HTTPRouteFilterRequestRedirect: - kongPlugins = append(kongPlugins, generateRequestRedirectKongPlugin(filter.RequestRedirect, path)...) - - case gatewayv1beta1.HTTPRouteFilterExtensionRef, - gatewayv1beta1.HTTPRouteFilterRequestMirror, - gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier, - gatewayv1beta1.HTTPRouteFilterURLRewrite: - // not supported - } - } - for _, p := range kongPlugins { - // This plugin is derived from an HTTPRoute filter, not a KongPlugin, so we apply tags indicating that - // HTTPRoute as the parent Kubernetes resource for these generated plugins. - p.Tags = tags - } - - return kongPlugins -} - -func generateRequestRedirectKongPlugin(modifier *gatewayv1beta1.HTTPRequestRedirectFilter, path string) []kong.Plugin { - plugins := make([]kong.Plugin, 2) - plugins[0] = kong.Plugin{ - Name: kong.String("request-termination"), - Config: kong.Configuration{ - "status_code": modifier.StatusCode, - }, - } - - var locationHeader string - scheme := "http" - port := 80 - - if modifier.Scheme != nil { - scheme = *modifier.Scheme - } - if modifier.Port != nil { - port = int(*modifier.Port) - } - if modifier.Path != nil && modifier.Path.Type == gatewayv1beta1.FullPathHTTPPathModifier && modifier.Path.ReplaceFullPath != nil { - // only ReplaceFullPath currently supported - path = *modifier.Path.ReplaceFullPath - } - if modifier.Hostname != nil { - locationHeader = fmt.Sprintf("Location: %s://%s", scheme, pathlib.Join(fmt.Sprintf("%s:%d", *modifier.Hostname, port), path)) - } else { - locationHeader = fmt.Sprintf("Location: %s", path) - } - - plugins[1] = kong.Plugin{ - Name: kong.String("response-transformer"), - Config: kong.Configuration{ - "add": map[string][]string{ - "headers": {locationHeader}, - }, - }, - } - - return plugins -} - -// generateRequestHeaderModifierKongPlugin converts a gatewayv1beta1.HTTPRequestHeaderFilter into a -// kong.Plugin of type request-transformer. -func generateRequestHeaderModifierKongPlugin(modifier *gatewayv1beta1.HTTPHeaderFilter) kong.Plugin { - plugin := kong.Plugin{ - Name: kong.String("request-transformer"), - Config: make(kong.Configuration), - } - - // modifier.Set is converted to a pair composed of "replace" and "add" - if modifier.Set != nil { - setModifiers := make([]string, 0, len(modifier.Set)) - for _, s := range modifier.Set { - setModifiers = append(setModifiers, kongHeaderFormatter(s)) - } - plugin.Config["replace"] = map[string][]string{ - "headers": setModifiers, - } - plugin.Config["add"] = map[string][]string{ - "headers": setModifiers, - } - } - - // modifier.Add is converted to "append" - if modifier.Add != nil { - appendModifiers := make([]string, 0, len(modifier.Add)) - for _, a := range modifier.Add { - appendModifiers = append(appendModifiers, kongHeaderFormatter(a)) - } - plugin.Config["append"] = map[string][]string{ - "headers": appendModifiers, - } - } - - if modifier.Remove != nil { - plugin.Config["remove"] = map[string][]string{ - "headers": modifier.Remove, - } - } - - return plugin -} - -func kongHeaderFormatter(header gatewayv1beta1.HTTPHeader) string { - return fmt.Sprintf("%s:%s", header.Name, header.Value) -} - func httpBackendRefsToBackendRefs(httpBackendRef []gatewayv1beta1.HTTPBackendRef) []gatewayv1beta1.BackendRef { backendRefs := make([]gatewayv1beta1.BackendRef, 0, len(httpBackendRef)) diff --git a/internal/dataplane/parser/translate_httproute_test.go b/internal/dataplane/parser/translate_httproute_test.go index 778aed2a66..589c786d98 100644 --- a/internal/dataplane/parser/translate_httproute_test.go +++ b/internal/dataplane/parser/translate_httproute_test.go @@ -1352,98 +1352,6 @@ func TestGetHTTPRouteHostnamesAsSliceOfStringPointers(t *testing.T) { } } -func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) { - testCases := []struct { - name string - filters []gatewayv1beta1.HTTPRouteFilter - path string - expectedPlugins []kong.Plugin - }{ - { - name: "no filters", - filters: []gatewayv1beta1.HTTPRouteFilter{}, - expectedPlugins: []kong.Plugin{}, - }, - { - name: "request header modifier", - filters: []gatewayv1beta1.HTTPRouteFilter{ - { - Type: gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier, - RequestHeaderModifier: &gatewayv1beta1.HTTPHeaderFilter{ - Set: []gatewayv1beta1.HTTPHeader{ - { - Name: "header-to-set", - Value: "bar", - }, - }, - Remove: []string{"header-to-remove"}, - }, - }, - }, - expectedPlugins: []kong.Plugin{ - { - Name: kong.String("request-transformer"), - Config: kong.Configuration{ - "add": map[string][]string{ - "headers": { - "header-to-set:bar", - }, - }, - "remove": map[string][]string{ - "headers": { - "header-to-remove", - }, - }, - "replace": map[string][]string{ - "headers": { - "header-to-set:bar", - }, - }, - }, - }, - }, - }, - { - name: "request redirect modifier", - filters: []gatewayv1beta1.HTTPRouteFilter{ - { - Type: gatewayv1beta1.HTTPRouteFilterRequestRedirect, - RequestRedirect: &gatewayv1beta1.HTTPRequestRedirectFilter{ - Hostname: (*gatewayv1beta1.PreciseHostname)(lo.ToPtr("example.org")), - StatusCode: lo.ToPtr(302), - }, - }, - }, - path: "/test", - expectedPlugins: []kong.Plugin{ - { - Name: kong.String("request-termination"), - Config: kong.Configuration{ - "status_code": lo.ToPtr(302), - }, - }, - { - Name: kong.String("response-transformer"), - Config: kong.Configuration{ - "add": map[string][]string{ - "headers": { - "Location: http://example.org:80/test", - }, - }, - }, - }, - }, - }, - } - - for _, tc := range testCases { - plugins := generatePluginsFromHTTPRouteFilters(tc.filters, tc.path, nil) - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expectedPlugins, plugins) - }) - } -} - func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) { fakestore, err := store.NewFakeStore(store.FakeObjects{}) require.NoError(t, err) diff --git a/internal/dataplane/parser/translators/httproute.go b/internal/dataplane/parser/translators/httproute.go index b2b51eb470..c64179c4f0 100644 --- a/internal/dataplane/parser/translators/httproute.go +++ b/internal/dataplane/parser/translators/httproute.go @@ -3,9 +3,11 @@ package translators import ( "encoding/json" "fmt" + pathlib "path" "sort" "strings" + "github.com/kong/go-kong/kong" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -346,3 +348,126 @@ func mustMarshalJSON[T any](val T) string { } return string(key) } + +// GeneratePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong plugins. +// path is the parameter to be used by the redirect plugin, to perform redirection. +func GeneratePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter, path string, tags []*string) []kong.Plugin { + kongPlugins := make([]kong.Plugin, 0) + if len(filters) == 0 { + return kongPlugins + } + + for _, filter := range filters { + switch filter.Type { + case gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier: + kongPlugins = append(kongPlugins, generateRequestHeaderModifierKongPlugin(filter.RequestHeaderModifier)) + + case gatewayv1beta1.HTTPRouteFilterRequestRedirect: + kongPlugins = append(kongPlugins, generateRequestRedirectKongPlugin(filter.RequestRedirect, path)...) + + case gatewayv1beta1.HTTPRouteFilterExtensionRef, + gatewayv1beta1.HTTPRouteFilterRequestMirror, + gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier, + gatewayv1beta1.HTTPRouteFilterURLRewrite: + // not supported + } + } + for _, p := range kongPlugins { + // This plugin is derived from an HTTPRoute filter, not a KongPlugin, so we apply tags indicating that + // HTTPRoute as the parent Kubernetes resource for these generated plugins. + p.Tags = tags + } + + return kongPlugins +} + +// generateRequestRedirectKongPlugin generates configurations of plugins to satisfy the specification +// of request redirect filter. +func generateRequestRedirectKongPlugin(modifier *gatewayv1beta1.HTTPRequestRedirectFilter, path string) []kong.Plugin { + plugins := make([]kong.Plugin, 2) + plugins[0] = kong.Plugin{ + Name: kong.String("request-termination"), + Config: kong.Configuration{ + "status_code": modifier.StatusCode, + }, + } + + var locationHeader string + scheme := "http" + port := 80 + + if modifier.Scheme != nil { + scheme = *modifier.Scheme + } + if modifier.Port != nil { + port = int(*modifier.Port) + } + if modifier.Path != nil && + modifier.Path.Type == gatewayv1beta1.FullPathHTTPPathModifier && + modifier.Path.ReplaceFullPath != nil { + // only ReplaceFullPath currently supported + path = *modifier.Path.ReplaceFullPath + } + if modifier.Hostname != nil { + locationHeader = fmt.Sprintf("Location: %s://%s", scheme, pathlib.Join(fmt.Sprintf("%s:%d", *modifier.Hostname, port), path)) + } else { + locationHeader = fmt.Sprintf("Location: %s", path) + } + + plugins[1] = kong.Plugin{ + Name: kong.String("response-transformer"), + Config: kong.Configuration{ + "add": map[string][]string{ + "headers": {locationHeader}, + }, + }, + } + + return plugins +} + +// generateRequestHeaderModifierKongPlugin converts a gatewayv1beta1.HTTPRequestHeaderFilter into a +// kong.Plugin of type request-transformer. +func generateRequestHeaderModifierKongPlugin(modifier *gatewayv1beta1.HTTPHeaderFilter) kong.Plugin { + plugin := kong.Plugin{ + Name: kong.String("request-transformer"), + Config: make(kong.Configuration), + } + + // modifier.Set is converted to a pair composed of "replace" and "add" + if modifier.Set != nil { + setModifiers := make([]string, 0, len(modifier.Set)) + for _, s := range modifier.Set { + setModifiers = append(setModifiers, kongHeaderFormatter(s)) + } + plugin.Config["replace"] = map[string][]string{ + "headers": setModifiers, + } + plugin.Config["add"] = map[string][]string{ + "headers": setModifiers, + } + } + + // modifier.Add is converted to "append" + if modifier.Add != nil { + appendModifiers := make([]string, 0, len(modifier.Add)) + for _, a := range modifier.Add { + appendModifiers = append(appendModifiers, kongHeaderFormatter(a)) + } + plugin.Config["append"] = map[string][]string{ + "headers": appendModifiers, + } + } + + if modifier.Remove != nil { + plugin.Config["remove"] = map[string][]string{ + "headers": modifier.Remove, + } + } + + return plugin +} + +func kongHeaderFormatter(header gatewayv1beta1.HTTPHeader) string { + return fmt.Sprintf("%s:%s", header.Name, header.Value) +} diff --git a/internal/dataplane/parser/translators/httproute_atc.go b/internal/dataplane/parser/translators/httproute_atc.go new file mode 100644 index 0000000000..59b0aa954d --- /dev/null +++ b/internal/dataplane/parser/translators/httproute_atc.go @@ -0,0 +1,213 @@ +package translators + +import ( + "sort" + "strings" + + "github.com/kong/go-kong/kong" + "github.com/samber/lo" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/atc" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" +) + +// GenerateKongExpressionRoutesFromHTTPRouteMatches generates Kong routes from HTTPRouteRule +// pointing to a specific backend. +func GenerateKongExpressionRoutesFromHTTPRouteMatches( + routeName string, + matches []gatewayv1beta1.HTTPRouteMatch, + filters []gatewayv1beta1.HTTPRouteFilter, + ingressObjectInfo util.K8sObjectInfo, + hostnames []string, + tags []*string, +) ([]kongstate.Route, error) { + // initialize the route with route name, preserve_host, and tags. + r := kongstate.Route{ + Ingress: ingressObjectInfo, + Route: kong.Route{ + Name: kong.String(routeName), + PreserveHost: kong.Bool(true), + Tags: tags, + }, + ExpressionRoutes: true, + } + + if len(matches) == 0 { + if len(hostnames) == 0 { + return []kongstate.Route{}, ErrRouteValidationNoMatchRulesOrHostnamesSpecified + } + + hostMatcher := hostMatcherFromHosts(hostnames) + atc.ApplyExpression(&r.Route, hostMatcher, 1) + return []kongstate.Route{r}, nil + } + + _, hasRedirectFilter := lo.Find(filters, func(filter gatewayv1beta1.HTTPRouteFilter) bool { + return filter.Type == gatewayv1beta1.HTTPRouteFilterRequestRedirect + }) + // if the rule has request redirect filter(s), we need to generate a route for each match to + // attach plugins for the filter. + if hasRedirectFilter { + routes := make([]kongstate.Route, 0, len(matches)) + for _, match := range matches { + matchRoute := kongstate.Route{ + Ingress: ingressObjectInfo, + Route: kong.Route{ + Name: kong.String(routeName), + PreserveHost: kong.Bool(true), + Tags: tags, + }, + ExpressionRoutes: true, + } + // generate matcher for this HTTPRoute Match. + matcher := atc.And(generateMatcherFromHTTPRouteMatch(match)) + + // add matcher from parent httproute (hostnames, protocols, SNIs) to be ANDed with the matcher from match. + matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) + for _, m := range matchersFromParent { + matcher.And(m) + } + atc.ApplyExpression(&matchRoute.Route, matcher, 1) + + // we need to extract the path to configure redirect path of the plugins for request redirect filter. + path := "" + if match.Path != nil && match.Path.Value != nil { + path = *match.Path.Value + } + plugins := GeneratePluginsFromHTTPRouteFilters(filters, path, tags) + matchRoute.Plugins = plugins + + routes = append(routes, matchRoute) + } + return routes, nil + } + + routeMatcher := atc.And(atc.Or(generateMatchersFromHTTPRouteMatches(matches)...)) + matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) + for _, matcher := range matchersFromParent { + routeMatcher.And(matcher) + } + + atc.ApplyExpression(&r.Route, routeMatcher, 1) + plugins := GeneratePluginsFromHTTPRouteFilters(filters, "", tags) + r.Plugins = plugins + return []kongstate.Route{r}, nil +} + +func generateMatchersFromHTTPRouteMatches(matches []gatewayv1beta1.HTTPRouteMatch) []atc.Matcher { + ret := make([]atc.Matcher, 0, len(matches)) + for _, match := range matches { + matcher := generateMatcherFromHTTPRouteMatch(match) + ret = append(ret, matcher) + } + return ret +} + +func generateMatcherFromHTTPRouteMatch(match gatewayv1beta1.HTTPRouteMatch) atc.Matcher { + var matcher atc.Matcher + + if match.Path != nil { + pathMatcher := pathMatcherFromHTTPPathMatch(match.Path) + matcher = atc.And(matcher, pathMatcher) + } + + if len(match.Headers) > 0 { + headerMatcher := headerMatcherFromHTTPHeaderMatches(match.Headers) + matcher = atc.And(matcher, headerMatcher) + } + + if match.Method != nil { + method := *match.Method + methodMatcher := methodMatcherFromMethods([]string{string(method)}) + matcher = atc.And(matcher, methodMatcher) + } + return matcher +} + +func appendSuffixSlashIfNotExist(path string) string { + if !strings.HasSuffix(path, "/") { + return path + "/" + } + return path +} + +func appendRegexBeginIfNotExist(regex string) string { + if !strings.HasPrefix(regex, "^") { + return "^" + regex + } + return regex +} + +func pathMatcherFromHTTPPathMatch(pathMatch *gatewayv1beta1.HTTPPathMatch) atc.Matcher { + path := "" + if pathMatch.Value != nil { + path = *pathMatch.Value + } + switch *pathMatch.Type { + case gatewayv1beta1.PathMatchExact: + return atc.NewPredicateHTTPPath(atc.OpEqual, path) + case gatewayv1beta1.PathMatchPathPrefix: + return atc.Or( + atc.NewPredicateHTTPPath(atc.OpEqual, path), + atc.NewPredicateHTTPPath(atc.OpPrefixMatch, appendSuffixSlashIfNotExist(path)), + ) + case gatewayv1beta1.PathMatchRegularExpression: + return atc.NewPredicateHTTPPath(atc.OpRegexMatch, appendRegexBeginIfNotExist(path)) + } + + return nil // should be unreachable +} + +func headerMatcherFromHTTPHeaderMatches(headerMatches []gatewayv1beta1.HTTPHeaderMatch) atc.Matcher { + // sort headerMatches by names to generate a stable output. + sort.Slice(headerMatches, func(i, j int) bool { + return string(headerMatches[i].Name) < string(headerMatches[j].Name) + }) + + matchers := make([]atc.Matcher, 0, len(headerMatches)) + for _, headerMatch := range headerMatches { + matchType := gatewayv1beta1.HeaderMatchExact + if headerMatch.Type != nil { + matchType = *headerMatch.Type + } + headerKey := strings.ReplaceAll(strings.ToLower(string(headerMatch.Name)), "-", "_") + switch matchType { + case gatewayv1beta1.HeaderMatchExact: + matchers = append(matchers, atc.NewPredicateHTTPHeader(headerKey, atc.OpEqual, headerMatch.Value)) + case gatewayv1beta1.HeaderMatchRegularExpression: + matchers = append(matchers, atc.NewPredicateHTTPHeader(headerKey, atc.OpRegexMatch, headerMatch.Value)) + } + } + return atc.And(matchers...) +} + +func matchersFromParentHTTPRoute(hostnames []string, metaAnnotations map[string]string) []atc.Matcher { + + // translate hostnames. + ret := []atc.Matcher{} + if len(hostnames) > 0 { + hostMatcher := hostMatcherFromHosts(hostnames) + ret = append(ret, hostMatcher) + } + + // translate protocols. + protocols := []string{"http", "https"} + // override from "protocols" key in annotations. + annonationProtocols := annotations.ExtractProtocolNames(metaAnnotations) + if len(annonationProtocols) > 0 { + protocols = annonationProtocols + } + protocolMatcher := protocolMatcherFromProtocols(protocols) + ret = append(ret, protocolMatcher) + + // translate SNIs. + snis, exist := annotations.ExtractSNIs(metaAnnotations) + if exist && len(snis) > 0 { + sniMatcher := sniMatcherFromSNIs(snis) + ret = append(ret, sniMatcher) + } + return ret +} diff --git a/internal/dataplane/parser/translators/httproute_test.go b/internal/dataplane/parser/translators/httproute_test.go new file mode 100644 index 0000000000..e328ceb040 --- /dev/null +++ b/internal/dataplane/parser/translators/httproute_test.go @@ -0,0 +1,103 @@ +package translators + +import ( + "testing" + + "github.com/kong/go-kong/kong" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func TestGeneratePluginsFromHTTPRouteFilters(t *testing.T) { + testCases := []struct { + name string + filters []gatewayv1beta1.HTTPRouteFilter + path string + expectedPlugins []kong.Plugin + }{ + { + name: "no filters", + filters: []gatewayv1beta1.HTTPRouteFilter{}, + expectedPlugins: []kong.Plugin{}, + }, + { + name: "request header modifier", + filters: []gatewayv1beta1.HTTPRouteFilter{ + { + Type: gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1beta1.HTTPHeaderFilter{ + Set: []gatewayv1beta1.HTTPHeader{ + { + Name: "header-to-set", + Value: "bar", + }, + }, + Remove: []string{"header-to-remove"}, + }, + }, + }, + expectedPlugins: []kong.Plugin{ + { + Name: kong.String("request-transformer"), + Config: kong.Configuration{ + "add": map[string][]string{ + "headers": { + "header-to-set:bar", + }, + }, + "remove": map[string][]string{ + "headers": { + "header-to-remove", + }, + }, + "replace": map[string][]string{ + "headers": { + "header-to-set:bar", + }, + }, + }, + }, + }, + }, + { + name: "request redirect modifier", + filters: []gatewayv1beta1.HTTPRouteFilter{ + { + Type: gatewayv1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1beta1.HTTPRequestRedirectFilter{ + Hostname: (*gatewayv1beta1.PreciseHostname)(lo.ToPtr("example.org")), + StatusCode: lo.ToPtr(302), + }, + }, + }, + path: "/test", + expectedPlugins: []kong.Plugin{ + { + Name: kong.String("request-termination"), + Config: kong.Configuration{ + "status_code": lo.ToPtr(302), + }, + }, + { + Name: kong.String("response-transformer"), + Config: kong.Configuration{ + "add": map[string][]string{ + "headers": { + "Location: http://example.org:80/test", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + plugins := GeneratePluginsFromHTTPRouteFilters(tc.filters, tc.path, nil) + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectedPlugins, plugins) + }) + } +} diff --git a/internal/dataplane/parser/translators/ingress_atc.go b/internal/dataplane/parser/translators/ingress_atc.go index d9dedb12c9..c8ee6c261d 100644 --- a/internal/dataplane/parser/translators/ingress_atc.go +++ b/internal/dataplane/parser/translators/ingress_atc.go @@ -67,7 +67,7 @@ func (m *ingressTranslationMeta) translateIntoKongExpressionRoutes() *kongstate. hostAliases, _ := annotations.ExtractHostAliases(ingressAnnotations) hosts = append(hosts, hostAliases...) if len(hosts) > 0 { - hostMatcher := hostMatcherFromIngressHosts(hosts) + hostMatcher := hostMatcherFromHosts(hosts) routeMatcher.And(hostMatcher) } @@ -116,8 +116,8 @@ func (m *ingressTranslationMeta) translateIntoKongExpressionRoutes() *kongstate. return route } -// hostMatcherFromIngressHosts translates hosts in IngressHost format to ATC matcher that matches any of them. -func hostMatcherFromIngressHosts(hosts []string) atc.Matcher { +// hostMatcherFromHosts translates hosts in IngressHost format to ATC matcher that matches any of them. +func hostMatcherFromHosts(hosts []string) atc.Matcher { matchers := make([]atc.Matcher, 0, len(hosts)) for _, host := range hosts { if !validHosts.MatchString(host) { diff --git a/internal/dataplane/parser/translators/ingress_atc_test.go b/internal/dataplane/parser/translators/ingress_atc_test.go index 73227f518c..5031857c43 100644 --- a/internal/dataplane/parser/translators/ingress_atc_test.go +++ b/internal/dataplane/parser/translators/ingress_atc_test.go @@ -321,7 +321,7 @@ func TestHeaderMatcherFromHeaders(t *testing.T) { } } -func TestHostMatcherFromIngressHosts(t *testing.T) { +func TestHostMatcherFromHosts(t *testing.T) { testCases := []struct { name string hosts []string @@ -352,7 +352,7 @@ func TestHostMatcherFromIngressHosts(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - matcher := hostMatcherFromIngressHosts(tc.hosts) + matcher := hostMatcherFromHosts(tc.hosts) require.Equal(t, tc.expression, matcher.Expression()) }) } diff --git a/internal/dataplane/parser/translators/translator_errors.go b/internal/dataplane/parser/translators/translator_errors.go new file mode 100644 index 0000000000..f44fc16168 --- /dev/null +++ b/internal/dataplane/parser/translators/translator_errors.go @@ -0,0 +1,9 @@ +package translators + +import "errors" + +var ( + ErrRouteValidationNoRules = errors.New("no rules provided") + ErrRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported") + ErrRouteValidationNoMatchRulesOrHostnamesSpecified = errors.New("no match rules or hostnames specified") +) diff --git a/test/integration/examples_test.go b/test/integration/examples_test.go index cf819f55f6..3b97f4c787 100644 --- a/test/integration/examples_test.go +++ b/test/integration/examples_test.go @@ -32,7 +32,6 @@ import ( const examplesDIR = "../../examples" func TestHTTPRouteExample(t *testing.T) { - skipTestForExpressionRouter(t) var ( httprouteExampleManifests = fmt.Sprintf("%s/gateway-httproute.yaml", examplesDIR) ctx = context.Background() diff --git a/test/integration/httproute_test.go b/test/integration/httproute_test.go index 7e94f83253..7437ae3a4e 100644 --- a/test/integration/httproute_test.go +++ b/test/integration/httproute_test.go @@ -35,9 +35,6 @@ import ( var emptyHeaderSet = make(map[string]string) func TestHTTPRouteEssentials(t *testing.T) { - // TODO: implement translator for HTTPRoutes: - // https://github.com/Kong/kubernetes-ingress-controller/issues/3751 - skipTestForExpressionRouter(t) ctx := context.Background() ns, cleaner := helpers.Setup(ctx, t, env) @@ -311,7 +308,6 @@ func TestHTTPRouteEssentials(t *testing.T) { } func TestHTTPRouteMultipleServices(t *testing.T) { - skipTestForExpressionRouter(t) ctx := context.Background() ns, cleaner := helpers.Setup(ctx, t, env) @@ -499,7 +495,6 @@ func TestHTTPRouteMultipleServices(t *testing.T) { } func TestHTTPRouteFilterHosts(t *testing.T) { - skipTestForExpressionRouter(t) ctx := context.Background() ns, cleaner := helpers.Setup(ctx, t, env) diff --git a/test/integration/httproute_webhook_test.go b/test/integration/httproute_webhook_test.go index c48941b33c..5b3c451c97 100644 --- a/test/integration/httproute_webhook_test.go +++ b/test/integration/httproute_webhook_test.go @@ -20,7 +20,6 @@ import ( ) func TestHTTPRouteValidationWebhook(t *testing.T) { - skipTestForExpressionRouter(t) ctx := context.Background() ns, cleaner := helpers.Setup(ctx, t, env) From e23b288d827b61b41ccc7bf70e4f2b4713d4a178 Mon Sep 17 00:00:00 2001 From: Yi Tao Date: Mon, 8 May 2023 15:29:28 +0800 Subject: [PATCH 2/3] add unit tests for translating HTTPRoutes to expression routes --- .../parser/translators/httproute_atc.go | 29 +- .../parser/translators/httproute_atc_test.go | 306 ++++++++++++++++++ internal/util/builder/httproutefilter.go | 95 ++++++ internal/util/builder/httproutematch.go | 20 +- 4 files changed, 430 insertions(+), 20 deletions(-) create mode 100644 internal/dataplane/parser/translators/httproute_atc_test.go create mode 100644 internal/util/builder/httproutefilter.go diff --git a/internal/dataplane/parser/translators/httproute_atc.go b/internal/dataplane/parser/translators/httproute_atc.go index 59b0aa954d..ab858bf8b4 100644 --- a/internal/dataplane/parser/translators/httproute_atc.go +++ b/internal/dataplane/parser/translators/httproute_atc.go @@ -85,13 +85,16 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches( return routes, nil } + // if we do not need to generate a kong route for each match, we OR matchers from all matches together. routeMatcher := atc.And(atc.Or(generateMatchersFromHTTPRouteMatches(matches)...)) + // add matcher from parent httproute (hostnames, protocols, SNIs) to be ANDed with the matcher from match. matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) for _, matcher := range matchersFromParent { routeMatcher.And(matcher) } atc.ApplyExpression(&r.Route, routeMatcher, 1) + // generate plugins. plugins := GeneratePluginsFromHTTPRouteFilters(filters, "", tags) r.Plugins = plugins return []kongstate.Route{r}, nil @@ -107,33 +110,26 @@ func generateMatchersFromHTTPRouteMatches(matches []gatewayv1beta1.HTTPRouteMatc } func generateMatcherFromHTTPRouteMatch(match gatewayv1beta1.HTTPRouteMatch) atc.Matcher { - var matcher atc.Matcher + matcher := atc.And() if match.Path != nil { pathMatcher := pathMatcherFromHTTPPathMatch(match.Path) - matcher = atc.And(matcher, pathMatcher) + matcher.And(pathMatcher) } if len(match.Headers) > 0 { headerMatcher := headerMatcherFromHTTPHeaderMatches(match.Headers) - matcher = atc.And(matcher, headerMatcher) + matcher.And(headerMatcher) } if match.Method != nil { method := *match.Method methodMatcher := methodMatcherFromMethods([]string{string(method)}) - matcher = atc.And(matcher, methodMatcher) + matcher.And(methodMatcher) } return matcher } -func appendSuffixSlashIfNotExist(path string) string { - if !strings.HasSuffix(path, "/") { - return path + "/" - } - return path -} - func appendRegexBeginIfNotExist(regex string) string { if !strings.HasPrefix(regex, "^") { return "^" + regex @@ -150,11 +146,19 @@ func pathMatcherFromHTTPPathMatch(pathMatch *gatewayv1beta1.HTTPPathMatch) atc.M case gatewayv1beta1.PathMatchExact: return atc.NewPredicateHTTPPath(atc.OpEqual, path) case gatewayv1beta1.PathMatchPathPrefix: + if path == "" || path == "/" { + return atc.NewPredicateHTTPPath(atc.OpPrefixMatch, "/") + } + // if path ends with /, we should remove the trailing / because it should be ignored: + // https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.PathMatchType + path = strings.TrimSuffix(path, "/") return atc.Or( atc.NewPredicateHTTPPath(atc.OpEqual, path), - atc.NewPredicateHTTPPath(atc.OpPrefixMatch, appendSuffixSlashIfNotExist(path)), + atc.NewPredicateHTTPPath(atc.OpPrefixMatch, path+"/"), ) case gatewayv1beta1.PathMatchRegularExpression: + // TODO: for compatibility with kong traditional routes, here we append the ^ prefix to match the path from beginning. + // Could we allow the regex to match any part of path? return atc.NewPredicateHTTPPath(atc.OpRegexMatch, appendRegexBeginIfNotExist(path)) } @@ -185,7 +189,6 @@ func headerMatcherFromHTTPHeaderMatches(headerMatches []gatewayv1beta1.HTTPHeade } func matchersFromParentHTTPRoute(hostnames []string, metaAnnotations map[string]string) []atc.Matcher { - // translate hostnames. ret := []atc.Matcher{} if len(hostnames) > 0 { diff --git a/internal/dataplane/parser/translators/httproute_atc_test.go b/internal/dataplane/parser/translators/httproute_atc_test.go new file mode 100644 index 0000000000..b52c07ce8e --- /dev/null +++ b/internal/dataplane/parser/translators/httproute_atc_test.go @@ -0,0 +1,306 @@ +package translators + +import ( + "testing" + + "github.com/kong/go-kong/kong" + "github.com/stretchr/testify/require" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util/builder" +) + +func TestGenerateKongExpressionRoutesFromHTTPRouteMatches(t *testing.T) { + testCases := []struct { + name string + routeName string + matches []gatewayv1beta1.HTTPRouteMatch + filters []gatewayv1beta1.HTTPRouteFilter + ingressObjectInfo util.K8sObjectInfo + hostnames []string + tags []string + expectedRoutes []kongstate.Route + expectedError error + }{ + { + name: "no hostnames and no matches", + routeName: "empty_route.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + expectedRoutes: []kongstate.Route{}, + expectedError: ErrRouteValidationNoMatchRulesOrHostnamesSpecified, + }, + { + name: "no matches but have hostnames", + routeName: "host_only.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + hostnames: []string{"foo.com", "*.bar.com"}, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("host_only.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`(http.host == "foo.com") || (http.host =^ ".bar.com")`), + Priority: kong.Int(1), + }, + ExpressionRoutes: true, + }, + }, + }, + { + name: "single prefix path match", + routeName: "prefix_path_match.defualt.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + matches: []gatewayv1beta1.HTTPRouteMatch{ + builder.NewHTTPRouteMatch().WithPathPrefix("/prefix").Build(), + }, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("prefix_path_match.defualt.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`((http.path == "/prefix") || (http.path ^= "/prefix/")) && ((net.protocol == "http") || (net.protocol == "https"))`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{}, + ExpressionRoutes: true, + }, + }, + }, + { + name: "multiple matches without filters", + routeName: "multiple_matches.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + matches: []gatewayv1beta1.HTTPRouteMatch{ + builder.NewHTTPRouteMatch().WithPathPrefix("/prefix").Build(), + builder.NewHTTPRouteMatch().WithPathExact("/exact").WithMethod(gatewayv1beta1.HTTPMethodGet).Build(), + }, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("multiple_matches.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`(((http.path == "/prefix") || (http.path ^= "/prefix/")) || ((http.path == "/exact") && (http.method == "GET"))) && ((net.protocol == "http") || (net.protocol == "https"))`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{}, + ExpressionRoutes: true, + }, + }, + }, + { + name: "multiple matches with request redirect filter", + routeName: "request_redirect.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + matches: []gatewayv1beta1.HTTPRouteMatch{ + builder.NewHTTPRouteMatch().WithPathExact("/exact/0").Build(), + builder.NewHTTPRouteMatch().WithPathExact("/exact/1").Build(), + }, + filters: []gatewayv1beta1.HTTPRouteFilter{ + builder.NewHTTPRouteRequestRedirectFilter(). + WithRequestRedirectScheme("http"). + WithRequestRedirectHost("a.foo.com"). + WithRequestRedirectStatusCode(301). + Build(), + }, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("request_redirect.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`(http.path == "/exact/0") && ((net.protocol == "http") || (net.protocol == "https"))`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{ + { + Name: kong.String("request-termination"), + Config: kong.Configuration{ + "status_code": kong.Int(301), + }, + }, + { + Name: kong.String("response-transformer"), + Config: kong.Configuration{ + "add": map[string][]string{ + "headers": {`Location: http://a.foo.com:80/exact/0`}, + }, + }, + }, + }, + ExpressionRoutes: true, + }, + { + Route: kong.Route{ + Name: kong.String("request_redirect.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`(http.path == "/exact/1") && ((net.protocol == "http") || (net.protocol == "https"))`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{ + { + Name: kong.String("request-termination"), + Config: kong.Configuration{ + "status_code": kong.Int(301), + }, + }, + { + Name: kong.String("response-transformer"), + Config: kong.Configuration{ + "add": map[string][]string{ + "headers": {`Location: http://a.foo.com:80/exact/1`}, + }, + }, + }, + }, + ExpressionRoutes: true, + }, + }, + }, + { + name: "multiple matches with request header transformer filter", + routeName: "request_header_mod.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{}, + matches: []gatewayv1beta1.HTTPRouteMatch{ + builder.NewHTTPRouteMatch().WithPathExact("/exact/0").Build(), + builder.NewHTTPRouteMatch().WithPathRegex("/regex/[a-z]+").Build(), + }, + filters: []gatewayv1beta1.HTTPRouteFilter{ + builder.NewHTTPRouteRequestHeaderModifierFilter().WithRequestHeaderAdd([]gatewayv1beta1.HTTPHeader{ + {Name: "foo", Value: "bar"}, + }).Build(), + }, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("request_header_mod.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`((http.path == "/exact/0") || (http.path ~ "^/regex/[a-z]+")) && ((net.protocol == "http") || (net.protocol == "https"))`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{ + { + Name: kong.String("request-transformer"), + Config: kong.Configuration{ + "append": map[string][]string{ + "headers": {"foo:bar"}, + }, + }, + }, + }, + ExpressionRoutes: true, + }, + }, + }, + { + name: "routes with annotations to set protocols and SNIs", + routeName: "annotations_protocol_sni.default.0.0", + ingressObjectInfo: util.K8sObjectInfo{ + Namespace: "default", + Name: "httproute-annotations", + Annotations: map[string]string{ + "konghq.com/protocols": "https", + "konghq.com/snis": "a.foo.com", + }, + }, + hostnames: []string{"a.foo.com"}, + matches: []gatewayv1beta1.HTTPRouteMatch{ + builder.NewHTTPRouteMatch().WithPathPrefix("/prefix/0/").Build(), + }, + expectedRoutes: []kongstate.Route{ + { + Ingress: util.K8sObjectInfo{ + Namespace: "default", + Name: "httproute-annotations", + Annotations: map[string]string{ + "konghq.com/protocols": "https", + "konghq.com/snis": "a.foo.com", + }, + }, + Route: kong.Route{ + Name: kong.String("annotations_protocol_sni.default.0.0"), + PreserveHost: kong.Bool(true), + Expression: kong.String(`((http.path == "/prefix/0") || (http.path ^= "/prefix/0/")) && (http.host == "a.foo.com") && (net.protocol == "https") && (tls.sni == "a.foo.com")`), + Priority: kong.Int(1), + }, + Plugins: []kong.Plugin{}, + ExpressionRoutes: true, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + routes, err := GenerateKongExpressionRoutesFromHTTPRouteMatches( + tc.routeName, + tc.matches, + tc.filters, + tc.ingressObjectInfo, + tc.hostnames, + kong.StringSlice(tc.tags...), + ) + + if tc.expectedError != nil { + require.ErrorAs(t, err, &tc.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedRoutes, routes) + }) + } +} + +func TestGenerateMatcherFromHTTPRouteMatch(t *testing.T) { + testCases := []struct { + name string + match gatewayv1beta1.HTTPRouteMatch + expression string + }{ + { + name: "empty prefix path match", + match: builder.NewHTTPRouteMatch().WithPathPrefix("/").Build(), + expression: `http.path ^= "/"`, + }, + { + name: "simple non-empty prefix path match", + match: builder.NewHTTPRouteMatch().WithPathPrefix("/prefix/0").Build(), + expression: `(http.path == "/prefix/0") || (http.path ^= "/prefix/0/")`, + }, + { + name: "simple exact path match", + match: builder.NewHTTPRouteMatch().WithPathExact("/exact/0/").Build(), + expression: `http.path == "/exact/0/"`, + }, + { + name: "simple regex match", + match: builder.NewHTTPRouteMatch().WithPathRegex("/regex/\\d{1,3}").Build(), + expression: `http.path ~ "^/regex/\\d{1,3}"`, + }, + { + name: "exact path and method and a single header", + match: builder.NewHTTPRouteMatch().WithPathExact("/exact/0"). + WithMethod(gatewayv1beta1.HTTPMethodGet). + WithHeader("foo", "bar"). + Build(), + expression: `(http.path == "/exact/0") && (http.headers.foo == "bar") && (http.method == "GET")`, + }, + { + name: "prefix path match and multiple headers", + match: builder.NewHTTPRouteMatch().WithPathPrefix("/prefix/0"). + WithHeader("X-Foo", "Bar"). + WithHeaderRegex("Hash", "[0-9A-Fa-f]{32}"). + Build(), + expression: `((http.path == "/prefix/0") || (http.path ^= "/prefix/0/")) && ((http.headers.hash ~ "[0-9A-Fa-f]{32}") && (http.headers.x_foo == "Bar"))`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expression, generateMatcherFromHTTPRouteMatch(tc.match).Expression()) + }) + } +} diff --git a/internal/util/builder/httproutefilter.go b/internal/util/builder/httproutefilter.go new file mode 100644 index 0000000000..d2213c6d9b --- /dev/null +++ b/internal/util/builder/httproutefilter.go @@ -0,0 +1,95 @@ +package builder + +import ( + "github.com/samber/lo" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// HTTPRouteFilterBuilder is a builder for gateway api HTTPRouteMatch. +// Primarily used for testing. +type HTTPRouteFilterBuilder struct { + httpRouteFilter gatewayv1beta1.HTTPRouteFilter +} + +func (b *HTTPRouteFilterBuilder) Build() gatewayv1beta1.HTTPRouteFilter { + return b.httpRouteFilter +} + +// NewHTTPRouteRequestRedirectFilter builds a request redirect HTTPRoute filter. +func NewHTTPRouteRequestRedirectFilter() *HTTPRouteFilterBuilder { + filter := gatewayv1beta1.HTTPRouteFilter{ + Type: gatewayv1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &gatewayv1beta1.HTTPRequestRedirectFilter{}, + } + return &HTTPRouteFilterBuilder{httpRouteFilter: filter} +} + +// WithRequestRedirectScheme sets scheme of request redirect filter. +func (b *HTTPRouteFilterBuilder) WithRequestRedirectScheme(scheme string) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestRedirect || + b.httpRouteFilter.RequestRedirect == nil { + return b + } + + b.httpRouteFilter.RequestRedirect.Scheme = lo.ToPtr(scheme) + return b +} + +// WithRequestRedirectHost sets host of request redirect filter. +func (b *HTTPRouteFilterBuilder) WithRequestRedirectHost(host string) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestRedirect || + b.httpRouteFilter.RequestRedirect == nil { + return b + } + + preciseHost := (gatewayv1beta1.PreciseHostname)(host) + b.httpRouteFilter.RequestRedirect.Hostname = lo.ToPtr(preciseHost) + return b +} + +// WithRequestRedirectStatusCode sets status code of response in request redirect filter. +func (b *HTTPRouteFilterBuilder) WithRequestRedirectStatusCode(code int) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestRedirect || + b.httpRouteFilter.RequestRedirect == nil { + return b + } + + b.httpRouteFilter.RequestRedirect.StatusCode = lo.ToPtr(code) + return b +} + +// NewHTTPRouteRequestHeaderModifierFilter builds a request header modifier HTTPRoute filter. +func NewHTTPRouteRequestHeaderModifierFilter() *HTTPRouteFilterBuilder { + filter := gatewayv1beta1.HTTPRouteFilter{ + Type: gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1beta1.HTTPHeaderFilter{}, + } + return &HTTPRouteFilterBuilder{httpRouteFilter: filter} +} + +func (b *HTTPRouteFilterBuilder) WithRequestHeaderAdd(headers []gatewayv1beta1.HTTPHeader) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier || + b.httpRouteFilter.RequestHeaderModifier == nil { + return b + } + b.httpRouteFilter.RequestHeaderModifier.Add = headers + return b +} + +func (b *HTTPRouteFilterBuilder) WithRequestHeaderSet(headers []gatewayv1beta1.HTTPHeader) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier || + b.httpRouteFilter.RequestHeaderModifier == nil { + return b + } + b.httpRouteFilter.RequestHeaderModifier.Set = headers + return b +} + +func (b *HTTPRouteFilterBuilder) WithRequestHeaderRemove(headerNames []string) *HTTPRouteFilterBuilder { + if b.httpRouteFilter.Type != gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier || + b.httpRouteFilter.RequestHeaderModifier == nil { + return b + } + b.httpRouteFilter.RequestHeaderModifier.Remove = headerNames + return b +} diff --git a/internal/util/builder/httproutematch.go b/internal/util/builder/httproutematch.go index c8272a1594..0fdf2de070 100644 --- a/internal/util/builder/httproutematch.go +++ b/internal/util/builder/httproutematch.go @@ -1,6 +1,7 @@ package builder import ( + "github.com/samber/lo" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -23,19 +24,15 @@ func (b *HTTPRouteMatchBuilder) Build() gatewayv1beta1.HTTPRouteMatch { } func (b *HTTPRouteMatchBuilder) WithPathPrefix(pathPrefix string) *HTTPRouteMatchBuilder { - return b.WithPathType(&pathPrefix, pathMatchTypePtr(gatewayv1beta1.PathMatchPathPrefix)) + return b.WithPathType(&pathPrefix, lo.ToPtr(gatewayv1beta1.PathMatchPathPrefix)) } func (b *HTTPRouteMatchBuilder) WithPathRegex(pathRegexp string) *HTTPRouteMatchBuilder { - return b.WithPathType(&pathRegexp, pathMatchTypePtr(gatewayv1beta1.PathMatchRegularExpression)) + return b.WithPathType(&pathRegexp, lo.ToPtr(gatewayv1beta1.PathMatchRegularExpression)) } func (b *HTTPRouteMatchBuilder) WithPathExact(pathRegexp string) *HTTPRouteMatchBuilder { - return b.WithPathType(&pathRegexp, pathMatchTypePtr(gatewayv1beta1.PathMatchExact)) -} - -func pathMatchTypePtr(pathMatchType gatewayv1beta1.PathMatchType) *gatewayv1beta1.PathMatchType { - return &pathMatchType + return b.WithPathType(&pathRegexp, lo.ToPtr(gatewayv1beta1.PathMatchExact)) } func (b *HTTPRouteMatchBuilder) WithPathType(pathValuePtr *string, pathTypePtr *gatewayv1beta1.PathMatchType) *HTTPRouteMatchBuilder { @@ -66,3 +63,12 @@ func (b *HTTPRouteMatchBuilder) WithHeader(name, value string) *HTTPRouteMatchBu }) return b } + +func (b *HTTPRouteMatchBuilder) WithHeaderRegex(name, value string) *HTTPRouteMatchBuilder { + b.httpRouteMatch.Headers = append(b.httpRouteMatch.Headers, gatewayv1beta1.HTTPHeaderMatch{ + Name: gatewayv1beta1.HTTPHeaderName(name), + Value: value, + Type: lo.ToPtr(gatewayv1beta1.HeaderMatchRegularExpression), + }) + return b +} From 32b0429c1b2c13bb36152682e7774df26ae44804 Mon Sep 17 00:00:00 2001 From: Yi Tao Date: Wed, 10 May 2023 17:33:04 +0800 Subject: [PATCH 3/3] address comments --- internal/dataplane/parser/translate_errors.go | 11 --- .../dataplane/parser/translate_grpcroute.go | 3 +- .../dataplane/parser/translate_httproute.go | 26 +++--- .../parser/translate_httproute_test.go | 7 +- .../dataplane/parser/translate_tcproute.go | 4 +- .../dataplane/parser/translate_tlsroute.go | 3 +- .../dataplane/parser/translate_udproute.go | 4 +- .../parser/translators/httproute_atc.go | 90 ++++++++++--------- .../parser/translators/httproute_atc_test.go | 8 +- 9 files changed, 78 insertions(+), 78 deletions(-) delete mode 100644 internal/dataplane/parser/translate_errors.go diff --git a/internal/dataplane/parser/translate_errors.go b/internal/dataplane/parser/translate_errors.go deleted file mode 100644 index eb0570b69e..0000000000 --- a/internal/dataplane/parser/translate_errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package parser - -import "errors" - -// REVIEW: use error variables in translator package instead? - -var ( - errRouteValidationNoRules = errors.New("no rules provided") - errRouteValidationQueryParamMatchesUnsupported = errors.New("query param matches are not yet supported") - errRouteValidationNoMatchRulesOrHostnamesSpecified = errors.New("no match rules or hostnames specified") -) diff --git a/internal/dataplane/parser/translate_grpcroute.go b/internal/dataplane/parser/translate_grpcroute.go index 0c228b08f2..6eca194f13 100644 --- a/internal/dataplane/parser/translate_grpcroute.go +++ b/internal/dataplane/parser/translate_grpcroute.go @@ -9,6 +9,7 @@ import ( gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators" "github.com/kong/kubernetes-ingress-controller/v2/internal/util" ) @@ -53,7 +54,7 @@ func (p *Parser) ingressRulesFromGRPCRoute(result *ingressRules, grpcroute *gate spec := grpcroute.Spec if len(spec.Rules) == 0 { - return errRouteValidationNoRules + return translators.ErrRouteValidationNoRules } // each rule may represent a different set of backend services that will be accepting diff --git a/internal/dataplane/parser/translate_httproute.go b/internal/dataplane/parser/translate_httproute.go index 11d11821f7..f311ab60f2 100644 --- a/internal/dataplane/parser/translate_httproute.go +++ b/internal/dataplane/parser/translate_httproute.go @@ -61,7 +61,7 @@ func validateHTTPRoute(httproute *gatewayv1beta1.HTTPRoute) error { // are invalid somehow make it past validation (e.g. the webhook is not enabled) we can // at least try to provide a helpful message about the situation in the manager logs. if len(spec.Rules) == 0 { - return errRouteValidationNoRules + return translators.ErrRouteValidationNoRules } return nil @@ -137,22 +137,18 @@ func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1bet // HTTPRoute specification into a []*string slice, which is the type required by translating to matchers // in expression based routes. func getHTTPRouteHostnamesAsSliceOfStrings(httproute *gatewayv1beta1.HTTPRoute) []string { - hostnames := make([]string, 0, len(httproute.Spec.Hostnames)) - for _, hostname := range httproute.Spec.Hostnames { - hostnames = append(hostnames, string(hostname)) - } - return hostnames + return lo.Map(httproute.Spec.Hostnames, func(h gatewayv1beta1.Hostname, _ int) string { + return string(h) + }) } // getHTTPRouteHostnamesAsSliceOfStringPointers translates the hostnames defined // in an HTTPRoute specification into a []*string slice, which is the type required // by kong.Route{}. func getHTTPRouteHostnamesAsSliceOfStringPointers(httproute *gatewayv1beta1.HTTPRoute) []*string { - hostnames := make([]*string, 0, len(httproute.Spec.Hostnames)) - for _, hostname := range httproute.Spec.Hostnames { - hostnames = append(hostnames, kong.String(string(hostname))) - } - return hostnames + return lo.Map(httproute.Spec.Hostnames, func(h gatewayv1beta1.Hostname, _ int) *string { + return kong.String(string(h)) + }) } // generateKongRoutesFromHTTPRouteRule converts an HTTPRoute rule to one or more @@ -242,9 +238,7 @@ func generateKongRouteFromTranslation( // get the hostnames from the HTTPRoute hostnames := getHTTPRouteHostnamesAsSliceOfStrings(httproute) return translators.GenerateKongExpressionRoutesFromHTTPRouteMatches( - translation.Name, - translation.Matches, - translation.Filters, + translation, objectInfo, hostnames, tags, @@ -294,7 +288,7 @@ func generateKongRoutesFromHTTPRouteMatches( // however in this case there must actually be some present hostnames // configured for the HTTPRoute or else it's not valid. if len(hostnames) == 0 { - return []kongstate.Route{}, errRouteValidationNoMatchRulesOrHostnamesSpecified + return []kongstate.Route{}, translators.ErrRouteValidationNoMatchRulesOrHostnamesSpecified } // otherwise apply the hostnames to the route @@ -305,7 +299,7 @@ func generateKongRoutesFromHTTPRouteMatches( // TODO: implement query param matches (https://github.com/Kong/kubernetes-ingress-controller/issues/2778) if len(matches[0].QueryParams) > 0 { - return []kongstate.Route{}, errRouteValidationQueryParamMatchesUnsupported + return []kongstate.Route{}, translators.ErrRouteValidationQueryParamMatchesUnsupported } r := generateKongstateHTTPRoute(routeName, ingressObjectInfo, hostnames) diff --git a/internal/dataplane/parser/translate_httproute_test.go b/internal/dataplane/parser/translate_httproute_test.go index 589c786d98..153da502de 100644 --- a/internal/dataplane/parser/translate_httproute_test.go +++ b/internal/dataplane/parser/translate_httproute_test.go @@ -14,6 +14,7 @@ import ( gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators" "github.com/kong/kubernetes-ingress-controller/v2/internal/store" "github.com/kong/kubernetes-ingress-controller/v2/internal/util" "github.com/kong/kubernetes-ingress-controller/v2/internal/util/builder" @@ -143,7 +144,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT } }, errs: []error{ - errRouteValidationNoMatchRulesOrHostnamesSpecified, + translators.ErrRouteValidationNoMatchRulesOrHostnamesSpecified, }, }, { @@ -233,7 +234,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT } }, errs: []error{ - errRouteValidationNoRules, + translators.ErrRouteValidationNoRules, }, }, { @@ -263,7 +264,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT } }, errs: []error{ - errRouteValidationQueryParamMatchesUnsupported, + translators.ErrRouteValidationQueryParamMatchesUnsupported, }, }, { diff --git a/internal/dataplane/parser/translate_tcproute.go b/internal/dataplane/parser/translate_tcproute.go index 3063292026..8965a3a4ef 100644 --- a/internal/dataplane/parser/translate_tcproute.go +++ b/internal/dataplane/parser/translate_tcproute.go @@ -4,6 +4,8 @@ import ( "fmt" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators" ) // ----------------------------------------------------------------------------- @@ -51,7 +53,7 @@ func (p *Parser) ingressRulesFromTCPRoute(result *ingressRules, tcproute *gatewa // are invalid somehow make it past validation (e.g. the webhook is not enabled) we can // at least try to provide a helpful message about the situation in the manager logs. if len(spec.Rules) == 0 { - return errRouteValidationNoRules + return translators.ErrRouteValidationNoRules } // each rule may represent a different set of backend services that will be accepting diff --git a/internal/dataplane/parser/translate_tlsroute.go b/internal/dataplane/parser/translate_tlsroute.go index 1166b13d5c..e6edffbddc 100644 --- a/internal/dataplane/parser/translate_tlsroute.go +++ b/internal/dataplane/parser/translate_tlsroute.go @@ -8,6 +8,7 @@ import ( gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators" "github.com/kong/kubernetes-ingress-controller/v2/internal/store" ) @@ -55,7 +56,7 @@ func (p *Parser) ingressRulesFromTLSRoute(result *ingressRules, tlsroute *gatewa return fmt.Errorf("no hostnames provided") } if len(spec.Rules) == 0 { - return errRouteValidationNoRules + return translators.ErrRouteValidationNoRules } tlsPassthrough, err := p.isTLSRoutePassthrough(tlsroute) diff --git a/internal/dataplane/parser/translate_udproute.go b/internal/dataplane/parser/translate_udproute.go index 19b0019574..8bfb5cf62a 100644 --- a/internal/dataplane/parser/translate_udproute.go +++ b/internal/dataplane/parser/translate_udproute.go @@ -4,6 +4,8 @@ import ( "fmt" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators" ) // ----------------------------------------------------------------------------- @@ -51,7 +53,7 @@ func (p *Parser) ingressRulesFromUDPRoute(result *ingressRules, udproute *gatewa // are invalid somehow make it past validation (e.g. the webhook is not enabled) we can // at least try to provide a helpful message about the situation in the manager logs. if len(spec.Rules) == 0 { - return errRouteValidationNoRules + return translators.ErrRouteValidationNoRules } // each rule may represent a different set of backend services that will be accepting diff --git a/internal/dataplane/parser/translators/httproute_atc.go b/internal/dataplane/parser/translators/httproute_atc.go index ab858bf8b4..3cd820a167 100644 --- a/internal/dataplane/parser/translators/httproute_atc.go +++ b/internal/dataplane/parser/translators/httproute_atc.go @@ -17,9 +17,7 @@ import ( // GenerateKongExpressionRoutesFromHTTPRouteMatches generates Kong routes from HTTPRouteRule // pointing to a specific backend. func GenerateKongExpressionRoutesFromHTTPRouteMatches( - routeName string, - matches []gatewayv1beta1.HTTPRouteMatch, - filters []gatewayv1beta1.HTTPRouteFilter, + translation KongRouteTranslation, ingressObjectInfo util.K8sObjectInfo, hostnames []string, tags []*string, @@ -28,14 +26,14 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches( r := kongstate.Route{ Ingress: ingressObjectInfo, Route: kong.Route{ - Name: kong.String(routeName), + Name: kong.String(translation.Name), PreserveHost: kong.Bool(true), Tags: tags, }, ExpressionRoutes: true, } - if len(matches) == 0 { + if len(translation.Matches) == 0 { if len(hostnames) == 0 { return []kongstate.Route{}, ErrRouteValidationNoMatchRulesOrHostnamesSpecified } @@ -45,48 +43,17 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches( return []kongstate.Route{r}, nil } - _, hasRedirectFilter := lo.Find(filters, func(filter gatewayv1beta1.HTTPRouteFilter) bool { + _, hasRedirectFilter := lo.Find(translation.Filters, func(filter gatewayv1beta1.HTTPRouteFilter) bool { return filter.Type == gatewayv1beta1.HTTPRouteFilterRequestRedirect }) // if the rule has request redirect filter(s), we need to generate a route for each match to // attach plugins for the filter. if hasRedirectFilter { - routes := make([]kongstate.Route, 0, len(matches)) - for _, match := range matches { - matchRoute := kongstate.Route{ - Ingress: ingressObjectInfo, - Route: kong.Route{ - Name: kong.String(routeName), - PreserveHost: kong.Bool(true), - Tags: tags, - }, - ExpressionRoutes: true, - } - // generate matcher for this HTTPRoute Match. - matcher := atc.And(generateMatcherFromHTTPRouteMatch(match)) - - // add matcher from parent httproute (hostnames, protocols, SNIs) to be ANDed with the matcher from match. - matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) - for _, m := range matchersFromParent { - matcher.And(m) - } - atc.ApplyExpression(&matchRoute.Route, matcher, 1) - - // we need to extract the path to configure redirect path of the plugins for request redirect filter. - path := "" - if match.Path != nil && match.Path.Value != nil { - path = *match.Path.Value - } - plugins := GeneratePluginsFromHTTPRouteFilters(filters, path, tags) - matchRoute.Plugins = plugins - - routes = append(routes, matchRoute) - } - return routes, nil + return generateKongExpressionRoutesWithRequestRedirectFilter(translation, ingressObjectInfo, hostnames, tags) } // if we do not need to generate a kong route for each match, we OR matchers from all matches together. - routeMatcher := atc.And(atc.Or(generateMatchersFromHTTPRouteMatches(matches)...)) + routeMatcher := atc.And(atc.Or(generateMatchersFromHTTPRouteMatches(translation.Matches)...)) // add matcher from parent httproute (hostnames, protocols, SNIs) to be ANDed with the matcher from match. matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) for _, matcher := range matchersFromParent { @@ -95,11 +62,51 @@ func GenerateKongExpressionRoutesFromHTTPRouteMatches( atc.ApplyExpression(&r.Route, routeMatcher, 1) // generate plugins. - plugins := GeneratePluginsFromHTTPRouteFilters(filters, "", tags) + plugins := GeneratePluginsFromHTTPRouteFilters(translation.Filters, "", tags) r.Plugins = plugins return []kongstate.Route{r}, nil } +func generateKongExpressionRoutesWithRequestRedirectFilter( + translation KongRouteTranslation, + ingressObjectInfo util.K8sObjectInfo, + hostnames []string, + tags []*string, +) ([]kongstate.Route, error) { + routes := make([]kongstate.Route, 0, len(translation.Matches)) + for _, match := range translation.Matches { + matchRoute := kongstate.Route{ + Ingress: ingressObjectInfo, + Route: kong.Route{ + Name: kong.String(translation.Name), + PreserveHost: kong.Bool(true), + Tags: tags, + }, + ExpressionRoutes: true, + } + // generate matcher for this HTTPRoute Match. + matcher := atc.And(generateMatcherFromHTTPRouteMatch(match)) + + // add matcher from parent httproute (hostnames, protocols, SNIs) to be ANDed with the matcher from match. + matchersFromParent := matchersFromParentHTTPRoute(hostnames, ingressObjectInfo.Annotations) + for _, m := range matchersFromParent { + matcher.And(m) + } + atc.ApplyExpression(&matchRoute.Route, matcher, 1) + + // we need to extract the path to configure redirect path of the plugins for request redirect filter. + path := "" + if match.Path != nil && match.Path.Value != nil { + path = *match.Path.Value + } + plugins := GeneratePluginsFromHTTPRouteFilters(translation.Filters, path, tags) + matchRoute.Plugins = plugins + + routes = append(routes, matchRoute) + } + return routes, nil +} + func generateMatchersFromHTTPRouteMatches(matches []gatewayv1beta1.HTTPRouteMatch) []atc.Matcher { ret := make([]atc.Matcher, 0, len(matches)) for _, match := range matches { @@ -158,7 +165,8 @@ func pathMatcherFromHTTPPathMatch(pathMatch *gatewayv1beta1.HTTPPathMatch) atc.M ) case gatewayv1beta1.PathMatchRegularExpression: // TODO: for compatibility with kong traditional routes, here we append the ^ prefix to match the path from beginning. - // Could we allow the regex to match any part of path? + // Could we allow the regex to match any part of the path? + // https://github.com/Kong/kubernetes-ingress-controller/issues/3983 return atc.NewPredicateHTTPPath(atc.OpRegexMatch, appendRegexBeginIfNotExist(path)) } diff --git a/internal/dataplane/parser/translators/httproute_atc_test.go b/internal/dataplane/parser/translators/httproute_atc_test.go index b52c07ce8e..c8d29d14b8 100644 --- a/internal/dataplane/parser/translators/httproute_atc_test.go +++ b/internal/dataplane/parser/translators/httproute_atc_test.go @@ -234,9 +234,11 @@ func TestGenerateKongExpressionRoutesFromHTTPRouteMatches(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { routes, err := GenerateKongExpressionRoutesFromHTTPRouteMatches( - tc.routeName, - tc.matches, - tc.filters, + KongRouteTranslation{ + Name: tc.routeName, + Matches: tc.matches, + Filters: tc.filters, + }, tc.ingressObjectInfo, tc.hostnames, kong.StringSlice(tc.tags...),