From 6f990463721c19ab1f8b9979849287d50bd8f6ab Mon Sep 17 00:00:00 2001 From: Yi Tao Date: Fri, 5 May 2023 16:23:18 +0800 Subject: [PATCH] WIP translate HTTPRoute to expression based routes --- internal/dataplane/parser/translate_errors.go | 2 + .../dataplane/parser/translate_httproute.go | 28 ++- .../parser/translators/httproute_atc.go | 164 ++++++++++++++++++ .../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 - 9 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 internal/dataplane/parser/translators/httproute_atc.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..30ce0d47ee 100644 --- a/internal/dataplane/parser/translate_httproute.go +++ b/internal/dataplane/parser/translate_httproute.go @@ -85,7 +85,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 +134,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 +232,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) diff --git a/internal/dataplane/parser/translators/httproute_atc.go b/internal/dataplane/parser/translators/httproute_atc.go new file mode 100644 index 0000000000..b450a4959a --- /dev/null +++ b/internal/dataplane/parser/translators/httproute_atc.go @@ -0,0 +1,164 @@ +package translators + +import ( + "sort" + "strings" + + "github.com/kong/go-kong/kong" + 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, + _ []gatewayv1beta1.HTTPRouteFilter, // TODO: translate filters + 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 + } + + // TODO: generate multiple routes if required by filters or priority specifications. + + matchers := generateMatchersFromHTTPRouteMatches(matches) + routeMatcher := atc.And(atc.Or(matchers...)) + + // append matcher from hosts. + if len(hostnames) > 0 { + hostMatcher := hostMatcherFromHosts(hostnames) + routeMatcher = atc.And(routeMatcher, hostMatcher) + } + + // override protocols and TLS SNIs from annotations. + // translate protocols. + protocols := []string{"http", "https"} + annonationProtocols := annotations.ExtractProtocolNames(ingressObjectInfo.Annotations) + if len(annonationProtocols) > 0 { + protocols = annonationProtocols + } + protocolMatcher := protocolMatcherFromProtocols(protocols) + routeMatcher.And(protocolMatcher) + + // translate SNIs. + snis, exist := annotations.ExtractSNIs(ingressObjectInfo.Annotations) + if exist && len(snis) > 0 { + sniMatcher := sniMatcherFromSNIs(snis) + routeMatcher.And(sniMatcher) + } + + atc.ApplyExpression(&r.Route, routeMatcher, 1) + 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...) +} 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)