From 40c1c8b3f22f05e41e5593dfaf34c51d294336e9 Mon Sep 17 00:00:00 2001 From: Yi Tao Date: Fri, 5 May 2023 16:23:18 +0800 Subject: [PATCH] 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 b7205cc420..a01d958741 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.flagEnabledRegexPathPrefix) + routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.flagEnabledRegexPathPrefix, p.featureEnabledExpressionRoutes) 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 4cf7b9859e..1687efdc40 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)