Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement assigning priorities to Kong routes translated from HTTPRoute #4296

Merged
merged 10 commits into from
Jul 13, 2023
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ Adding a new version? You'll need three changes:
method is adopted to keep the compatibility with traditional router on
maximum effort.
[#4240](https://github.com/Kong/kubernetes-ingress-controller/pull/4240)
- Assign priorities to routes translated from HTTPRoutes when parser translates
them to expression based Kong routes. The assigning method follows the
[specification on priorities of matches in `HTTPRoute`][httproute-specification].
[#4296](https://github.com/Kong/kubernetes-ingress-controller/pull/4296)

### Changed

Expand All @@ -115,6 +119,7 @@ Adding a new version? You'll need three changes:
[#4222](https://github.com/Kong/kubernetes-ingress-controller/pull/4222)

[gojson]: https://github.com/goccy/go-json
[httproute-specification]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute

## [2.10.2]

Expand Down
83 changes: 83 additions & 0 deletions internal/dataplane/parser/translate_httproute.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package parser

import (
"errors"
"fmt"
"strings"

"github.com/blang/semver/v4"
"github.com/kong/go-kong/kong"
"github.com/samber/lo"
k8stypes "k8s.io/apimachinery/pkg/types"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
Expand All @@ -29,6 +31,46 @@ func (p *Parser) ingressRulesFromHTTPRoutes() ingressRules {
return result
}

if p.featureFlags.ExpressionRoutes {
splitHTTPRoutes := []*gatewayv1beta1.HTTPRoute{}
for _, httproute := range httpRouteList {
if err := validateHTTPRoute(httproute); err != nil {
p.registerTranslationFailure(fmt.Sprintf("HTTPRoute can't be routed: %s", err), httproute)
continue
}
splitHTTPRoutes = append(splitHTTPRoutes, translators.SplitHTTPRoute(httproute)...)
}

splitHTTPRoutesWithPriorities := translators.AssignRoutePriorityToSplitHTTPRoutes(splitHTTPRoutes)
httpRouteNameToTranslationFailure := map[k8stypes.NamespacedName][]error{}

for _, httpRouteWithPriority := range splitHTTPRoutesWithPriorities {
err := p.ingressRulesFromSplitHTTPRouteWithPriority(&result, httpRouteWithPriority)
if err != nil {
nsName := k8stypes.NamespacedName{
Namespace: httpRouteWithPriority.HTTPRoute.Namespace,
Name: httpRouteWithPriority.HTTPRoute.Name,
}
httpRouteNameToTranslationFailure[nsName] = append(httpRouteNameToTranslationFailure[nsName], err)
}
}
for _, httproute := range httpRouteList {
nsName := k8stypes.NamespacedName{
Namespace: httproute.Namespace,
Name: httproute.Name,
}
if translationFailures, ok := httpRouteNameToTranslationFailure[nsName]; ok {
p.registerTranslationFailure(
fmt.Sprintf("HTTPRoute can't be routed: %v", errors.Join(translationFailures...)),
httproute,
)
continue
}
p.registerSuccessfullyParsedObject(httproute)
}
return result
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
}

for _, httproute := range httpRouteList {
if err := p.ingressRulesFromHTTPRoute(&result, httproute); err != nil {
p.registerTranslationFailure(fmt.Sprintf("HTTPRoute can't be routed: %s", err), httproute)
Expand Down Expand Up @@ -470,3 +512,44 @@ func httpBackendRefsToBackendRefs(httpBackendRef []gatewayv1beta1.HTTPBackendRef
}
return backendRefs
}

func (p *Parser) ingressRulesFromSplitHTTPRouteWithPriority(
rules *ingressRules,
httpRouteWithPriority translators.SplitHTTPRouteToKongRoutePriority,
) error {
httpRoute := httpRouteWithPriority.HTTPRoute
if len(httpRoute.Spec.Rules) == 0 {
return translators.ErrRouteValidationNoRules
}

httpRouteRule := httpRoute.Spec.Rules[0]
if len(httpRoute.Spec.Hostnames) == 0 && len(httpRouteRule.Matches) == 0 {
return translators.ErrRouteValidationNoMatchRulesOrHostnamesSpecified
}

backendRefs := httpBackendRefsToBackendRefs(httpRouteRule.BackendRefs)

serviceName := translators.KongServiceNameFromHTTPRouteWithPriority(httpRouteWithPriority)

kongService, err := generateKongServiceFromBackendRefWithName(
p.logger,
p.storer,
rules,
serviceName,
httpRoute,
"http",
backendRefs...,
)
if err != nil {
return err
}

kongService.Routes = append(
kongService.Routes,
translators.KongExpressionRouteFromHTTPRouteWithPriority(httpRouteWithPriority),
)
// cache the service to avoid duplicates in further loop iterations
rules.ServiceNameToServices[serviceName] = kongService
rules.ServiceNameToParent[serviceName] = httpRoute
return nil
}
224 changes: 223 additions & 1 deletion internal/dataplane/parser/translate_httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1357,7 +1357,6 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) {
fakestore, err := store.NewFakeStore(store.FakeObjects{})
require.NoError(t, err)
parser := mustNewParser(t, fakestore)
require.NoError(t, err)
parser.featureFlags.RegexPathPrefix = true
parserWithCombinedServiceRoutes := mustNewParser(t, fakestore)
parserWithCombinedServiceRoutes.featureFlags.RegexPathPrefix = true
Expand Down Expand Up @@ -1470,6 +1469,229 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) {
}
}

func TestIngressRulesWithPriority(t *testing.T) {
fakestore, err := store.NewFakeStore(store.FakeObjects{})
require.NoError(t, err)
parser := mustNewParser(t, fakestore)
parser.featureFlags.CombinedServiceRoutes = true
parser.featureFlags.ExpressionRoutes = true
httpRouteTypeMeta := metav1.TypeMeta{Kind: "HTTPRoute", APIVersion: gatewayv1beta1.SchemeGroupVersion.String()}

testCases := []struct {
name string
httpRouteWithPriority translators.SplitHTTPRouteToKongRoutePriority
expectedKongService kongstate.Service
expectedKongRoute kongstate.Route
expectedError error
}{
{
name: "no hostname",
httpRouteWithPriority: translators.SplitHTTPRouteToKongRoutePriority{
HTTPRoute: &gatewayv1beta1.HTTPRoute{
TypeMeta: httpRouteTypeMeta,
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "httproute-1",
Annotations: map[string]string{
translators.InternalRuleIndexAnnotationKey: "0",
translators.InternalMatchIndexAnnotationKey: "0",
},
},
Spec: gatewayv1beta1.HTTPRouteSpec{
Rules: []gatewayv1beta1.HTTPRouteRule{
{
Matches: []gatewayv1beta1.HTTPRouteMatch{
builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(),
},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{
builder.NewHTTPBackendRef("service1").WithPort(80).Build(),
},
},
},
},
},
Priority: 1024,
},
expectedKongService: kongstate.Service{
Service: kong.Service{
Name: kong.String("httproute.default.httproute-1._.0"),
},
Backends: []kongstate.ServiceBackend{
{
Name: "service1",
PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)},
},
},
},
expectedKongRoute: kongstate.Route{
Route: kong.Route{
Name: kong.String("httproute.default.httproute-1._.0.0"),
Expression: kong.String(`((net.protocol == "http") || (net.protocol == "https")) && (http.path == "/v1/foo")`),
PreserveHost: kong.Bool(true),
Priority: kong.Int(1024),
},
Plugins: []kong.Plugin{},
ExpressionRoutes: true,
},
},
{
name: "precise hostname and filter",
httpRouteWithPriority: translators.SplitHTTPRouteToKongRoutePriority{
HTTPRoute: &gatewayv1beta1.HTTPRoute{
TypeMeta: httpRouteTypeMeta,
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "httproute-1",
Annotations: map[string]string{
translators.InternalRuleIndexAnnotationKey: "0",
translators.InternalMatchIndexAnnotationKey: "1",
},
},
Spec: gatewayv1beta1.HTTPRouteSpec{
Hostnames: []gatewayv1beta1.Hostname{
"foo.com",
},
Rules: []gatewayv1beta1.HTTPRouteRule{
{
Matches: []gatewayv1beta1.HTTPRouteMatch{
builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(),
},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{
builder.NewHTTPBackendRef("service1").WithPort(80).Build(),
},
Filters: []gatewayv1beta1.HTTPRouteFilter{
builder.NewHTTPRouteRequestRedirectFilter().
WithRequestRedirectStatusCode(301).
WithRequestRedirectHost("bar.com").
Build(),
},
},
},
},
},
Priority: 1024,
},
expectedKongService: kongstate.Service{
Service: kong.Service{
Name: kong.String("httproute.default.httproute-1.foo.com.0"),
},
Backends: []kongstate.ServiceBackend{
{
Name: "service1",
PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)},
},
},
},
expectedKongRoute: kongstate.Route{
Route: kong.Route{
Name: kong.String("httproute.default.httproute-1.foo.com.0.1"),
Expression: kong.String(`(http.host == "foo.com") && ((net.protocol == "http") || (net.protocol == "https")) && (http.path == "/v1/foo")`),
PreserveHost: kong.Bool(true),
Priority: kong.Int(1024),
},
Plugins: []kong.Plugin{
{
Name: kong.String("request-termination"),
Config: kong.Configuration{
"status_code": kong.Int(301),
},
},
{
Name: kong.String("response-transformer"),
Config: kong.Configuration{
"add": map[string][]string{
"headers": {"Location: http://bar.com:80/v1/foo"},
},
},
},
},
ExpressionRoutes: true,
},
},
{
name: "wildcard hostname with multiple backends",
httpRouteWithPriority: translators.SplitHTTPRouteToKongRoutePriority{
HTTPRoute: &gatewayv1beta1.HTTPRoute{
TypeMeta: httpRouteTypeMeta,
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "httproute-1",
Annotations: map[string]string{
translators.InternalRuleIndexAnnotationKey: "0",
translators.InternalMatchIndexAnnotationKey: "0",
},
},
Spec: gatewayv1beta1.HTTPRouteSpec{
Hostnames: []gatewayv1beta1.Hostname{
"*.foo.com",
},
Rules: []gatewayv1beta1.HTTPRouteRule{
{
Matches: []gatewayv1beta1.HTTPRouteMatch{
builder.NewHTTPRouteMatch().WithPathExact("/v1/foo").Build(),
},
BackendRefs: []gatewayv1beta1.HTTPBackendRef{
builder.NewHTTPBackendRef("service1").WithPort(80).WithWeight(10).Build(),
builder.NewHTTPBackendRef("service2").WithPort(80).WithWeight(20).Build(),
},
},
},
},
},
Priority: 1024,
},
expectedKongService: kongstate.Service{
Service: kong.Service{
Name: kong.String("httproute.default.httproute-1._.foo.com.0"),
},
Backends: []kongstate.ServiceBackend{
{
Name: "service1",
PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)},
Weight: lo.ToPtr(int32(10)),
},
{
Name: "service2",
PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)},
Weight: lo.ToPtr(int32(20)),
},
},
},
expectedKongRoute: kongstate.Route{
Route: kong.Route{
Name: kong.String("httproute.default.httproute-1._.foo.com.0.0"),
Expression: kong.String(`(http.host =^ ".foo.com") && ((net.protocol == "http") || (net.protocol == "https")) && (http.path == "/v1/foo")`),
PreserveHost: kong.Bool(true),
Priority: kong.Int(1024),
},
Plugins: []kong.Plugin{},
ExpressionRoutes: true,
},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.expectedKongRoute.Tags = util.GenerateTagsForObject(tc.httpRouteWithPriority.HTTPRoute)
tc.expectedKongRoute.Ingress = util.FromK8sObject(tc.httpRouteWithPriority.HTTPRoute)

res := newIngressRules()
err := parser.ingressRulesFromSplitHTTPRouteWithPriority(&res, tc.httpRouteWithPriority)
if tc.expectedError != nil {
require.ErrorAs(t, err, tc.expectedError)
return
}
require.NoError(t, err)
kongService, ok := res.ServiceNameToServices[*tc.expectedKongService.Name]
require.True(t, ok)
require.Equal(t, tc.expectedKongService.Backends, kongService.Backends)
require.Len(t, kongService.Routes, 1)
require.Equal(t, tc.expectedKongRoute, kongService.Routes[0])
})
}
}

func HTTPMethodPointer(method gatewayv1beta1.HTTPMethod) *gatewayv1beta1.HTTPMethod {
return &method
}
Expand Down
Loading