diff --git a/internal/dataplane/parser/translate_kong_l4_test.go b/internal/dataplane/parser/translate_kong_l4_test.go index 8f9e327d8d..19a84a6524 100644 --- a/internal/dataplane/parser/translate_kong_l4_test.go +++ b/internal/dataplane/parser/translate_kong_l4_test.go @@ -1,14 +1,19 @@ package parser import ( + "strings" "testing" "github.com/kong/go-kong/kong" + "github.com/samber/lo" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/failures" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" "github.com/kong/kubernetes-ingress-controller/v2/internal/store" configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1beta1" @@ -208,3 +213,239 @@ func TestFromTCPIngressV1beta1(t *testing.T) { assert.Equal(2, len(parsedInfo.SecretNameToSNIs.Hosts("default/sooper-secret2"))) }) } + +func TestIngressRulesFromTCPIngressV1beta1UsingExpressionRoutes(t *testing.T) { + testCases := []struct { + name string + tcpIngresses []*configurationv1beta1.TCPIngress + expectedKongServices []kongstate.Service + expectedKongRoutes map[string][]kongstate.Route + expectedFailures []failures.ResourceFailure + }{ + { + name: "tcpingress with single rule", + tcpIngresses: []*configurationv1beta1.TCPIngress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Spec: configurationv1beta1.TCPIngressSpec{ + Rules: []configurationv1beta1.IngressRule{ + { + Port: 9000, + Backend: configurationv1beta1.IngressBackend{ + ServiceName: "foo-svc", + ServicePort: 80, + }, + }, + }, + }, + }, + }, + expectedKongServices: []kongstate.Service{ + { + Service: kong.Service{ + Name: kong.String("default.foo-svc.80"), + }, + Backends: []kongstate.ServiceBackend{ + { + Name: "foo-svc", + PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)}, + }, + }, + }, + }, + expectedKongRoutes: map[string][]kongstate.Route{ + "default.foo-svc.80": { + { + Route: kong.Route{ + Name: kong.String("default.foo.0"), + Expression: kong.String(`((net.protocol == "tcp") || (net.protocol == "tls")) && (net.dst.port == 9000)`), + PreserveHost: kong.Bool(true), + }, + ExpressionRoutes: true, + }, + }, + }, + }, + { + name: "tcpingress with multiple rules", + tcpIngresses: []*configurationv1beta1.TCPIngress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Spec: configurationv1beta1.TCPIngressSpec{ + Rules: []configurationv1beta1.IngressRule{ + { + Port: 9000, + Backend: configurationv1beta1.IngressBackend{ + ServiceName: "foo-svc1", + ServicePort: 80, + }, + }, + { + Port: 9090, + Backend: configurationv1beta1.IngressBackend{ + ServiceName: "foo-svc2", + ServicePort: 8080, + }, + }, + }, + }, + }, + }, + expectedKongServices: []kongstate.Service{ + { + Service: kong.Service{ + Name: kong.String("default.foo-svc1.80"), + }, + Backends: []kongstate.ServiceBackend{ + { + Name: "foo-svc1", + PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)}, + }, + }, + }, + { + Service: kong.Service{ + Name: kong.String("default.foo-svc2.8080"), + }, + Backends: []kongstate.ServiceBackend{ + { + Name: "foo-svc2", + PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(8080)}, + }, + }, + }, + }, + expectedKongRoutes: map[string][]kongstate.Route{ + "default.foo-svc1.80": { + { + Route: kong.Route{ + Name: kong.String("default.foo.0"), + Expression: kong.String(`((net.protocol == "tcp") || (net.protocol == "tls")) && (net.dst.port == 9000)`), + PreserveHost: kong.Bool(true), + }, + ExpressionRoutes: true, + }, + }, + "default.foo-svc2.8080": { + { + Route: kong.Route{ + Name: kong.String("default.foo.1"), + Expression: kong.String(`((net.protocol == "tcp") || (net.protocol == "tls")) && (net.dst.port == 9090)`), + PreserveHost: kong.Bool(true), + }, + ExpressionRoutes: true, + }, + }, + }, + }, + { + name: "tcpingress with sni", + tcpIngresses: []*configurationv1beta1.TCPIngress{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Spec: configurationv1beta1.TCPIngressSpec{ + Rules: []configurationv1beta1.IngressRule{ + { + Host: "www.foo-bar.com", + Port: 9000, + Backend: configurationv1beta1.IngressBackend{ + ServiceName: "foo-svc", + ServicePort: 80, + }, + }, + }, + }, + }, + }, + expectedKongServices: []kongstate.Service{ + { + Service: kong.Service{ + Name: kong.String("default.foo-svc.80"), + }, + Backends: []kongstate.ServiceBackend{ + { + Name: "foo-svc", + PortDef: kongstate.PortDef{Mode: kongstate.PortModeByNumber, Number: int32(80)}, + }, + }, + }, + }, + expectedKongRoutes: map[string][]kongstate.Route{ + "default.foo-svc.80": { + { + Route: kong.Route{ + Name: kong.String("default.foo.0"), + Expression: kong.String(`((net.protocol == "tcp") || (net.protocol == "tls")) && (tls.sni == "www.foo-bar.com") && (net.dst.port == 9000)`), + PreserveHost: kong.Bool(true), + }, + ExpressionRoutes: true, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + fakestore, err := store.NewFakeStore(store.FakeObjects{TCPIngresses: tc.tcpIngresses}) + require.NoError(t, err) + parser := mustNewParser(t, fakestore) + parser.featureFlags.ExpressionRoutes = true + + failureCollector, err := failures.NewResourceFailuresCollector(logrus.New()) + require.NoError(t, err) + parser.failuresCollector = failureCollector + + result := parser.ingressRulesFromTCPIngressV1beta1() + // check services + require.Equal(t, len(tc.expectedKongServices), len(result.ServiceNameToServices), + "should have expected number of services") + for _, expectedKongService := range tc.expectedKongServices { + kongService, ok := result.ServiceNameToServices[*expectedKongService.Name] + require.Truef(t, ok, "should find service %s", expectedKongService.Name) + require.Equal(t, expectedKongService.Backends, kongService.Backends) + // check routes + expectedKongRoutes := tc.expectedKongRoutes[*kongService.Name] + require.Equal(t, len(expectedKongRoutes), len(kongService.Routes)) + + kongRouteNameToRoute := lo.SliceToMap(kongService.Routes, func(r kongstate.Route) (string, kongstate.Route) { + return *r.Name, r + }) + for _, expectedRoute := range expectedKongRoutes { + routeName := expectedRoute.Name + r, ok := kongRouteNameToRoute[*routeName] + require.Truef(t, ok, "should find route %s", *routeName) + require.Equal(t, expectedRoute.Expression, r.Expression) + } + } + // check translation failures + translationFailures := failureCollector.PopResourceFailures() + require.Equal(t, len(tc.expectedFailures), len(translationFailures)) + for _, expectedTranslationFailure := range tc.expectedFailures { + expectedFailureMessage := expectedTranslationFailure.Message() + require.True(t, lo.ContainsBy(translationFailures, func(failure failures.ResourceFailure) bool { + return strings.Contains(failure.Message(), expectedFailureMessage) + })) + } + }) + } +}