diff --git a/internal/dataplane/testdata/golden/httproute-example/expression-routes-on_golden.yaml b/internal/dataplane/testdata/golden/httproute-example/expression-routes-on_golden.yaml index 7fdbe21e76..15d23e745a 100644 --- a/internal/dataplane/testdata/golden/httproute-example/expression-routes-on_golden.yaml +++ b/internal/dataplane/testdata/golden/httproute-example/expression-routes-on_golden.yaml @@ -1,9 +1,9 @@ _format_version: "3.0" services: - connect_timeout: 60000 - host: httproute.default.httproute-testing._.3 - id: 23683d37-44ee-559b-add2-a0b650ad36c0 - name: httproute.default.httproute-testing._.3 + host: httproute.default.httproute-testing.3 + id: efb4a7d7-791f-504e-b005-0bd38bf1435e + name: httproute.default.httproute-testing.3 port: 8080 protocol: http read_timeout: 60000 @@ -29,9 +29,9 @@ services: - k8s-version:v1 write_timeout: 60000 - connect_timeout: 60000 - host: httproute.default.httproute-testing._.2 - id: ff054481-b58a-588f-a8c4-e6cedb6cb489 - name: httproute.default.httproute-testing._.2 + host: httproute.default.httproute-testing.1 + id: 672dc964-a7f8-5a80-a518-96fc39500cc8 + name: httproute.default.httproute-testing.1 port: 80 protocol: http read_timeout: 60000 @@ -51,21 +51,6 @@ services: - k8s-group:gateway.networking.k8s.io - k8s-version:v1 - k8s-named-route-rule:content - tags: - - k8s-name:httpbin - - k8s-namespace:default - - k8s-kind:Service - - k8s-version:v1 - write_timeout: 60000 -- connect_timeout: 60000 - host: httproute.default.httproute-testing._.1 - id: 13be00e6-9c94-561f-9598-8896cd755468 - name: httproute.default.httproute-testing._.1 - port: 80 - protocol: http - read_timeout: 60000 - retries: 5 - routes: - expression: (http.path == "/echo") || (http.path ^= "/echo/") https_redirect_status_code: 426 id: 88d36cfe-fbb0-5d7a-93c1-df18d1db3a12 @@ -87,9 +72,9 @@ services: - k8s-version:v1 write_timeout: 60000 - connect_timeout: 60000 - host: httproute.default.httproute-testing._.0 - id: 2fad71d1-7599-5c6e-9d4f-4afd44f99587 - name: httproute.default.httproute-testing._.0 + host: httproute.default.httproute-testing.0 + id: 4e3cb785-a8d0-5866-aa05-117f7c64f24d + name: httproute.default.httproute-testing.0 port: 8080 protocol: http read_timeout: 60000 @@ -118,28 +103,21 @@ services: write_timeout: 60000 upstreams: - algorithm: round-robin - name: httproute.default.httproute-testing._.3 + name: httproute.default.httproute-testing.3 tags: - k8s-name:nginx - k8s-namespace:default - k8s-kind:Service - k8s-version:v1 - algorithm: round-robin - name: httproute.default.httproute-testing._.2 - tags: - - k8s-name:httpbin - - k8s-namespace:default - - k8s-kind:Service - - k8s-version:v1 -- algorithm: round-robin - name: httproute.default.httproute-testing._.1 + name: httproute.default.httproute-testing.1 tags: - k8s-name:httpbin - k8s-namespace:default - k8s-kind:Service - k8s-version:v1 - algorithm: round-robin - name: httproute.default.httproute-testing._.0 + name: httproute.default.httproute-testing.0 tags: - k8s-name:httproute-testing - k8s-namespace:default diff --git a/internal/dataplane/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml b/internal/dataplane/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml index efbb57f4d3..1b8eba5a72 100644 --- a/internal/dataplane/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml +++ b/internal/dataplane/testdata/golden/httproute-url-rewrite-path-prefix/expression-routes-on_golden.yaml @@ -1,9 +1,9 @@ _format_version: "3.0" services: - connect_timeout: 60000 - host: httproute.default.httproute-testing._.0 - id: 2fad71d1-7599-5c6e-9d4f-4afd44f99587 - name: httproute.default.httproute-testing._.0 + host: httproute.default.httproute-testing.0 + id: 4e3cb785-a8d0-5866-aa05-117f7c64f24d + name: httproute.default.httproute-testing.0 port: 8080 protocol: http read_timeout: 60000 @@ -42,7 +42,7 @@ services: write_timeout: 60000 upstreams: - algorithm: round-robin - name: httproute.default.httproute-testing._.0 + name: httproute.default.httproute-testing.0 tags: - k8s-name:httproute-testing - k8s-namespace:default diff --git a/internal/dataplane/translator/subtranslator/httproute.go b/internal/dataplane/translator/subtranslator/httproute.go index 171426dd66..2261af8bda 100644 --- a/internal/dataplane/translator/subtranslator/httproute.go +++ b/internal/dataplane/translator/subtranslator/httproute.go @@ -61,16 +61,51 @@ func TranslateHTTPRoutesToKongstateServices( storer store.Storer, routes []*gatewayapi.HTTPRoute, combinedServicesFromDifferentHTTPRoutes bool, + expressionRoutes bool, ) HTTPRoutesTranslationResult { serviceNameToRules := groupRulesFromHTTPRoutesByKongServiceName(routes, combinedServicesFromDifferentHTTPRoutes) + // first, split HTTPRoutes by hostnames and matches. + ruleToSplitMatchesWithPriorities := make(map[string][]SplitHTTPRouteMatchToKongRoutePriority) + if expressionRoutes { + splitHTTPRouteMatches := []SplitHTTPRouteMatch{} + for _, route := range routes { + splitHTTPRouteMatches = append(splitHTTPRouteMatches, SplitHTTPRoute(route)...) + } + // assign priorities to split HTTPRoutes. + splitHTTPRouteMatchesWithPriorities := AssignRoutePriorityToSplitHTTPRouteMatches(logger, splitHTTPRouteMatches) + for _, matchWithPriority := range splitHTTPRouteMatchesWithPriorities { + sourceRoute := matchWithPriority.Match.Source + ruleKey := fmt.Sprintf("%s/%s.%d", sourceRoute.Namespace, sourceRoute.Name, matchWithPriority.Match.RuleIndex) + ruleToSplitMatchesWithPriorities[ruleKey] = append(ruleToSplitMatchesWithPriorities[ruleKey], matchWithPriority) + } + } + kongstateServiceCache := map[string]kongstate.Service{} routeTranslationErrors := map[k8stypes.NamespacedName][]error{} for serviceName, rulesMeta := range serviceNameToRules { if len(rulesMeta) == 0 { continue } - service, err := translateHTTPRouteRulesMetaToKongstateService(logger, storer, serviceName, rulesMeta) + + // Sort the rules according to the namespace, name and rule number to guarantee a fixed order. + sort.SliceStable(rulesMeta, func(i, j int) bool { + return rulesMeta[i].getRuleKey() < rulesMeta[j].getRuleKey() + }) + + matchesWithPriorities := []SplitHTTPRouteMatchToKongRoutePriority{} + if expressionRoutes { + for _, ruleMeta := range rulesMeta { + ruleKey := ruleMeta.getRuleKey() + matchesWithPriorities = append(matchesWithPriorities, ruleToSplitMatchesWithPriorities[ruleKey]...) + } + } + service, err := translateHTTPRouteRulesMetaToKongstateService( + logger, storer, serviceName, rulesMeta, + expressionRoutes, + matchesWithPriorities, + ) + // Set translation errors for involved HTTPRoutes on failure of translation. if err != nil { httpRoutes := extractUniqueHTTPRoutes(rulesMeta) for _, route := range httpRoutes { @@ -82,6 +117,7 @@ func TranslateHTTPRoutesToKongstateServices( } continue } + kongstateServiceCache[serviceName] = service } @@ -165,6 +201,8 @@ func translateHTTPRouteRulesMetaToKongstateService( storer store.Storer, serviceName string, rulesMeta []httpRouteRuleMeta, + expressionRoutes bool, + matchesWithPriorities []SplitHTTPRouteMatchToKongRoutePriority, ) (kongstate.Service, error) { // Fill in the common fields of the kongstate.Service. service := kongstate.Service{ @@ -235,13 +273,21 @@ func translateHTTPRouteRulesMetaToKongstateService( applyTimeoutToServiceFromHTTPRouteRule(&service, ruleMeta.Rule) } - routes, err := translateHTTPRouteRulesMetaToKongstateRoutes(rulesMeta) - if err != nil { - return kongstate.Service{}, err + if expressionRoutes { + routes, err := translateHTTPRouteRulesMetaToKongstateRoutesWithExpression(matchesWithPriorities) + if err != nil { + return kongstate.Service{}, err + } + service.Routes = make([]kongstate.Route, 0, len(routes)) + service.Routes = append(service.Routes, routes...) + } else { + routes, err := translateHTTPRouteRulesMetaToKongstateRoutes(rulesMeta) + if err != nil { + return kongstate.Service{}, err + } + service.Routes = make([]kongstate.Route, 0, len(routes)) + service.Routes = append(service.Routes, routes...) } - // preallocate slice for the routes. - service.Routes = make([]kongstate.Route, 0, len(routes)) - service.Routes = append(service.Routes, routes...) return service, nil } @@ -355,6 +401,24 @@ func translateHTTPRouteRulesMetaToKongstateRoutes( return routes, nil } +func translateHTTPRouteRulesMetaToKongstateRoutesWithExpression( + matchesWithPriorities []SplitHTTPRouteMatchToKongRoutePriority, +) ([]kongstate.Route, error) { + routes := []kongstate.Route{} + for _, matchWithPriority := range matchesWithPriorities { + // Since each match is assigned a deterministic priority, we have to generate one route for each split match + // because every match have a different priority. + // TODO: update the algorithm to assign priorities to matches to make it possible to consilidate some matches. + // For example, we can assign the same priority to multiple matches from the same rule if they tie on the priority from the fixed fields. + route, err := KongExpressionRouteFromHTTPRouteMatchWithPriority(matchWithPriority) + if err != nil { + return []kongstate.Route{}, err + } + routes = append(routes, *route) + } + return routes, nil +} + // extractUniqueHTTPRoutes extracts unique HTTPRoutes in a grouped list of HTTPRouteRuleMeta. func extractUniqueHTTPRoutes(rulesMeta []httpRouteRuleMeta) []*gatewayapi.HTTPRoute { routes := lo.Map(rulesMeta, func(m httpRouteRuleMeta, _ int) *gatewayapi.HTTPRoute { @@ -428,6 +492,10 @@ func (m httpRouteRuleMeta) getHTTPBackendRefsKey() string { return getSortedItemsString(m.Rule.BackendRefs) } +func (m httpRouteRuleMeta) getRuleKey() string { + return fmt.Sprintf("%s/%s.%d", m.parentRoute.Namespace, m.parentRoute.Name, m.RuleNumber) +} + // getFiltersKey computes a key from a list of filters. // The order of the filters is not important. func (m httpRouteRuleMeta) getFiltersKey() string { diff --git a/internal/dataplane/translator/subtranslator/httproute_atc.go b/internal/dataplane/translator/subtranslator/httproute_atc.go index a75d87526a..ccd6b36137 100644 --- a/internal/dataplane/translator/subtranslator/httproute_atc.go +++ b/internal/dataplane/translator/subtranslator/httproute_atc.go @@ -175,9 +175,6 @@ func pathMatcherFromHTTPPathMatch(pathMatch *gatewayapi.HTTPPathMatch) atc.Match atc.NewPredicateHTTPPath(atc.OpPrefixMatch, path+"/"), ) case gatewayapi.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 the path? - // https://github.com/Kong/kubernetes-ingress-controller/issues/3983 return atc.NewPredicateHTTPPath(atc.OpRegexMatch, appendRegexBeginIfNotExist(path)) } diff --git a/internal/dataplane/translator/subtranslator/httproute_test.go b/internal/dataplane/translator/subtranslator/httproute_test.go index 2c7314cb73..95ebdc12e0 100644 --- a/internal/dataplane/translator/subtranslator/httproute_test.go +++ b/internal/dataplane/translator/subtranslator/httproute_test.go @@ -1694,7 +1694,7 @@ func TestTranslateHTTPRoutesToKongstateServices(t *testing.T) { oldHTTPRoutes = append(oldHTTPRoutes, r.DeepCopy()) } - translationResult := TranslateHTTPRoutesToKongstateServices(logger, fakestore, tc.httpRoutes, true) + translationResult := TranslateHTTPRoutesToKongstateServices(logger, fakestore, tc.httpRoutes, true, false) require.Len(t, translationResult.HTTPRouteNameToTranslationErrors, 0, "Should not get translation errors in translating") kongstateServices := translationResult.ServiceNameToKongstateService @@ -1720,3 +1720,117 @@ func TestTranslateHTTPRoutesToKongstateServices(t *testing.T) { }) } } + +func TestTranslateHTTPRouteRulesMetaToKongstateRoutes(t *testing.T) { + + httpRouteTypeMeta := metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + } + backendRefList := []gatewayapi.HTTPBackendRef{ + { + BackendRef: gatewayapi.BackendRef{ + BackendObjectReference: gatewayapi.BackendObjectReference{ + Kind: lo.ToPtr(gatewayapi.Kind("Service")), + Name: gatewayapi.ObjectName("service-1"), + Namespace: lo.ToPtr(gatewayapi.Namespace("default")), + Port: lo.ToPtr(gatewayapi.PortNumber(80)), + }, + }, + }, + } + httpRouteWithoutHost := &gatewayapi.HTTPRoute{ + TypeMeta: httpRouteTypeMeta, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "httproute-1", + }, + } + + testCases := []struct { + name string + rulesMeta []httpRouteRuleMeta + expectedRoutes []kongstate.Route + expectError bool + }{ + { + name: "multiple rules with the same(empty) filter from the same HTTPRoute", + rulesMeta: []httpRouteRuleMeta{ + { + Rule: gatewayapi.HTTPRouteRule{ + BackendRefs: backendRefList, + Matches: []gatewayapi.HTTPRouteMatch{ + { + Path: &gatewayapi.HTTPPathMatch{ + Type: lo.ToPtr(gatewayapi.PathMatchExact), + Value: lo.ToPtr("/foo"), + }, + }, + { + Path: &gatewayapi.HTTPPathMatch{ + Type: lo.ToPtr(gatewayapi.PathMatchExact), + Value: lo.ToPtr("/bar"), + }, + }, + }, + }, + RuleNumber: 0, + parentRoute: httpRouteWithoutHost, + }, + { + Rule: gatewayapi.HTTPRouteRule{ + BackendRefs: backendRefList, + Matches: []gatewayapi.HTTPRouteMatch{ + { + Path: &gatewayapi.HTTPPathMatch{ + Type: lo.ToPtr(gatewayapi.PathMatchExact), + Value: lo.ToPtr("/baz"), + }, + }, + }, + }, + RuleNumber: 1, + parentRoute: httpRouteWithoutHost, + }, + }, + expectedRoutes: []kongstate.Route{ + { + Route: kong.Route{ + Name: kong.String("httproute.default.httproute-1.0.0"), + Paths: kong.StringSlice("~/foo$", "~/bar$", "~/baz$"), + PreserveHost: kong.Bool(true), + StripPath: kong.Bool(false), + Protocols: []*string{ + kong.String("http"), + kong.String("https"), + }, + Tags: []*string{ + kong.String("k8s-name:httproute-1"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1"), + }, + }, + Ingress: util.FromK8sObject(httpRouteWithoutHost), + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + routes, err := translateHTTPRouteRulesMetaToKongstateRoutes(tc.rulesMeta) + if tc.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Len(t, routes, len(tc.expectedRoutes)) + for i, expectedRoute := range tc.expectedRoutes { + require.Equal(t, expectedRoute, routes[i]) + } + }) + } +} diff --git a/internal/dataplane/translator/translate_httproute.go b/internal/dataplane/translator/translate_httproute.go index 3baf6bc6b3..9d2a02f576 100644 --- a/internal/dataplane/translator/translate_httproute.go +++ b/internal/dataplane/translator/translate_httproute.go @@ -3,7 +3,6 @@ package translator import ( "errors" "fmt" - "time" "github.com/kong/go-kong/kong" "github.com/samber/lo" @@ -40,58 +39,10 @@ func (t *Translator) ingressRulesFromHTTPRoutes() ingressRules { httpRoutesToTranslate = append(httpRoutesToTranslate, httproute) } - if t.featureFlags.ExpressionRoutes { - t.ingressRulesFromHTTPRoutesUsingExpressionRoutes(httpRoutesToTranslate, &result) - return result - } - t.ingressRulesFromHTTPRoutesWithCombinedService(httpRoutesToTranslate, &result) return result } -// applyTimeoutsToService applies timeouts from HTTPRoute to the service. -// If the HTTPRoute has multiple rules, the timeout from the last rule which has specific timeout will be applied to the service. -// If the HTTPRoute has multiple rules and the first rule doesn't have timeout, the default timeout will be applied to the service. -func applyTimeoutsToService(httpRoute *gatewayapi.HTTPRoute, rules *ingressRules) { - // If the HTTPRoute doesn't have rules, we don't need to apply timeouts to the service. - if httpRoute.Spec.Rules == nil { - return - } - - backendRequestTimeout := DefaultServiceTimeout - for _, rule := range httpRoute.Spec.Rules { - if rule.Timeouts != nil && rule.Timeouts.BackendRequest != nil { - duration, err := time.ParseDuration(string(*rule.Timeouts.BackendRequest)) - // We ignore the error here because the rule.Timeouts.BackendRequest is validated - // to be a strict subset of Golang time.ParseDuration so it should never happen - if err != nil { - continue - } - backendRequestTimeout = int(duration.Milliseconds()) - } - } - - // if the backendRequestTimeout is the same as the default timeout, we don't need to apply it to the service. - if backendRequestTimeout == DefaultServiceTimeout { - return - } - - // due rules.ServiceNameToServices is a map, we need to iterate over the map to find the service - // which has the same parent as the HTTPRoute. - for serviceName, service := range rules.ServiceNameToServices { - if service.Parent.GetObjectKind() == httpRoute.GetObjectKind() && service.Parent.GetName() == httpRoute.Name && service.Parent.GetNamespace() == httpRoute.Namespace { - // Due to only one field being available in the Gateway API to control this behavior, - // when users set `spec.rules[].timeouts` in HTTPRoute, - // KIC will also set ReadTimeout, WriteTimeout and ConnectTimeout for the service to this value - // https://github.com/Kong/kubernetes-ingress-controller/issues/4914#issuecomment-1813964669 - service.Service.ReadTimeout = kong.Int(backendRequestTimeout) - service.Service.ConnectTimeout = kong.Int(backendRequestTimeout) - service.Service.WriteTimeout = kong.Int(backendRequestTimeout) - rules.ServiceNameToServices[serviceName] = service - } - } -} - func validateHTTPRoute(httproute *gatewayapi.HTTPRoute, featureFlags FeatureFlags) error { spec := httproute.Spec @@ -131,6 +82,7 @@ func (t *Translator) ingressRulesFromHTTPRoutesWithCombinedService(httpRoutes [] t.storer, httpRoutes, t.featureFlags.CombinedServicesFromDifferentHTTPRoutes, + t.featureFlags.ExpressionRoutes, ) for serviceName, service := range translationResult.ServiceNameToKongstateService { result.ServiceNameToServices[serviceName] = service @@ -155,52 +107,6 @@ func (t *Translator) ingressRulesFromHTTPRoutesWithCombinedService(httpRoutes [] } } -// ingressRulesFromHTTPRoutesUsingExpressionRoutes translates HTTPRoutes to expression based routes -// when ExpressionRoutes feature flag is enabled. -// Because we need to assign different priorities based on the hostname and match in the specification of HTTPRoutes, -// We need to split the HTTPRoutes into ones with only one hostname and one match, then assign priority to them -// and finally translate the split HTTPRoutes into Kong services and routes with assigned priorities. -func (t *Translator) ingressRulesFromHTTPRoutesUsingExpressionRoutes(httpRoutes []*gatewayapi.HTTPRoute, result *ingressRules) { - // first, split HTTPRoutes by hostnames and matches. - splitHTTPRouteMatches := []subtranslator.SplitHTTPRouteMatch{} - for _, httproute := range httpRoutes { - splitHTTPRouteMatches = append(splitHTTPRouteMatches, subtranslator.SplitHTTPRoute(httproute)...) - } - // assign priorities to split HTTPRoutes. - splitHTTPRoutesWithPriorities := subtranslator.AssignRoutePriorityToSplitHTTPRouteMatches(t.logger, splitHTTPRouteMatches) - httpRouteNameToTranslationFailure := map[k8stypes.NamespacedName][]error{} - - // translate split HTTPRoute matches to ingress rules, including services, routes, upstreams. - for _, httpRouteWithPriority := range splitHTTPRoutesWithPriorities { - err := t.ingressRulesFromSplitHTTPRouteMatchWithPriority(result, httpRouteWithPriority) - if err != nil { - nsName := k8stypes.NamespacedName{ - Namespace: httpRouteWithPriority.Match.Source.Namespace, - Name: httpRouteWithPriority.Match.Source.Name, - } - httpRouteNameToTranslationFailure[nsName] = append(httpRouteNameToTranslationFailure[nsName], err) - } - } - // Register successful translated objects and translation failures. - // Because one HTTPRoute may be split into multiple HTTPRoutes, we need to de-duplicate by namespace and name. - for _, httproute := range httpRoutes { - nsName := k8stypes.NamespacedName{ - Namespace: httproute.Namespace, - Name: httproute.Name, - } - if translationFailures, ok := httpRouteNameToTranslationFailure[nsName]; !ok { - applyTimeoutsToService(httproute, result) - } else { - t.registerTranslationFailure( - fmt.Sprintf("HTTPRoute can't be routed: %v", errors.Join(translationFailures...)), - httproute, - ) - continue - } - t.registerSuccessfullyTranslatedObject(httproute) - } -} - // ----------------------------------------------------------------------------- // Translate HTTPRoute - Utils // ----------------------------------------------------------------------------- @@ -248,7 +154,6 @@ func GenerateKongRouteFromTranslation( // get the hostnames from the HTTPRoute hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) - return subtranslator.GenerateKongRoutesFromHTTPRouteMatches( translation.Name, translation.Matches, @@ -258,58 +163,3 @@ func GenerateKongRouteFromTranslation( tags, ) } - -func httpBackendRefsToBackendRefs(httpBackendRef []gatewayapi.HTTPBackendRef) []gatewayapi.BackendRef { - backendRefs := make([]gatewayapi.BackendRef, 0, len(httpBackendRef)) - - for _, hRef := range httpBackendRef { - backendRefs = append(backendRefs, hRef.BackendRef) - } - return backendRefs -} - -// ingressRulesFromSplitHTTPRouteMatchWithPriority translates a single match split from HTTPRoute -// to ingress rule, including Kong service and Kong route. -func (t *Translator) ingressRulesFromSplitHTTPRouteMatchWithPriority( - rules *ingressRules, - httpRouteMatchWithPriority subtranslator.SplitHTTPRouteMatchToKongRoutePriority, -) error { - match := httpRouteMatchWithPriority.Match - httpRoute := httpRouteMatchWithPriority.Match.Source - if match.RuleIndex >= len(httpRoute.Spec.Rules) { - t.logger.Error(nil, "Split match has rule out of bound of rules in source HTTPRoute", - "rule_index", match.RuleIndex, "rule_count", len(httpRoute.Spec.Rules)) - return nil - } - - rule := httpRoute.Spec.Rules[match.RuleIndex] - backendRefs := httpBackendRefsToBackendRefs(rule.BackendRefs) - serviceName := subtranslator.KongServiceNameFromSplitHTTPRouteMatch(httpRouteMatchWithPriority.Match) - - kongService, err := generateKongServiceFromBackendRefWithName( - t.logger, - t.storer, - rules, - serviceName, - httpRoute, - "http", - backendRefs..., - ) - if err != nil { - return err - } - - additionalRoutes, err := subtranslator.KongExpressionRouteFromHTTPRouteMatchWithPriority(httpRouteMatchWithPriority) - if err != nil { - return err - } - - kongService.Routes = append( - kongService.Routes, - *additionalRoutes, - ) - // cache the service to avoid duplicates in further loop iterations - rules.ServiceNameToServices[serviceName] = kongService - rules.ServiceNameToParent[serviceName] = httpRoute - return nil -} diff --git a/internal/dataplane/translator/translate_httproute_test.go b/internal/dataplane/translator/translate_httproute_test.go index bdd370ea5e..4034706ccc 100644 --- a/internal/dataplane/translator/translate_httproute_test.go +++ b/internal/dataplane/translator/translate_httproute_test.go @@ -2379,7 +2379,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { builder.NewHTTPRouteMatch().WithPathExact("/v1/barr").Build(), }, BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), + builder.NewHTTPBackendRef("service-1").WithPort(80).Build(), }, }, }, @@ -2391,7 +2391,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "service1", + Name: "service-1", }, }, }, @@ -2399,10 +2399,10 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { expectedKongServices: []kongstate.Service{ { Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.0"), + Name: kong.String("httproute.default.svc.default.service-1.80"), }, Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1"). + builder.NewKongstateServiceBackend("service-1"). WithNamespace("default"). WithPortNumber(80). MustBuild(), @@ -2410,7 +2410,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, }, expectedKongRoutes: map[string][]kongstate.Route{ - "httproute.default.httproute-1._.0": { + "httproute.default.svc.default.service-1.80": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1._.0.0"), @@ -2450,7 +2450,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), }, BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), + builder.NewHTTPBackendRef("service-1").WithPort(80).Build(), }, }, { @@ -2458,7 +2458,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { builder.NewHTTPRouteMatch().WithPathExact("/v1/barr").Build(), }, BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service2").WithPort(80).Build(), + builder.NewHTTPBackendRef("service-2").WithPort(80).Build(), }, }, }, @@ -2470,13 +2470,13 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "service1", + Name: "service-1", }, }, { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "service2", + Name: "service-2", }, }, }, @@ -2484,10 +2484,10 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { expectedKongServices: []kongstate.Service{ { Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1.foo.com.0"), + Name: kong.String("httproute.default.svc.default.service-1.80"), }, Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1"). + builder.NewKongstateServiceBackend("service-1"). WithNamespace("default"). WithPortNumber(80). MustBuild(), @@ -2495,32 +2495,10 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, { Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.bar.com.0"), + Name: kong.String("httproute.default.svc.default.service-2.80"), }, Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1"). - WithNamespace("default"). - WithPortNumber(80). - MustBuild(), - }, - }, - { - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1.foo.com.1"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service2"). - WithNamespace("default"). - WithPortNumber(80). - MustBuild(), - }, - }, - { - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.bar.com.1"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service2"). + builder.NewKongstateServiceBackend("service-2"). WithNamespace("default"). WithPortNumber(80). MustBuild(), @@ -2528,7 +2506,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, }, expectedKongRoutes: map[string][]kongstate.Route{ - "httproute.default.httproute-1.foo.com.0": { + "httproute.default.svc.default.service-1.80": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1.foo.com.0.0"), @@ -2537,8 +2515,6 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, ExpressionRoutes: true, }, - }, - "httproute.default.httproute-1._.bar.com.0": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1._.bar.com.0.0"), @@ -2548,7 +2524,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { ExpressionRoutes: true, }, }, - "httproute.default.httproute-1.foo.com.1": { + "httproute.default.svc.default.service-2.80": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1.foo.com.1.0"), @@ -2557,8 +2533,6 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, ExpressionRoutes: true, }, - }, - "httproute.default.httproute-1._.bar.com.1": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1._.bar.com.1.0"), @@ -2593,7 +2567,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), }, BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), + builder.NewHTTPBackendRef("service-1").WithPort(80).Build(), }, }, }, @@ -2605,7 +2579,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "service1", + Name: "service-1", }, }, }, @@ -2613,10 +2587,10 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { expectedKongServices: []kongstate.Service{ { Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1.foo.com.0"), + Name: kong.String("httproute.default.svc.default.service-1.80"), }, Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1"). + builder.NewKongstateServiceBackend("service-1"). WithNamespace("default"). WithPortNumber(80). MustBuild(), @@ -2624,7 +2598,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, }, expectedKongRoutes: map[string][]kongstate.Route{ - "httproute.default.httproute-1.foo.com.0": { + "httproute.default.svc.default.service-1.80": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1.foo.com.0.0"), @@ -2653,7 +2627,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { builder.NewHTTPRouteMatch().WithPathExact("/v1/barr").Build(), }, BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), + builder.NewHTTPBackendRef("service-1").WithPort(80).Build(), }, Timeouts: func() *gatewayapi.HTTPRouteTimeouts { timeout := gatewayapi.Duration("500ms") @@ -2671,7 +2645,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", - Name: "service1", + Name: "service-1", }, }, }, @@ -2679,13 +2653,13 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { expectedKongServices: []kongstate.Service{ { Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.0"), + Name: kong.String("httproute.default.svc.default.service-1.80"), ConnectTimeout: kong.Int(500), ReadTimeout: kong.Int(500), WriteTimeout: kong.Int(500), }, Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1"). + builder.NewKongstateServiceBackend("service-1"). WithNamespace("default"). WithPortNumber(80). MustBuild(), @@ -2693,7 +2667,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { }, }, expectedKongRoutes: map[string][]kongstate.Route{ - "httproute.default.httproute-1._.0": { + "httproute.default.svc.default.service-1.80": { { Route: kong.Route{ Name: kong.String("httproute.default.httproute-1._.0.0"), @@ -2724,7 +2698,7 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { translator.failuresCollector = failures.NewResourceFailuresCollector(zapr.NewLogger(zap.NewNop())) result := newIngressRules() - translator.ingressRulesFromHTTPRoutesUsingExpressionRoutes(tc.httpRoutes, &result) + translator.ingressRulesFromHTTPRoutesWithCombinedService(tc.httpRoutes, &result) // check services require.Equal(t, len(tc.expectedKongServices), len(result.ServiceNameToServices), "should have expected number of services") @@ -2750,388 +2724,6 @@ func TestIngressRulesFromHTTPRoutesUsingExpressionRoutes(t *testing.T) { } } -func TestIngressRulesFromSplitHTTPRouteMatchWithPriority(t *testing.T) { - httpRouteTypeMeta := metav1.TypeMeta{Kind: "HTTPRoute", APIVersion: gatewayv1beta1.GroupVersion.String()} - - testCases := []struct { - name string - matchWithPriority subtranslator.SplitHTTPRouteMatchToKongRoutePriority - storeObjects store.FakeObjects - expectedKongService kongstate.Service - expectedKongRoute kongstate.Route - expectedError error - }{ - { - name: "no hostname", - matchWithPriority: subtranslator.SplitHTTPRouteMatchToKongRoutePriority{ - Match: subtranslator.SplitHTTPRouteMatch{ - Source: &gatewayapi.HTTPRoute{ - TypeMeta: httpRouteTypeMeta, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "httproute-1", - }, - Spec: gatewayapi.HTTPRouteSpec{ - Rules: []gatewayapi.HTTPRouteRule{ - { - Matches: []gatewayapi.HTTPRouteMatch{ - builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - }, - BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), - }, - }, - }, - }, - }, - Match: builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - RuleIndex: 0, - MatchIndex: 0, - }, - Priority: 1024, - }, - storeObjects: store.FakeObjects{ - Services: []*corev1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service1", - }, - }, - }, - }, - expectedKongService: kongstate.Service{ - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.0"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1").WithPortNumber(80).MustBuild(), - }, - }, - expectedKongRoute: kongstate.Route{ - Route: kong.Route{ - Name: kong.String("httproute.default.httproute-1._.0.0"), - Expression: kong.String(`http.path == "/v1/foo"`), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(false), - Priority: kong.Uint64(1024), - }, - ExpressionRoutes: true, - }, - }, - { - name: "precise hostname and filter", - matchWithPriority: subtranslator.SplitHTTPRouteMatchToKongRoutePriority{ - Match: subtranslator.SplitHTTPRouteMatch{ - Source: &gatewayapi.HTTPRoute{ - TypeMeta: httpRouteTypeMeta, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "httproute-1", - }, - Spec: gatewayapi.HTTPRouteSpec{ - Hostnames: []gatewayapi.Hostname{ - "foo.com", - }, - Rules: []gatewayapi.HTTPRouteRule{ - { - Matches: []gatewayapi.HTTPRouteMatch{ - builder.NewHTTPRouteMatch().WithPathExact("/foo").Build(), - builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - }, - BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), - }, - Filters: []gatewayapi.HTTPRouteFilter{ - builder.NewHTTPRouteRequestRedirectFilter(). - WithRequestRedirectStatusCode(301). - WithRequestRedirectHost("bar.com"). - Build(), - }, - }, - }, - }, - }, - Hostname: "foo.com", - Match: builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - RuleIndex: 0, - MatchIndex: 1, - }, - Priority: 1024, - }, - storeObjects: store.FakeObjects{ - Services: []*corev1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service1", - }, - }, - }, - }, - expectedKongService: kongstate.Service{ - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1.foo.com.0"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1").WithPortNumber(80).MustBuild(), - }, - }, - expectedKongRoute: kongstate.Route{ - Route: kong.Route{ - Name: kong.String("httproute.default.httproute-1.foo.com.0.1"), - Expression: kong.String(`(http.host == "foo.com") && (http.path == "/v1/foo")`), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(false), - Priority: kong.Uint64(1024), - }, - Plugins: []kong.Plugin{ - { - Name: kong.String("request-termination"), - Config: kong.Configuration{ - "status_code": kong.Int(301), - }, - Tags: []*string{ - kong.String("k8s-name:httproute-1"), - kong.String("k8s-namespace:default"), - kong.String("k8s-kind:HTTPRoute"), - kong.String("k8s-group:gateway.networking.k8s.io"), - kong.String("k8s-version:v1beta1"), - }, - }, - { - Name: kong.String("response-transformer"), - Config: kong.Configuration{ - "add": subtranslator.TransformerPluginConfig{ - Headers: []string{"Location: http://bar.com:80/v1/foo"}, - }, - }, - Tags: []*string{ - kong.String("k8s-name:httproute-1"), - kong.String("k8s-namespace:default"), - kong.String("k8s-kind:HTTPRoute"), - kong.String("k8s-group:gateway.networking.k8s.io"), - kong.String("k8s-version:v1beta1"), - }, - }, - }, - ExpressionRoutes: true, - }, - }, - { - name: "wildcard hostname with multiple backends", - matchWithPriority: subtranslator.SplitHTTPRouteMatchToKongRoutePriority{ - Match: subtranslator.SplitHTTPRouteMatch{ - Source: &gatewayapi.HTTPRoute{ - TypeMeta: httpRouteTypeMeta, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "httproute-1", - }, - Spec: gatewayapi.HTTPRouteSpec{ - Hostnames: []gatewayapi.Hostname{ - "*.foo.com", - }, - Rules: []gatewayapi.HTTPRouteRule{ - { - Matches: []gatewayapi.HTTPRouteMatch{ - builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - }, - BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).WithWeight(10).Build(), - builder.NewHTTPBackendRef("service2").WithPort(80).WithWeight(20).Build(), - }, - }, - }, - }, - }, - Hostname: "*.foo.com", - Match: builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(), - RuleIndex: 0, - MatchIndex: 0, - }, - Priority: 1024, - }, - storeObjects: store.FakeObjects{ - Services: []*corev1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service1", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service2", - }, - }, - }, - }, - expectedKongService: kongstate.Service{ - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.foo.com.0"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1").WithPortNumber(80).WithWeight(10).MustBuild(), - builder.NewKongstateServiceBackend("service2").WithPortNumber(80).WithWeight(20).MustBuild(), - }, - }, - expectedKongRoute: kongstate.Route{ - Route: kong.Route{ - Name: kong.String("httproute.default.httproute-1._.foo.com.0.0"), - Expression: kong.String(`(http.host =^ ".foo.com") && (http.path == "/v1/foo")`), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(false), - Priority: kong.Uint64(1024), - }, - ExpressionRoutes: true, - }, - }, - { - name: "precise hostname and no match", - matchWithPriority: subtranslator.SplitHTTPRouteMatchToKongRoutePriority{ - Match: subtranslator.SplitHTTPRouteMatch{ - Source: &gatewayapi.HTTPRoute{ - TypeMeta: httpRouteTypeMeta, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "httproute-1", - }, - Spec: gatewayapi.HTTPRouteSpec{ - Hostnames: []gatewayapi.Hostname{ - "a.foo.com", - }, - Rules: []gatewayapi.HTTPRouteRule{ - { - BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), - }, - }, - }, - }, - }, - Hostname: "a.foo.com", - Match: builder.NewHTTPRouteMatch().Build(), - RuleIndex: 0, - MatchIndex: 0, - }, - Priority: 1024, - }, - storeObjects: store.FakeObjects{ - Services: []*corev1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service1", - }, - }, - }, - }, - expectedKongService: kongstate.Service{ - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1.a.foo.com.0"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1").WithPortNumber(80).MustBuild(), - }, - }, - expectedKongRoute: kongstate.Route{ - Route: kong.Route{ - Name: kong.String("httproute.default.httproute-1.a.foo.com.0.0"), - Expression: kong.String(`http.host == "a.foo.com"`), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(false), - Priority: kong.Uint64(1024), - }, - ExpressionRoutes: true, - }, - }, - { - name: "no hostname and no match", - matchWithPriority: subtranslator.SplitHTTPRouteMatchToKongRoutePriority{ - Match: subtranslator.SplitHTTPRouteMatch{ - Source: &gatewayapi.HTTPRoute{ - TypeMeta: httpRouteTypeMeta, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "httproute-1", - }, - Spec: gatewayapi.HTTPRouteSpec{ - Rules: []gatewayapi.HTTPRouteRule{ - { - BackendRefs: []gatewayapi.HTTPBackendRef{ - builder.NewHTTPBackendRef("service1").WithPort(80).Build(), - }, - }, - }, - }, - }, - Hostname: "", - Match: builder.NewHTTPRouteMatch().Build(), - RuleIndex: 0, - MatchIndex: 0, - }, - Priority: 1024, - }, - storeObjects: store.FakeObjects{ - Services: []*corev1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service1", - }, - }, - }, - }, - expectedKongService: kongstate.Service{ - Service: kong.Service{ - Name: kong.String("httproute.default.httproute-1._.0"), - }, - Backends: []kongstate.ServiceBackend{ - builder.NewKongstateServiceBackend("service1").WithPortNumber(80).MustBuild(), - }, - }, - expectedKongRoute: kongstate.Route{ - Route: kong.Route{ - Name: kong.String("httproute.default.httproute-1._.0.0"), - Expression: kong.String(subtranslator.CatchAllHTTPExpression), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(false), - Priority: kong.Uint64(1024), - }, - ExpressionRoutes: true, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fakestore, err := store.NewFakeStore(tc.storeObjects) - require.NoError(t, err) - translator := mustNewTranslator(t, fakestore) - translator.featureFlags.ExpressionRoutes = true - - match := tc.matchWithPriority.Match - tc.expectedKongRoute.Tags = util.GenerateTagsForObject(match.Source) - tc.expectedKongRoute.Ingress = util.FromK8sObject(match.Source) - - res := newIngressRules() - err = translator.ingressRulesFromSplitHTTPRouteMatchWithPriority(&res, tc.matchWithPriority) - if tc.expectedError != nil { - require.ErrorAs(t, err, tc.expectedError) - return - } - require.NoError(t, err) - kongService, ok := res.ServiceNameToServices[*tc.expectedKongService.Name] - require.Truef(t, ok, "should find service %s", *tc.expectedKongService.Name) - require.Equal(t, tc.expectedKongService.Backends, kongService.Backends) - require.Len(t, kongService.Routes, 1) - require.Equal(t, tc.expectedKongRoute, kongService.Routes[0]) - }) - } -} - func commonRouteSpecMock(parentReferentName string) gatewayapi.CommonRouteSpec { return gatewayapi.CommonRouteSpec{ ParentRefs: []gatewayapi.ParentReference{{