diff --git a/internal/dataplane/parser/translate_httproute.go b/internal/dataplane/parser/translate_httproute.go index 3c5623b6ea..7fd70f615c 100644 --- a/internal/dataplane/parser/translate_httproute.go +++ b/internal/dataplane/parser/translate_httproute.go @@ -78,39 +78,25 @@ func (p *Parser) ingressRulesFromHTTPRoute(result *ingressRules, httproute *gate // ingressRulesFromHTTPRouteWithCombinedServiceRoutes generates a set of proto-Kong routes (ingress rules) from an HTTPRoute. // If multiple rules in the HTTPRoute use the same Service, it combines them into a single Kong route. func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *gatewayv1beta1.HTTPRoute, result *ingressRules) error { - for _, translationMeta := range translators.TranslateHTTPRoute(httproute) { + for _, kongServiceTranslation := range translators.TranslateHTTPRoute(httproute) { // HTTPRoute uses a wrapper HTTPBackendRef to add optional filters to its BackendRefs - backendRefs := httpBackendRefsToBackendRefs(translationMeta.BackendRefs) + backendRefs := httpBackendRefsToBackendRefs(kongServiceTranslation.BackendRefs) - // use the original index of the first rule that uses this service as the rule number - firstCombinedRuleNum := translationMeta.FirstRuleNumber + serviceName := kongServiceTranslation.Name // create a service and attach the routes to it - service, err := generateKongServiceFromBackendRef(p.logger, p.storer, result, httproute, firstCombinedRuleNum, "http", backendRefs...) + service, err := generateKongServiceFromBackendRefWithName(p.logger, p.storer, serviceName, result, httproute, "http", backendRefs...) if err != nil { return err } // generate the routes for the service and attach them to the service - for _, matchGroup := range translationMeta.MatchGroups { - // flatten the match group into a match slice - var matches []gatewayv1beta1.HTTPRouteMatch - for _, match := range matchGroup.Matches { - matches = append(matches, *match.Match) - } - - ruleNumber := matchGroup.FirstRuleNumber - rule := *matchGroup.FirstRule - routes, err := generateKongRouteFromHTTPRouteRuleWithConsolidatedMatches(httproute, ruleNumber, rule, matches, p.flagEnabledRegexPathPrefix) + for _, kongRouteTranslation := range kongServiceTranslation.KongRoutes { + routes, err := generateKongRouteFromTranslation(httproute, kongRouteTranslation, p.flagEnabledRegexPathPrefix) if err != nil { return err } - // for _, route := range routes { - // val, _ := json.MarshalIndent(route.Route, "", "\t") - // fmt.Println(string(val)) - // } service.Routes = append(service.Routes, routes...) - } // cache the service to avoid duplicates in further loop iterations @@ -177,72 +163,160 @@ func generateKongRoutesFromHTTPRouteRule( rule gatewayv1beta1.HTTPRouteRule, addRegexPrefix bool, ) ([]kongstate.Route, error) { - if len(rule.Matches) == 0 { - // it's acceptable for an HTTPRoute to have no matches in the rulesets, - // but only backends as long as there are hostnames. In this case, we - // match all traffic based on the hostname and leave all other routing - // options default. - // however in this case there must actually be some present hostnames - // configured for the HTTPRoute or else it's not valid. - // otherwise apply the hostnames to the route - // attach the plugins to be applied to the given route - return generateKongRoutesFromHTTPRouteRuleWithNoMatches(httproute, rule) - } - // gather the k8s object information and hostnames from the httproute objectInfo := util.FromK8sObject(httproute) hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) - // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteRuleFilters(rule) - // the HTTPRoute specification upstream specifically defines matches as // independent (e.g. each match is an OR with other matches, not an AND). // Therefore we treat each match rule as a separate Kong Route, so we iterate through // all matches to determine all the routes that will be needed for the services. var routes []kongstate.Route - for matchNumber, match := range rule.Matches { - // determine the name of the route, identify it as a route that belongs - // to a Kubernetes HTTPRoute object. - kongRouteName := fmt.Sprintf( - "httproute.%s.%s.%d.%d", - httproute.Namespace, - httproute.Name, - ruleNumber, - matchNumber, - ) + // generate kong plugins from rule.filters + plugins := generatePluginsFromHTTPRouteFilters(rule.Filters) + if len(rule.Matches) > 0 { + for matchNumber, match := range rule.Matches { + // determine the name of the route, identify it as a route that belongs + // to a Kubernetes HTTPRoute object. + routeName := kong.String(fmt.Sprintf( + "httproute.%s.%s.%d.%d", + httproute.Namespace, + httproute.Name, + ruleNumber, + matchNumber, + )) + + // TODO: implement query param matches (https://github.com/Kong/kubernetes-ingress-controller/issues/2778) + if len(match.QueryParams) > 0 { + return nil, fmt.Errorf("query param matches are not yet supported") + } - matches := []gatewayv1beta1.HTTPRouteMatch{match} - kongRoute, err := generateKongRouteFromHTTPRouteMatches(kongRouteName, matches, objectInfo, hostnames, plugins, addRegexPrefix) + // build the route object using the method and pathing information + r := kongstate.Route{ + Ingress: objectInfo, + Route: kong.Route{ + Name: routeName, + Protocols: kong.StringSlice("http", "https"), + PreserveHost: kong.Bool(true), + }, + } - if err != nil { - return nil, err + // attach any hostnames associated with the httproute + if len(hostnames) > 0 { + r.Hosts = hostnames + } + + // configure path matching information about the route if paths matching was defined + // Kong automatically infers whether or not a path is a regular expression and uses a prefix match by + // default it it is not. For those types, we use the path value as-is and let Kong determine the type. + // For exact matches, we transform the path into a regular expression that terminates after the value + if match.Path != nil { + switch *match.Path.Type { + case gatewayv1beta1.PathMatchExact: + terminated := *match.Path.Value + "$" + if addRegexPrefix { + terminated = translators.KongPathRegexPrefix + terminated + } + r.Route.Paths = []*string{&terminated} + case gatewayv1beta1.PathMatchPathPrefix: + path := *match.Path.Value + r.Route.Paths = []*string{&path} + case gatewayv1beta1.PathMatchRegularExpression: + path := *match.Path.Value + if addRegexPrefix { + path = translators.KongPathRegexPrefix + path + } + r.Route.Paths = []*string{&path} + } + } + + // configure method matching information about the route if method + // matching was defined. + if match.Method != nil { + r.Route.Methods = append(r.Route.Methods, kong.String(string(*match.Method))) + } + + // convert header matching from HTTPRoute to Route format + headers, err := convertGatewayMatchHeadersToKongRouteMatchHeaders(match.Headers) + if err != nil { + return nil, err + } + if len(headers) > 0 { + r.Route.Headers = headers + } + + // stripPath needs to be disabled by default to be conformant with the Gateway API + r.StripPath = kong.Bool(false) + + // attach the plugins to be applied to the given route + if len(plugins) != 0 { + if r.Plugins == nil { + r.Plugins = make([]kong.Plugin, 0, len(plugins)) + } + r.Plugins = append(r.Plugins, plugins...) + } + + // add the route to the list of routes for the service(s) + routes = append(routes, r) + } + } else { + // it's acceptable for an HTTPRoute to have no matches in the rulesets, + // but only backends as long as there are hostnames. In this case, we + // match all traffic based on the hostname and leave all other routing + // options default. + r := kongstate.Route{ + Ingress: objectInfo, + Route: kong.Route{ + Name: kong.String(fmt.Sprintf("httproute.%s.%s.0.0", httproute.Namespace, httproute.Name)), + Protocols: kong.StringSlice("http", "https"), + PreserveHost: kong.Bool(true), + }, } - routes = append(routes, kongRoute) + // 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 nil, fmt.Errorf("no match rules or hostnames specified") + } + + // otherwise apply the hostnames to the route + r.Hosts = append(r.Hosts, hostnames...) + + // attach the plugins to be applied to the given route + r.Plugins = append(r.Plugins, plugins...) + + // add the route to the list of routes for the service(s) + routes = append(routes, r) } return routes, nil } -func generateKongRouteFromHTTPRouteRuleWithConsolidatedMatches( +func generateKongRouteFromTranslation( httproute *gatewayv1beta1.HTTPRoute, - ruleNumber int, - rule gatewayv1beta1.HTTPRouteRule, - matches []gatewayv1beta1.HTTPRouteMatch, + translation translators.KongRoute, addRegexPrefix bool, ) ([]kongstate.Route, error) { - if len(rule.Matches) == 0 { + if len(translation.Matches) == 0 { // it's acceptable for an HTTPRoute to have no matches in the rulesets, // but only backends as long as there are hostnames. In this case, we // match all traffic based on the hostname and leave all other routing // options default. - // however in this case there must actually be some present hostnames - // configured for the HTTPRoute or else it's not valid. - // otherwise apply the hostnames to the route - // attach the plugins to be applied to the given route - return generateKongRoutesFromHTTPRouteRuleWithNoMatches(httproute, rule) + + hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) + if len(hostnames) == 0 { + return nil, fmt.Errorf("no match rules or hostnames specified") + } + + routeName := fmt.Sprintf("httproute.%s.%s.0.0", httproute.Namespace, httproute.Name) + objectInfo := util.FromK8sObject(httproute) + r := generateKongstateRoute(routeName, objectInfo, hostnames) + + plugins := generatePluginsFromHTTPRouteFilters(translation.Filters) + r.Plugins = append(r.Plugins, plugins...) + + return []kongstate.Route{r}, nil } // gather the k8s object information and hostnames from the httproute @@ -250,24 +324,17 @@ func generateKongRouteFromHTTPRouteRuleWithConsolidatedMatches( hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteRuleFilters(rule) - - // the HTTPRoute specification upstream specifically defines matches as - // independent (e.g. each match is an OR with other matches, not an AND). - // However, we treat all matches that were already consolidated as a single Kong Route - // with multiple paths. - - kongRouteName := fmt.Sprintf( - "httproute.%s.%s.%d.%d", - httproute.Namespace, - httproute.Name, - ruleNumber, - // matchNumber, - 0, //TODO: should not be 0!!! + plugins := generatePluginsFromHTTPRouteFilters(translation.Filters) + + kongRoute, err := generateKongRouteFromHTTPRouteMatches( + translation.Name, + translation.Matches, + objectInfo, + hostnames, + plugins, + addRegexPrefix, ) - kongRoute, err := generateKongRouteFromHTTPRouteMatches(kongRouteName, matches, objectInfo, hostnames, plugins, addRegexPrefix) - if err != nil { return nil, err } @@ -275,28 +342,6 @@ func generateKongRouteFromHTTPRouteRuleWithConsolidatedMatches( return []kongstate.Route{kongRoute}, nil } -func generateKongRoutesFromHTTPRouteRuleWithNoMatches(httproute *gatewayv1beta1.HTTPRoute, rule gatewayv1beta1.HTTPRouteRule) ([]kongstate.Route, error) { - hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) - if len(hostnames) == 0 { - return nil, fmt.Errorf("no match rules or hostnames specified") - } - - routeName := fmt.Sprintf("httproute.%s.%s.0.0", httproute.Namespace, httproute.Name) - objectInfo := util.FromK8sObject(httproute) - r := generateKongstateRoute(routeName, objectInfo, hostnames) - - // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteRuleFilters(rule) - if len(plugins) != 0 { - if r.Plugins == nil { - r.Plugins = make([]kong.Plugin, 0, len(plugins)) - } - r.Plugins = append(r.Plugins, plugins...) - } - - return []kongstate.Route{r}, nil -} - // generateKongRouteFromHTTPRouteMatches converts an HTTPRouteMatches to a Kong Route object. // This function assumes that the HTTPRouteMatches share the query params, headers and methods. func generateKongRouteFromHTTPRouteMatches( @@ -407,15 +452,14 @@ func generateKongstateRoute(routeName string, ingressObjectInfo util.K8sObjectIn return r } -// generatePluginsFromHTTPRouteRuleFilters accepts a rule as argument and converts -// HttpRouteRule.Filters into Kong filters. -func generatePluginsFromHTTPRouteRuleFilters(rule gatewayv1beta1.HTTPRouteRule) []kong.Plugin { +// generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong filters. +func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter) []kong.Plugin { kongPlugins := make([]kong.Plugin, 0) - if rule.Filters == nil { + if len(filters) == 0 { return kongPlugins } - for _, filter := range rule.Filters { + for _, filter := range filters { if filter.Type == gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier { kongPlugins = append(kongPlugins, generateRequestHeaderModifierKongPlugin(filter.RequestHeaderModifier)) } diff --git a/internal/dataplane/parser/translate_httproute_test.go b/internal/dataplane/parser/translate_httproute_test.go index 3ba0130c94..670c8b140b 100644 --- a/internal/dataplane/parser/translate_httproute_test.go +++ b/internal/dataplane/parser/translate_httproute_test.go @@ -1100,7 +1100,7 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, { - msg: "a single HTTPRoute with multiple rules and backendRefs generates consolidated routes", + msg: "a single HTTPRoute with multiple matches in rule generates consolidated kong rout paths", routes: []*gatewayv1beta1.HTTPRoute{{ ObjectMeta: metav1.ObjectMeta{ Name: "basic-httproute", @@ -1115,79 +1115,65 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul Rules: []gatewayv1beta1.HTTPRouteRule{ { Matches: []gatewayv1beta1.HTTPRouteMatch{ + // Two matches eligible for consolidation into a single kong route { Path: &gatewayv1beta1.HTTPPathMatch{ Type: &pathMatchPrefix, - Value: kong.String("/httpbin-1"), + Value: kong.String("/path-0"), }, }, - }, - BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(90), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-1"), }, }, + // Other two matches eligible for consolidation, but not with the above two + // as they have different methods { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(10), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-2"), }, + Method: HTTPMethodPointer(gatewayv1beta1.HTTPMethodDelete), }, - }, - }, - { - Matches: []gatewayv1beta1.HTTPRouteMatch{{ - Path: &gatewayv1beta1.HTTPPathMatch{ - Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), + { + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-3"), + }, + Method: HTTPMethodPointer(gatewayv1beta1.HTTPMethodDelete), }, - }}, - BackendRefs: []gatewayv1beta1.HTTPBackendRef{ + // Other two matches eligible for consolidation, but not with the above two + // as they have different headers { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(90), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-4"), + }, + Headers: []gatewayv1beta1.HTTPHeaderMatch{ + {Name: "x-header-1", Value: "x-value-1"}, + {Name: "x-header-2", Value: "x-value-2"}, }, }, { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(10), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-5"), + }, + Headers: []gatewayv1beta1.HTTPHeaderMatch{ + // Note the different header order + {Name: "x-header-2", Value: "x-value-2"}, + {Name: "x-header-1", Value: "x-value-1"}, }, }, }, - }, - { - Matches: []gatewayv1beta1.HTTPRouteMatch{{ - Path: &gatewayv1beta1.HTTPPathMatch{ - Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), - }, - }}, BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, + Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, Kind: util.StringToGatewayAPIKindPtr("Service"), }, Weight: pointer.Int32(90), @@ -1196,7 +1182,7 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v3"), + Name: gatewayv1beta1.ObjectName("foo-v2"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, Kind: util.StringToGatewayAPIKindPtr("Service"), }, @@ -1241,11 +1227,39 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, Namespace: "default", Routes: []kongstate.Route{ + // First two matches consolidated into a single route { Route: kong.Route{ Name: kong.String("httproute.default.basic-httproute.0.0"), Paths: []*string{ - kong.String("/httpbin-1"), + kong.String("/path-0"), + kong.String("/path-1"), + }, + PreserveHost: kong.Bool(true), + Protocols: []*string{ + kong.String("http"), + kong.String("https"), + }, + StripPath: pointer.BoolPtr(false), + }, + Ingress: util.K8sObjectInfo{ + Name: "basic-httproute", + Namespace: corev1.NamespaceDefault, + Annotations: make(map[string]string), + GroupVersionKind: schema.GroupVersionKind{ + Group: "gateway.networking.k8s.io", + Version: "v1beta1", + Kind: "HTTPRoute", + }, + }, + }, + // Second two matches consolidated into a single route + { + Route: kong.Route{ + Name: kong.String("httproute.default.basic-httproute.0.2"), + Paths: []*string{ + kong.String("/path-2"), + kong.String("/path-3"), }, PreserveHost: kong.Bool(true), Protocols: []*string{ @@ -1253,6 +1267,7 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: pointer.BoolPtr(false), + Methods: []*string{kong.String("DELETE")}, }, Ingress: util.K8sObjectInfo{ Name: "basic-httproute", @@ -1265,11 +1280,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, }, }, + // Third two matches consolidated into a single route { Route: kong.Route{ - Name: kong.String("httproute.default.basic-httproute.1.0"), + Name: kong.String("httproute.default.basic-httproute.0.4"), Paths: []*string{ - kong.String("/httpbin-2"), + kong.String("/path-4"), + kong.String("/path-5"), }, PreserveHost: kong.Bool(true), Protocols: []*string{ @@ -1277,6 +1294,10 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: pointer.BoolPtr(false), + Headers: map[string][]string{ + "x-header-1": {"x-value-1"}, + "x-header-2": {"x-value-2"}, + }, }, Ingress: util.K8sObjectInfo{ Name: "basic-httproute", @@ -1302,81 +1323,65 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul Rules: []gatewayv1beta1.HTTPRouteRule{ { Matches: []gatewayv1beta1.HTTPRouteMatch{ + // Two matches eligible for consolidation into a single kong route { Path: &gatewayv1beta1.HTTPPathMatch{ Type: &pathMatchPrefix, - Value: kong.String("/httpbin-1"), + Value: kong.String("/path-0"), }, }, - }, - BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(90), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-1"), }, }, + // Other two matches eligible for consolidation, but not with the above two + // as they have different methods { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(10), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-2"), }, + Method: HTTPMethodPointer(gatewayv1beta1.HTTPMethodDelete), }, - }, - }, - { - Matches: []gatewayv1beta1.HTTPRouteMatch{ { Path: &gatewayv1beta1.HTTPPathMatch{ Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), + Value: kong.String("/path-3"), }, + Method: HTTPMethodPointer(gatewayv1beta1.HTTPMethodDelete), }, - }, - BackendRefs: []gatewayv1beta1.HTTPBackendRef{ + // Other two matches eligible for consolidation, but not with the above two + // as they have different headers { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(90), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-4"), + }, + Headers: []gatewayv1beta1.HTTPHeaderMatch{ + {Name: "x-header-1", Value: "x-value-1"}, + {Name: "x-header-2", Value: "x-value-2"}, }, }, { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(10), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/path-5"), + }, + Headers: []gatewayv1beta1.HTTPHeaderMatch{ + // Note the different header order + {Name: "x-header-2", Value: "x-value-2"}, + {Name: "x-header-1", Value: "x-value-1"}, }, }, }, - }, - { - Matches: []gatewayv1beta1.HTTPRouteMatch{{ - Path: &gatewayv1beta1.HTTPPathMatch{ - Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), - }, - }}, BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, + Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, Kind: util.StringToGatewayAPIKindPtr("Service"), }, Weight: pointer.Int32(90), @@ -1385,7 +1390,7 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v3"), + Name: gatewayv1beta1.ObjectName("foo-v2"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, Kind: util.StringToGatewayAPIKindPtr("Service"), }, @@ -1406,62 +1411,103 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, }, }, + }, + }, + }, - "httproute.default.basic-httproute.2": { - Service: kong.Service{ + { + msg: "a single HTTPRoute with multiple rules with different backendRefs results in a multiple services", + routes: []*gatewayv1beta1.HTTPRoute{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-httproute", + Namespace: corev1.NamespaceDefault, + }, + Spec: gatewayv1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{ + ParentRefs: []gatewayv1beta1.ParentReference{{ + Name: gatewayv1beta1.ObjectName("fake-gateway"), + }}, + }, + Rules: []gatewayv1beta1.HTTPRouteRule{{ + Matches: []gatewayv1beta1.HTTPRouteMatch{{ + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/httpbin-1"), + }, + }}, + BackendRefs: []gatewayv1beta1.HTTPBackendRef{{ + BackendRef: gatewayv1beta1.BackendRef{ + BackendObjectReference: gatewayv1beta1.BackendObjectReference{ + Name: gatewayv1beta1.ObjectName("fake-service"), + Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, + Kind: util.StringToGatewayAPIKindPtr("Service"), + }, + }, + }}, + }, { + Matches: []gatewayv1beta1.HTTPRouteMatch{{ + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/httpbin-2"), + }, + }}, + BackendRefs: []gatewayv1beta1.HTTPBackendRef{{ + BackendRef: gatewayv1beta1.BackendRef{ + BackendObjectReference: gatewayv1beta1.BackendObjectReference{ + Name: gatewayv1beta1.ObjectName("fake-service"), + Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, + Kind: util.StringToGatewayAPIKindPtr("Service"), + }, + }, + }}, + }}, + }, + }}, + expected: ingressRules{ + SecretNameToSNIs: SecretNameToSNIs{}, + ServiceNameToServices: map[string]kongstate.Service{ + "httproute.default.basic-httproute.0": { + Service: kong.Service{ // 1 service per route should be created ConnectTimeout: kong.Int(60000), - Host: kong.String("httproute.default.basic-httproute.2"), - Name: kong.String("httproute.default.basic-httproute.2"), + Host: kong.String("httproute.default.basic-httproute.0"), + Name: kong.String("httproute.default.basic-httproute.0"), Protocol: kong.String("http"), ReadTimeout: kong.Int(60000), Retries: kong.Int(5), WriteTimeout: kong.Int(60000), }, - Backends: kongstate.ServiceBackends{ - { - Name: "foo-v1", - PortDef: kongstate.PortDef{ - Mode: kongstate.PortMode(1), - Number: 8080, - }, - Weight: pointer.Int32(90), - }, - { - Name: "foo-v3", - PortDef: kongstate.PortDef{ - Mode: kongstate.PortMode(1), - Number: 8080, - }, - Weight: pointer.Int32(10), + Backends: kongstate.ServiceBackends{{ + Name: "fake-service", + PortDef: kongstate.PortDef{ + Mode: kongstate.PortMode(1), + Number: int32(ingressRulesFromHTTPRoutesCommonCasesHTTPPort1), }, - }, + }}, Namespace: "default", - Routes: []kongstate.Route{ - { - Route: kong.Route{ - Name: kong.String("httproute.default.basic-httproute.2.0"), - Paths: []*string{ - kong.String("/httpbin-2"), - }, - PreserveHost: kong.Bool(true), - Protocols: []*string{ - kong.String("http"), - kong.String("https"), - }, - StripPath: pointer.BoolPtr(false), + Routes: []kongstate.Route{{ // only 1 route should be created for this service + Route: kong.Route{ + Name: kong.String("httproute.default.basic-httproute.0.0"), + Paths: []*string{ + kong.String("/httpbin-1"), }, - Ingress: util.K8sObjectInfo{ - Name: "basic-httproute", - Namespace: corev1.NamespaceDefault, - Annotations: make(map[string]string), - GroupVersionKind: schema.GroupVersionKind{ - Group: "gateway.networking.k8s.io", - Version: "v1beta1", - Kind: "HTTPRoute", - }, + PreserveHost: kong.Bool(true), + Protocols: []*string{ + kong.String("http"), + kong.String("https"), }, + StripPath: pointer.BoolPtr(false), }, - }, + Ingress: util.K8sObjectInfo{ + Name: "basic-httproute", + Namespace: corev1.NamespaceDefault, + Annotations: make(map[string]string), + GroupVersionKind: schema.GroupVersionKind{ + Group: "gateway.networking.k8s.io", + Version: "v1beta1", + Kind: "HTTPRoute", + }, + }, + }}, Parent: &gatewayv1beta1.HTTPRoute{ Spec: gatewayv1beta1.HTTPRouteSpec{ CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{ @@ -1485,31 +1531,106 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), + Name: gatewayv1beta1.ObjectName("fake-service"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, Kind: util.StringToGatewayAPIKindPtr("Service"), }, - Weight: pointer.Int32(90), }, }, + }, + }, + { + Matches: []gatewayv1beta1.HTTPRouteMatch{ + { + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/httpbin-2"), + }, + }, + }, + BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), + Name: gatewayv1beta1.ObjectName("fake-service"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, Kind: util.StringToGatewayAPIKindPtr("Service"), }, - Weight: pointer.Int32(10), }, }, }, }, + }, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-httproute", + Namespace: "default", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "HTTPRoute", + APIVersion: "gateway.networking.k8s.io/v1beta1", + }, + }, + }, + + "httproute.default.basic-httproute.1": { + Service: kong.Service{ // 1 service per route should be created + ConnectTimeout: kong.Int(60000), + Host: kong.String("httproute.default.basic-httproute.1"), + Name: kong.String("httproute.default.basic-httproute.1"), + Protocol: kong.String("http"), + ReadTimeout: kong.Int(60000), + Retries: kong.Int(5), + WriteTimeout: kong.Int(60000), + }, + Backends: kongstate.ServiceBackends{{ + Name: "fake-service", + PortDef: kongstate.PortDef{ + Mode: kongstate.PortMode(1), + Number: int32(ingressRulesFromHTTPRoutesCommonCasesHTTPPort2), + }, + }}, + Namespace: "default", + Routes: []kongstate.Route{{ + Route: kong.Route{ + Name: kong.String("httproute.default.basic-httproute.1.0"), + Paths: []*string{ + kong.String("/httpbin-2"), + }, + PreserveHost: kong.Bool(true), + Protocols: []*string{ + kong.String("http"), + kong.String("https"), + }, + StripPath: pointer.BoolPtr(false), + }, + Ingress: util.K8sObjectInfo{ + Name: "basic-httproute", + Namespace: corev1.NamespaceDefault, + Annotations: make(map[string]string), + GroupVersionKind: schema.GroupVersionKind{ + Group: "gateway.networking.k8s.io", + Version: "v1beta1", + Kind: "HTTPRoute", + }, + }, + }}, + Parent: &gatewayv1beta1.HTTPRoute{ + Spec: gatewayv1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1beta1.CommonRouteSpec{ + ParentRefs: []gatewayv1beta1.ParentReference{ + { + Name: gatewayv1beta1.ObjectName("fake-gateway"), + }, + }, + }, + Rules: []gatewayv1beta1.HTTPRouteRule{ { Matches: []gatewayv1beta1.HTTPRouteMatch{ { Path: &gatewayv1beta1.HTTPPathMatch{ Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), + Value: kong.String("/httpbin-1"), }, }, }, @@ -1517,51 +1638,31 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), + Name: gatewayv1beta1.ObjectName("fake-service"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort1, Kind: util.StringToGatewayAPIKindPtr("Service"), }, - Weight: pointer.Int32(90), - }, - }, - { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v2"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(10), }, }, }, }, { - Matches: []gatewayv1beta1.HTTPRouteMatch{{ - Path: &gatewayv1beta1.HTTPPathMatch{ - Type: &pathMatchPrefix, - Value: kong.String("/httpbin-2"), - }, - }}, - BackendRefs: []gatewayv1beta1.HTTPBackendRef{ + Matches: []gatewayv1beta1.HTTPRouteMatch{ { - BackendRef: gatewayv1beta1.BackendRef{ - BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v1"), - Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, - Kind: util.StringToGatewayAPIKindPtr("Service"), - }, - Weight: pointer.Int32(90), + Path: &gatewayv1beta1.HTTPPathMatch{ + Type: &pathMatchPrefix, + Value: kong.String("/httpbin-2"), }, }, + }, + BackendRefs: []gatewayv1beta1.HTTPBackendRef{ { BackendRef: gatewayv1beta1.BackendRef{ BackendObjectReference: gatewayv1beta1.BackendObjectReference{ - Name: gatewayv1beta1.ObjectName("foo-v3"), + Name: gatewayv1beta1.ObjectName("fake-service"), Port: &ingressRulesFromHTTPRoutesCommonCasesHTTPPort2, Kind: util.StringToGatewayAPIKindPtr("Service"), }, - Weight: pointer.Int32(10), }, }, }, @@ -1869,6 +1970,6 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) { } } -func HTTPMethodPointer(method string) *gatewayv1beta1.HTTPMethod { - return (*gatewayv1beta1.HTTPMethod)(&method) +func HTTPMethodPointer(method gatewayv1beta1.HTTPMethod) *gatewayv1beta1.HTTPMethod { + return &method } diff --git a/internal/dataplane/parser/translate_utils.go b/internal/dataplane/parser/translate_utils.go index f7657e837f..365045937d 100644 --- a/internal/dataplane/parser/translate_utils.go +++ b/internal/dataplane/parser/translate_utils.go @@ -150,6 +150,81 @@ func generateKongServiceFromBackendRef[ return service, nil } +// generateKongServiceFromBackendRefWithName translates backendRefs for rule ruleNumber into a Kong service for use with the +// rules generated from a Gateway APIs route. +// The service name is provided by the caller. +func generateKongServiceFromBackendRefWithName[ + T types.BackendRefT, +]( + logger logrus.FieldLogger, + storer store.Storer, + serviceName string, + rules *ingressRules, + route client.Object, + protocol string, + backendRefs ...T, +) (kongstate.Service, error) { + objName := fmt.Sprintf("%s %s/%s", + route.GetObjectKind().GroupVersionKind().String(), route.GetNamespace(), route.GetName()) + if len(backendRefs) == 0 { + return kongstate.Service{}, fmt.Errorf("no backendRefs present for %s, cannot build Kong service", objName) + } + + grants, err := storer.ListReferenceGrants() + if err != nil { + return kongstate.Service{}, fmt.Errorf("could not retrieve ReferenceGrants for %s: %w", objName, err) + } + allowed := getPermittedForReferenceGrantFrom(gatewayv1alpha2.ReferenceGrantFrom{ + Group: gatewayv1alpha2.Group(route.GetObjectKind().GroupVersionKind().Group), + Kind: gatewayv1alpha2.Kind(route.GetObjectKind().GroupVersionKind().Kind), + Namespace: gatewayv1alpha2.Namespace(route.GetNamespace()), + }, grants) + + backends := backendRefsToKongStateBackends(logger, route, backendRefs, allowed) + + // the service host needs to be a resolvable name due to legacy logic so we'll + // use the anchor backendRef as the basis for the name + serviceHost := serviceName + + // check if the service is already known, and if not create it + service, ok := rules.ServiceNameToServices[serviceName] + if !ok { + service = kongstate.Service{ + Service: kong.Service{ + Name: kong.String(serviceName), + Host: kong.String(serviceHost), + Protocol: kong.String(protocol), + ConnectTimeout: kong.Int(DefaultServiceTimeout), + ReadTimeout: kong.Int(DefaultServiceTimeout), + WriteTimeout: kong.Int(DefaultServiceTimeout), + Retries: kong.Int(DefaultRetries), + }, + Namespace: route.GetNamespace(), + Backends: backends, + Parent: route, + } + } + + // In the context of the gateway API conformance tests, if there is no service for the backend, + // the response must have a status code of 500. Since The default behavior of Kong is returning 503 + // if there is no backend for a service, we inject a plugin that terminates all requests with 500 + // as status code + if len(service.Backends) == 0 { + if service.Plugins == nil { + service.Plugins = make([]kong.Plugin, 0) + } + service.Plugins = append(service.Plugins, kong.Plugin{ + Name: kong.String("request-termination"), + Config: kong.Configuration{ + "status_code": 500, + "message": "no existing backendRef provided", + }, + }) + } + + return service, nil +} + // maybePrependRegexPrefix takes a path, controller regex prefix, and a legacy heuristic toggle. It returns the path // with the Kong regex path prefix if it either began with the controller prefix or did not, but matched the legacy // heuristic, and the heuristic was enabled. diff --git a/internal/dataplane/parser/translators/httproute.go b/internal/dataplane/parser/translators/httproute.go index 79125d903a..8f0779511b 100644 --- a/internal/dataplane/parser/translators/httproute.go +++ b/internal/dataplane/parser/translators/httproute.go @@ -2,34 +2,28 @@ package translators import ( "encoding/json" - "reflect" + "fmt" "sort" - "strconv" "strings" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) -// HTTPRouteTranslationMeta is a translation of a single HTTPRoute into metadata +// KongService is a translation of a single HTTPRoute into metadata // that can be used to instantiate Kong routes and services. -// Rules from this object should route traffic to BackendRefs from this object. -type HTTPRouteTranslationMeta struct { - BackendRefs []gatewayv1beta1.HTTPBackendRef - MatchGroups []HTTPRouteMatchGroup - FirstRuleNumber int +// Routes from this object should route traffic to BackendRefs from this object. +type KongService struct { + Name string + BackendRefs []gatewayv1beta1.HTTPBackendRef + KongRoutes []KongRoute } -type HTTPRouteMatchGroup struct { - Matches []HTTPRouteMatchMeta - FirstRule *gatewayv1beta1.HTTPRouteRule - FirstRuleNumber int -} - -type HTTPRouteMatchMeta struct { - Rule *gatewayv1beta1.HTTPRouteRule - Match *gatewayv1beta1.HTTPRouteMatch - RuleNumber int - MatchNumber int +// KongRoute is a translation of a single HTTPRoute rule into metadata +// that can be used to instantiate Kong routes. +type KongRoute struct { + Name string + Matches []gatewayv1beta1.HTTPRouteMatch + Filters []gatewayv1beta1.HTTPRouteFilter } // TranslateHTTPRoutes translates a list of HTTPRoutes into a list of HTTPRouteTranslationMeta @@ -37,7 +31,7 @@ type HTTPRouteMatchMeta struct { // The translation is done by grouping the HTTPRoutes by their backendRefs. // This means that all the rules of a single HTTPRoute will be grouped together // if they share the same backendRefs. -func TranslateHTTPRoute(route *gatewayv1beta1.HTTPRoute) []*HTTPRouteTranslationMeta { +func TranslateHTTPRoute(route *gatewayv1beta1.HTTPRoute) []*KongService { index := newHTTPRouteTranslationIndex() index.setRoute(route) return index.translate() @@ -49,23 +43,20 @@ func TranslateHTTPRoute(route *gatewayv1beta1.HTTPRoute) []*HTTPRouteTranslation // httpRouteTranslationIndex aggregates all rules routing to the same backends group. type httpRouteTranslationIndex struct { + httpRoute *gatewayv1beta1.HTTPRoute rulesEntries []httpRouteRuleMeta } -type httpRouteRuleMeta struct { - ruleNumber int - rule gatewayv1beta1.HTTPRouteRule -} - // newHTTPRouteTranslationIndex creates a new httpRouteTranslationIndex. func newHTTPRouteTranslationIndex() *httpRouteTranslationIndex { return &httpRouteTranslationIndex{ - // backendsRules: make(map[httpBackendRefsKey]*rulesEntry), + rulesEntries: make([]httpRouteRuleMeta, 0), } } // setRoute sets an HTTPRoute of the index. The rules of the route are indexed. func (i *httpRouteTranslationIndex) setRoute(route *gatewayv1beta1.HTTPRoute) { + i.httpRoute = route for ruleNumber, rule := range route.Spec.Rules { i.rulesEntries = append(i.rulesEntries, httpRouteRuleMeta{ ruleNumber: ruleNumber, @@ -75,77 +66,102 @@ func (i *httpRouteTranslationIndex) setRoute(route *gatewayv1beta1.HTTPRoute) { } // translate the index into a list of HTTPRouteTranslationMeta objects. -func (i *httpRouteTranslationIndex) translate() []*HTTPRouteTranslationMeta { - translations := make([]*HTTPRouteTranslationMeta, 0) +func (i *httpRouteTranslationIndex) translate() []*KongService { + translations := make([]*KongService, 0) + + // Translates the rules in groups defined by their backendRefs and filters. for _, rulesGroup := range groupRulesByBackendRefs(i.rulesEntries) { for _, rulesGroupByFilter := range groupRulesByFilter(rulesGroup) { - translations = append(translations, translateRulesGroup(rulesGroupByFilter)) + translations = append(translations, i.translateRules(rulesGroupByFilter)) } } return translations } -func translateRulesGroup(rulesGroup []httpRouteRuleMeta) *HTTPRouteTranslationMeta { - // get the backendRefs from any rule, as they are all the same - backendRefs := rulesGroup[0].rule.BackendRefs +func (i *httpRouteTranslationIndex) translateRules(rulesMeta []httpRouteRuleMeta) *KongService { + kongRoutes := make([]KongRoute, 0) - meta := &HTTPRouteTranslationMeta{ - BackendRefs: backendRefs, - MatchGroups: make([]HTTPRouteMatchGroup, 0), - FirstRuleNumber: rulesGroup[0].ruleNumber, - } + firstRuleInGroup := rulesMeta[0].ruleNumber - for _, ruleEntry := range rulesGroup { - var matches []HTTPRouteMatchMeta - for matchNumber, match := range ruleEntry.rule.Matches { - match := match - matches = append(matches, HTTPRouteMatchMeta{ - Rule: &ruleEntry.rule, - Match: &match, - RuleNumber: ruleEntry.ruleNumber, - MatchNumber: matchNumber, - }) + for _, ruleMeta := range rulesMeta { + + if ruleMeta.ruleNumber < firstRuleInGroup { + firstRuleInGroup = ruleMeta.ruleNumber } - groupedMatches := groupByKeyFn(matches, func(m HTTPRouteMatchMeta) string { - return getJsonKey(struct { - Method *gatewayv1beta1.HTTPMethod - Headers []gatewayv1beta1.HTTPHeaderMatch - Query []gatewayv1beta1.HTTPQueryParamMatch - }{ - m.Match.Method, - m.Match.Headers, - m.Match.QueryParams, - }) + matchGroups := groupSliceByKeyFn(ruleMeta.enumerateHTTPRouteMatches(), func(m httpRouteMatchMeta) string { + return getHTTPRouteMatchKey(m) }) - if len(groupedMatches) == 0 { - matchGroup := HTTPRouteMatchGroup{ - FirstRule: &ruleEntry.rule, - FirstRuleNumber: ruleEntry.ruleNumber, - } - meta.MatchGroups = append(meta.MatchGroups, matchGroup) + // no matches means a catch-all route + if len(matchGroups) == 0 { + kongRouteName := fmt.Sprintf("httproute.%s.%s.0.0", i.httpRoute.Namespace, i.httpRoute.Name) + kongRoutes = append(kongRoutes, KongRoute{Name: kongRouteName}) continue } - for _, groupedMatch := range groupedMatches { - matchGroup := HTTPRouteMatchGroup{ - Matches: groupedMatch, - FirstRule: &ruleEntry.rule, - FirstRuleNumber: ruleEntry.ruleNumber, + for _, matchGroup := range matchGroups { + firstRuleNumber := matchGroup[0].RuleNumber + firstMatchNumber := matchGroup[0].MatchNumber + + for _, match := range matchGroup { + if match.MatchNumber < firstMatchNumber { + firstMatchNumber = match.MatchNumber + } + if match.RuleNumber < firstRuleNumber { + firstRuleNumber = match.RuleNumber + } + } + + kongRouteName := fmt.Sprintf( + "httproute.%s.%s.%d.%d", + i.httpRoute.Namespace, + i.httpRoute.Name, + firstRuleNumber, + firstMatchNumber, + ) + + var matches []gatewayv1beta1.HTTPRouteMatch + for _, matchMeta := range matchGroup { + matches = append(matches, *matchMeta.Match) } - meta.MatchGroups = append(meta.MatchGroups, matchGroup) + + kongRoutes = append(kongRoutes, KongRoute{ + Name: kongRouteName, + Matches: matches, + Filters: ruleMeta.rule.Filters, + }) } } - return meta + // Sort the routes by name to ensure that the order is deterministic. + sort.Slice(kongRoutes, func(i, j int) bool { + return kongRoutes[i].Name < kongRoutes[j].Name + }) + + // get the backendRefs and filters from any rule, as they are all the same + backendRefs := rulesMeta[0].rule.BackendRefs + + servicenName := fmt.Sprintf( + "httproute.%s.%s.%d", + i.httpRoute.Namespace, + i.httpRoute.Name, + firstRuleInGroup, + ) + + return &KongService{ + Name: servicenName, + BackendRefs: backendRefs, + KongRoutes: kongRoutes, + } } // groupRulesByBackendRefs groups the rules by their backendRefs. // The backendRefs are grouped by their key. +// The elements in the groups have the order of the original slice, but the groups themselves are not ordered. func groupRulesByBackendRefs(ruleEntries []httpRouteRuleMeta) map[string][]httpRouteRuleMeta { - rulesByBackendRefsMap := groupByKeyFn(ruleEntries, func(e httpRouteRuleMeta) string { + rulesByBackendRefsMap := groupSliceByKeyFn(ruleEntries, func(e httpRouteRuleMeta) string { return getHTTPBackendRefsKey(e.rule.BackendRefs...) }) @@ -155,67 +171,18 @@ func groupRulesByBackendRefs(ruleEntries []httpRouteRuleMeta) map[string][]httpR // groupRulesByFilter groups the rules by their filters. // The filters are grouped by deep equality, key being the numeric index. +// The elements in the groups have the order of the original slice, but the groups themselves are not ordered. func groupRulesByFilter(ruleEntries []httpRouteRuleMeta) map[string][]httpRouteRuleMeta { - filtersIndex := indexRulesFilters(ruleEntries) - - rulesByFilterMap := groupByKeyFn(ruleEntries, func(e httpRouteRuleMeta) string { - return filtersIndex.getFiltersKey(e.rule.Filters) + rulesByFilterMap := groupSliceByKeyFn(ruleEntries, func(e httpRouteRuleMeta) string { + return getFiltersKey(e.rule.Filters) }) return rulesByFilterMap } -func reduceRulesByHTTPMatch(ruleEntries []httpRouteRuleMeta) []httpRouteRuleMeta { - - // // group rules by their HTTPMatch headers as the kong.Route can only have one - // // set of headers. - // reducedRulesMap := make(map[string]httpRouteMatchMeta) - - // for _, ruleEntry := range ruleEntries { - // httpRouteMatches := make([]httpRouteMatchMeta, 0, len(ruleEntry.rule.Matches)) - // for i, match := range ruleEntry.rule.Matches { - // httpRouteMatches = append(httpRouteMatches, httpRouteMatchMeta{ - // rule: &ruleEntry.rule, - // httpRouteMatch: &match, - // matchNumber: i, - // }) - // } - - // groupedMatches := groupByKeyFn(httpRouteMatches, func(match httpRouteMatchMeta) string { - // return getJsonKey(struct { - // Method *gatewayv1beta1.HTTPMethod - // Headers []gatewayv1beta1.HTTPHeaderMatch - // }{ - // match.httpRouteMatch.Method, - // match.httpRouteMatch.Headers, - // }) - // }) - - // for key, matches := range groupedMatches { - // // aggregate the rules matches paths - - // if _, ok := reducedRulesMap[key]; !ok { - // // reducedRulesMap[key] = - // } - // reducedRulesMap[key].rule.Matches = append(reducedRulesMap[key].rule.Matches, matches...) - // } - // } - - return nil -} - -// indexRulesFilters indexes all unique filters of the rules. -func indexRulesFilters(ruleEntries []httpRouteRuleMeta) filtersIndex { - filtersIndex := newFiltersIndex() - for _, ruleEntry := range ruleEntries { - for _, filter := range ruleEntry.rule.Filters { - filtersIndex.addFilter(filter) - } - } - return filtersIndex -} - -func groupByKeyFn[T any](vals []T, keyFn func(val T) string) map[string][]T { +// groupSliceByKeyFn groups a slice by a key function. The elements in the groups +// have the order of the original slice, but the groups themselves are not ordered. +func groupSliceByKeyFn[T any](vals []T, keyFn func(val T) string) map[string][]T { groups := make(map[string][]T) for _, val := range vals { key := keyFn(val) @@ -225,135 +192,117 @@ func groupByKeyFn[T any](vals []T, keyFn func(val T) string) map[string][]T { return groups } -// flattenRulesGroups flattens a map of rules groups into a list. -// The flattening is done by keeping the order of the groups. -func flattenRulesGroups(rulesByBackendRefsMap map[string][]httpRouteRuleMeta) [][]httpRouteRuleMeta { - keys := make([]string, 0, len(rulesByBackendRefsMap)) - for k := range rulesByBackendRefsMap { - keys = append(keys, k) - } - sort.Strings(keys) - - rulesByBackendRefs := make([][]httpRouteRuleMeta, 0, len(rulesByBackendRefsMap)) - for _, k := range keys { - rulesByBackendRefs = append(rulesByBackendRefs, rulesByBackendRefsMap[k]) - } - return rulesByBackendRefs -} - // ----------------------------------------------------------------------------- // HttpRoute Translation - Private - Metadata // ----------------------------------------------------------------------------- -type filtersIndex struct { - filters map[int]gatewayv1beta1.HTTPRouteFilter +type httpRouteRuleMeta struct { + ruleNumber int + rule gatewayv1beta1.HTTPRouteRule } -func newFiltersIndex() filtersIndex { - return filtersIndex{ - filters: make(map[int]gatewayv1beta1.HTTPRouteFilter), +func (m *httpRouteRuleMeta) enumerateHTTPRouteMatches() []httpRouteMatchMeta { + var matches []httpRouteMatchMeta + + for matchNumber, match := range m.rule.Matches { + match := match + matches = append(matches, httpRouteMatchMeta{ + Match: &match, + RuleNumber: m.ruleNumber, + MatchNumber: matchNumber, + }) } + + return matches } -func (i *filtersIndex) addFilter(filter gatewayv1beta1.HTTPRouteFilter) { - for _, existingFilter := range i.filters { - if reflect.DeepEqual(existingFilter, filter) { - return - } - } - i.filters[len(i.filters)] = filter +type httpRouteMatchMeta struct { + Match *gatewayv1beta1.HTTPRouteMatch + RuleNumber int + MatchNumber int } -// getFiltersKey returns a string key representing the filters by their index. -// filters must be present in the filtersIndex or the function will return -// an empty string. -func (i *filtersIndex) getFiltersKey(filters []gatewayv1beta1.HTTPRouteFilter) string { - indexes := make([]int, 0, len(filters)) - for _, filter := range filters { - index, ok := i.getFilterIndex(filter) - if !ok { - return "" +// getHTTPRouteMatchKey computes a key from a HTTPRouteMatch, to be used to group rules by their match. +// The key is derived from the match method, headers and query parameters. +func getHTTPRouteMatchKey(m httpRouteMatchMeta) string { + // Normalize the headers used in the key. + // This is needed because the order or uniqueness of the headers is not guaranteed. + seenHeaders := make(map[string]struct{}) + headers := make([]gatewayv1beta1.HTTPHeaderMatch, 0, len(m.Match.Headers)) + for _, header := range m.Match.Headers { + name := strings.ToLower(string(header.Name)) + header.Name = gatewayv1beta1.HTTPHeaderName(name) + + if _, ok := seenHeaders[name]; ok { + continue } - indexes = append(indexes, index) - } - sort.Ints(indexes) + seenHeaders[name] = struct{}{} - var keys []string - for _, index := range indexes { - keys = append(keys, strconv.Itoa(index)) + headers = append(headers, header) } + sort.Slice(headers, func(i, j int) bool { + return headers[i].Name < headers[j].Name + }) - return strings.Join(keys, ",") -} + // Normalize the query parameters used in the key. + // This is needed because the order or uniqueness of the query parameters is not guaranteed. + seenQueryParams := make(map[string]struct{}) + queryParams := make([]gatewayv1beta1.HTTPQueryParamMatch, 0, len(m.Match.QueryParams)) + for _, queryParam := range m.Match.QueryParams { + queryParam.Name = strings.ToLower(queryParam.Name) -// getFilterIndex returns the index of the filter in the index. -// If the filter is not found the second return value is false. -func (i *filtersIndex) getFilterIndex(filter gatewayv1beta1.HTTPRouteFilter) (int, bool) { - for index, existingFilter := range i.filters { - if reflect.DeepEqual(existingFilter, filter) { - return index, true + if _, ok := seenQueryParams[queryParam.Name]; ok { + continue } + seenQueryParams[queryParam.Name] = struct{}{} + + queryParams = append(queryParams, queryParam) } - return -1, false + sort.Slice(queryParams, func(i, j int) bool { + return queryParams[i].Name < queryParams[j].Name + }) + + keySource := struct { + Method *gatewayv1beta1.HTTPMethod + Headers []gatewayv1beta1.HTTPHeaderMatch + Query []gatewayv1beta1.HTTPQueryParamMatch + }{ + m.Match.Method, + headers, + m.Match.QueryParams, + } + + return mustMarshalJSON(keySource) } // getHTTPBackendRefsKey computes a key from a list of backendRefs. +// The order of backedRefs is not important. func getHTTPBackendRefsKey(backendRefs ...gatewayv1beta1.HTTPBackendRef) string { backendKeys := make([]string, 0, len(backendRefs)) // create a list of backend keys for _, backendRef := range backendRefs { - var backendKey strings.Builder - - if backendRef.Group != nil { - backendKey.WriteString(string(*backendRef.Group)) - } - backendKey.WriteString(".") - - if backendRef.Kind != nil { - backendKey.WriteString(string(*backendRef.Kind)) - } - backendKey.WriteString(".") - - if backendRef.Namespace != nil { - backendKey.WriteString(string(*backendRef.Namespace)) - } - backendKey.WriteString(".") - - backendKey.WriteString(string(backendRef.Name)) - backendKey.WriteString(".") - - if backendRef.Port != nil { - backendKey.WriteString(strconv.Itoa(int(*backendRef.Port))) - } - backendKey.WriteString(".") - - if backendRef.Weight != nil { - backendKey.WriteString(strconv.Itoa(int(*backendRef.Weight))) - } - - backendKeys = append(backendKeys, backendKey.String()) + backendKeys = append(backendKeys, mustMarshalJSON(backendRef)) } sort.Strings(backendKeys) return strings.Join(backendKeys, ";") } -// getHTTPRouteMatchReduceKey computes a key from a HTTPRouteMatch. -// The path is not included in the key. -func getHTTPRouteMatchReduceKey(match gatewayv1beta1.HTTPRouteMatch) string { - match.Path = nil - return getJsonKey(match) -} +// getFiltersKey computes a key from a list of filters. +// The order of the filters is not important. +func getFiltersKey(filters []gatewayv1beta1.HTTPRouteFilter) string { + filterKeys := make([]string, 0, len(filters)) -func getHTTPPathMatchKey(path *gatewayv1beta1.HTTPPathMatch) string { - if path == nil { - return "" + for _, filter := range filters { + filterKeys = append(filterKeys, mustMarshalJSON(filter)) } - return getJsonKey(path) + sort.Strings(filterKeys) + + return strings.Join(filterKeys, ";") } -func getJsonKey[T any](val T) string { +func mustMarshalJSON[T any](val T) string { key, err := json.Marshal(val) if err != nil { return ""