diff --git a/internal/dataplane/translator/subtranslator/ingress.go b/internal/dataplane/translator/subtranslator/ingress.go index ac898f4f65..298e73db93 100644 --- a/internal/dataplane/translator/subtranslator/ingress.go +++ b/internal/dataplane/translator/subtranslator/ingress.go @@ -183,7 +183,7 @@ func (i *ingressTranslationIndex) getIngressPathBackend(namespace string, httpIn } if resource := httpIngressPath.Backend.Resource; resource != nil { - if !isKongServiceFacade(resource) { + if !IsKongServiceFacade(resource) { gk := resource.Kind if resource.APIGroup != nil { gk = *resource.APIGroup + "/" + gk @@ -209,7 +209,8 @@ func (i *ingressTranslationIndex) getIngressPathBackend(namespace string, httpIn return ingressTranslationMetaBackend{}, fmt.Errorf("no Service or Resource specified for Ingress path") } -func isKongServiceFacade(resource *corev1.TypedLocalObjectReference) bool { +// IsKongServiceFacade returns true if the given resource reference is a KongServiceFacade. +func IsKongServiceFacade(resource *corev1.TypedLocalObjectReference) bool { return resource.Kind == incubatorv1alpha1.KongServiceFacadeKind && resource.APIGroup != nil && *resource.APIGroup == incubatorv1alpha1.GroupVersion.Group } diff --git a/internal/dataplane/translator/translate_ingress.go b/internal/dataplane/translator/translate_ingress.go index 4bd8c5b939..4dbec4c31d 100644 --- a/internal/dataplane/translator/translate_ingress.go +++ b/internal/dataplane/translator/translate_ingress.go @@ -6,11 +6,14 @@ import ( "sort" "github.com/kong/go-kong/kong" + corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" + "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/failures" "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/kongstate" "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/atc" "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/subtranslator" + "github.com/kong/kubernetes-ingress-controller/v3/internal/manager/featuregates" "github.com/kong/kubernetes-ingress-controller/v3/internal/store" "github.com/kong/kubernetes-ingress-controller/v3/internal/util" ) @@ -66,7 +69,7 @@ func (t *Translator) ingressRulesFromIngressV1() ingressRules { } // Add a default backend if it exists. - defaultBackendService, ok := getDefaultBackendService(allDefaultBackends, t.featureFlags.ExpressionRoutes) + defaultBackendService, ok := getDefaultBackendService(t.storer, t.failuresCollector, allDefaultBackends, t.featureFlags) if ok { // When such service would overwrite an existing service, merge the routes. if svc, ok := result.ServiceNameToServices[*defaultBackendService.Name]; ok { @@ -81,53 +84,133 @@ func (t *Translator) ingressRulesFromIngressV1() ingressRules { } // getDefaultBackendService picks the oldest Ingress with a DefaultBackend defined and returns a Kong Service for it. -func getDefaultBackendService(allDefaultBackends []netv1.Ingress, expressionRoutes bool) (kongstate.Service, bool) { +func getDefaultBackendService( + storer store.Storer, + failuresCollector *failures.ResourceFailuresCollector, + allDefaultBackends []netv1.Ingress, + features FeatureFlags, +) (kongstate.Service, bool) { + // Sort the default backends by creation timestamp, so that the oldest one is picked. sort.SliceStable(allDefaultBackends, func(i, j int) bool { return allDefaultBackends[i].CreationTimestamp.Before(&allDefaultBackends[j].CreationTimestamp) }) if len(allDefaultBackends) > 0 { ingress := allDefaultBackends[0] - defaultBackend := allDefaultBackends[0].Spec.DefaultBackend - port := subtranslator.PortDefFromServiceBackendPort(&defaultBackend.Service.Port) - serviceName := fmt.Sprintf( - "%s.%s.%s", - allDefaultBackends[0].Namespace, - defaultBackend.Service.Name, - port.CanonicalString(), - ) - service := kongstate.Service{ - Service: kong.Service{ - Name: kong.String(serviceName), - Host: kong.String(fmt.Sprintf( - "%s.%s.%s.svc", - defaultBackend.Service.Name, - ingress.Namespace, - port.CanonicalString(), - )), - Port: kong.Int(DefaultHTTPPort), - Protocol: kong.String("http"), - ConnectTimeout: kong.Int(DefaultServiceTimeout), - ReadTimeout: kong.Int(DefaultServiceTimeout), - WriteTimeout: kong.Int(DefaultServiceTimeout), - Retries: kong.Int(DefaultRetries), - Tags: util.GenerateTagsForObject(&ingress), - }, - Namespace: ingress.Namespace, - Backends: []kongstate.ServiceBackend{{ - Name: defaultBackend.Service.Name, - PortDef: port, - }}, - Parent: &ingress, + defaultBackend := ingress.Spec.DefaultBackend + route := translateIngressDefaultBackendRoute(&ingress, util.GenerateTagsForObject(&ingress), features.ExpressionRoutes) + + // If the default backend is defined as an arbitrary resource, then we need handle it differently. + if resource := defaultBackend.Resource; resource != nil { + return translateIngressDefaultBackendResource( + resource, + ingress, + route, + storer, + failuresCollector, + features, + ) } - r := translateIngressDefaultBackendRoute(&ingress, util.GenerateTagsForObject(&ingress), expressionRoutes) - service.Routes = append(service.Routes, *r) - return service, true + + // Otherwise, the default backend is defined as a Kubernetes Service. + return translateIngressDefaultBackendService(ingress, route) } return kongstate.Service{}, false } +func translateIngressDefaultBackendResource( + resource *corev1.TypedLocalObjectReference, + ingress netv1.Ingress, + route *kongstate.Route, + storer store.Storer, + failuresCollector *failures.ResourceFailuresCollector, + features FeatureFlags, +) (kongstate.Service, bool) { + if !subtranslator.IsKongServiceFacade(resource) { + gk := resource.Kind + if resource.APIGroup != nil { + gk = *resource.APIGroup + "/" + gk + } + failuresCollector.PushResourceFailure(fmt.Sprintf("default backend: unknown resource type %s", gk), &ingress) + return kongstate.Service{}, false + } + if !features.KongServiceFacade { + failuresCollector.PushResourceFailure( + fmt.Sprintf("default backend: KongServiceFacade is not enabled, please set the %q feature gate to 'true' to enable it", featuregates.KongServiceFacade), + &ingress, + ) + return kongstate.Service{}, false + } + facade, err := storer.GetKongServiceFacade(ingress.Namespace, resource.Name) + if err != nil { + failuresCollector.PushResourceFailure( + fmt.Sprintf("default backend: KongServiceFacade %q could not be fetched: %s", resource.Name, err), + &ingress, + ) + return kongstate.Service{}, false + } + + serviceName := fmt.Sprintf("%s.%s.svc.facade", ingress.Namespace, resource.Name) + return kongstate.Service{ + Service: kong.Service{ + Name: kong.String(serviceName), + Host: kong.String(serviceName), + Port: kong.Int(DefaultHTTPPort), + Protocol: kong.String("http"), + ConnectTimeout: kong.Int(DefaultServiceTimeout), + ReadTimeout: kong.Int(DefaultServiceTimeout), + WriteTimeout: kong.Int(DefaultServiceTimeout), + Retries: kong.Int(DefaultRetries), + }, + Namespace: ingress.Namespace, + Backends: []kongstate.ServiceBackend{{ + Type: kongstate.ServiceBackendTypeKongServiceFacade, + Name: resource.Name, + Namespace: ingress.Namespace, + PortDef: subtranslator.PortDefFromPortNumber(facade.Spec.Backend.Port), + }}, + Parent: facade, + Routes: []kongstate.Route{*route}, + }, true +} + +func translateIngressDefaultBackendService(ingress netv1.Ingress, route *kongstate.Route) (kongstate.Service, bool) { + defaultBackend := ingress.Spec.DefaultBackend + port := subtranslator.PortDefFromServiceBackendPort(&defaultBackend.Service.Port) + serviceName := fmt.Sprintf( + "%s.%s.%s", + ingress.Namespace, + defaultBackend.Service.Name, + port.CanonicalString(), + ) + return kongstate.Service{ + Service: kong.Service{ + Name: kong.String(serviceName), + Host: kong.String(fmt.Sprintf( + "%s.%s.%s.svc", + defaultBackend.Service.Name, + ingress.Namespace, + port.CanonicalString(), + )), + Port: kong.Int(DefaultHTTPPort), + Protocol: kong.String("http"), + ConnectTimeout: kong.Int(DefaultServiceTimeout), + ReadTimeout: kong.Int(DefaultServiceTimeout), + WriteTimeout: kong.Int(DefaultServiceTimeout), + Retries: kong.Int(DefaultRetries), + Tags: util.GenerateTagsForObject(&ingress), + }, + Namespace: ingress.Namespace, + Backends: []kongstate.ServiceBackend{{ + Name: defaultBackend.Service.Name, + PortDef: port, + }}, + Parent: &ingress, + Routes: []kongstate.Route{*route}, + }, true +} + func translateIngressDefaultBackendRoute(ingress *netv1.Ingress, tags []*string, expressionRoutes bool) *kongstate.Route { r := &kongstate.Route{ Ingress: util.FromK8sObject(ingress), diff --git a/internal/dataplane/translator/translate_ingress_test.go b/internal/dataplane/translator/translate_ingress_test.go index b057b51f0b..3fce326bfc 100644 --- a/internal/dataplane/translator/translate_ingress_test.go +++ b/internal/dataplane/translator/translate_ingress_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/go-logr/logr" "github.com/kong/go-kong/kong" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -14,9 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/kubernetes-ingress-controller/v3/internal/annotations" + "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/failures" "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/kongstate" "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/translator/subtranslator" "github.com/kong/kubernetes-ingress-controller/v3/internal/store" + incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1" ) func TestFromIngressV1(t *testing.T) { @@ -153,13 +156,14 @@ func TestFromIngressV1(t *testing.T) { } func TestGetDefaultBackendService(t *testing.T) { - someIngress := func(creationTimestamp time.Time, serviceName string) netv1.Ingress { + ingressWithDefaultBackendService := func(creationTimestamp time.Time, serviceName string) netv1.Ingress { return netv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "foo-namespace", CreationTimestamp: metav1.NewTime(creationTimestamp), }, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"}, Spec: netv1.IngressSpec{ DefaultBackend: &netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ @@ -170,40 +174,60 @@ func TestGetDefaultBackendService(t *testing.T) { }, } } + ingressWithDefaultBackendKongServiceFacade := func(creationTimestamp time.Time, serviceFacadeName string) netv1.Ingress { + return netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo-namespace", + CreationTimestamp: metav1.NewTime(creationTimestamp), + }, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"}, + Spec: netv1.IngressSpec{ + DefaultBackend: &netv1.IngressBackend{ + Resource: &corev1.TypedLocalObjectReference{ + APIGroup: lo.ToPtr(incubatorv1alpha1.GroupVersion.Group), + Kind: incubatorv1alpha1.KongServiceFacadeKind, + Name: serviceFacadeName, + }, + Service: &netv1.IngressServiceBackend{}, + }, + }, + } + } now := time.Now() testCases := []struct { name string ingresses []netv1.Ingress - expressionRoutes bool + featureFlags FeatureFlags + storerObjects store.FakeObjects expectedHaveBackendService bool + expectedFailures []string expectedServiceName string expectedServiceHost string }{ { name: "no ingresses", ingresses: []netv1.Ingress{}, - expressionRoutes: false, expectedHaveBackendService: false, }, { name: "no ingresses with expression routes", ingresses: []netv1.Ingress{}, - expressionRoutes: true, + featureFlags: FeatureFlags{ExpressionRoutes: true}, expectedHaveBackendService: false, }, { name: "one ingress with default backend", - ingresses: []netv1.Ingress{someIngress(now, "foo-svc")}, - expressionRoutes: false, + ingresses: []netv1.Ingress{ingressWithDefaultBackendService(now, "foo-svc")}, expectedHaveBackendService: true, expectedServiceName: "foo-namespace.foo-svc.80", expectedServiceHost: "foo-svc.foo-namespace.80.svc", }, { name: "one ingress with default backend and expression routes enabled", - ingresses: []netv1.Ingress{someIngress(now, "foo-svc")}, - expressionRoutes: true, + ingresses: []netv1.Ingress{ingressWithDefaultBackendService(now, "foo-svc")}, + featureFlags: FeatureFlags{ExpressionRoutes: true}, expectedHaveBackendService: true, expectedServiceName: "foo-namespace.foo-svc.80", expectedServiceHost: "foo-svc.foo-namespace.80.svc", @@ -211,10 +235,9 @@ func TestGetDefaultBackendService(t *testing.T) { { name: "multiple ingresses with default backend", ingresses: []netv1.Ingress{ - someIngress(now.Add(time.Second), "newer"), - someIngress(now, "older"), + ingressWithDefaultBackendService(now.Add(time.Second), "newer"), + ingressWithDefaultBackendService(now, "older"), }, - expressionRoutes: false, expectedHaveBackendService: true, expectedServiceName: "foo-namespace.older.80", expectedServiceHost: "older.foo-namespace.80.svc", @@ -222,27 +245,102 @@ func TestGetDefaultBackendService(t *testing.T) { { name: "multiple ingresses with default backend and expression routes enabled", ingresses: []netv1.Ingress{ - someIngress(now.Add(time.Second), "newer"), - someIngress(now, "older"), + ingressWithDefaultBackendService(now.Add(time.Second), "newer"), + ingressWithDefaultBackendService(now, "older"), }, - expressionRoutes: true, + featureFlags: FeatureFlags{ExpressionRoutes: true}, expectedHaveBackendService: true, expectedServiceName: "foo-namespace.older.80", expectedServiceHost: "older.foo-namespace.80.svc", }, + { + name: "ingress with default backend kong service facade", + ingresses: []netv1.Ingress{ + ingressWithDefaultBackendKongServiceFacade(now, "foo-svc-facade"), + }, + featureFlags: FeatureFlags{KongServiceFacade: true}, + storerObjects: store.FakeObjects{ + KongServiceFacades: []*incubatorv1alpha1.KongServiceFacade{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc-facade", + Namespace: "foo-namespace", + }, + Spec: incubatorv1alpha1.KongServiceFacadeSpec{ + Backend: incubatorv1alpha1.KongServiceFacadeBackend{ + Name: "foo-svc", + Port: 8080, + }, + }, + }}, + }, + expectedHaveBackendService: true, + expectedServiceName: "foo-namespace.foo-svc-facade.svc.facade", + expectedServiceHost: "foo-namespace.foo-svc-facade.svc.facade", + }, + { + name: "ingress with default backend kong service facade and expression routes enabled", + ingresses: []netv1.Ingress{ + ingressWithDefaultBackendKongServiceFacade(now, "foo-svc-facade"), + }, + featureFlags: FeatureFlags{ + KongServiceFacade: true, + ExpressionRoutes: true, + }, + storerObjects: store.FakeObjects{ + KongServiceFacades: []*incubatorv1alpha1.KongServiceFacade{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc-facade", + Namespace: "foo-namespace", + }, + Spec: incubatorv1alpha1.KongServiceFacadeSpec{ + Backend: incubatorv1alpha1.KongServiceFacadeBackend{ + Name: "foo-svc", + Port: 8080, + }, + }, + }}, + }, + expectedHaveBackendService: true, + expectedServiceName: "foo-namespace.foo-svc-facade.svc.facade", + expectedServiceHost: "foo-namespace.foo-svc-facade.svc.facade", + }, + { + name: "ingress with default backend kong service facade and no feature flag enabled", + ingresses: []netv1.Ingress{ + ingressWithDefaultBackendKongServiceFacade(now, "foo-svc-facade"), + }, + expectedHaveBackendService: false, + expectedFailures: []string{`default backend: KongServiceFacade is not enabled, please set the "KongServiceFacade" feature gate to 'true' to enable it`}, + }, + { + name: "ingress with default non existing backend kong service facade", + ingresses: []netv1.Ingress{ + ingressWithDefaultBackendKongServiceFacade(now, "foo-svc-facade"), + }, + featureFlags: FeatureFlags{KongServiceFacade: true}, + expectedHaveBackendService: false, + expectedFailures: []string{`default backend: KongServiceFacade "foo-svc-facade" could not be fetched: KongServiceFacade foo-namespace/foo-svc-facade not found`}, + }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - svc, ok := getDefaultBackendService(tc.ingresses, tc.expressionRoutes) + storer := lo.Must(store.NewFakeStore(tc.storerObjects)) + failuresCollector := failures.NewResourceFailuresCollector(logr.Discard()) + svc, ok := getDefaultBackendService(storer, failuresCollector, tc.ingresses, tc.featureFlags) require.Equal(t, tc.expectedHaveBackendService, ok) + var gotFailures []string + for _, failure := range failuresCollector.PopResourceFailures() { + gotFailures = append(gotFailures, failure.Message()) + } + require.Equal(t, tc.expectedFailures, gotFailures) if tc.expectedHaveBackendService { require.Equal(t, tc.expectedServiceName, *svc.Name) require.Equal(t, tc.expectedServiceHost, *svc.Host) require.Len(t, svc.Routes, 1) route := svc.Routes[0] - if tc.expressionRoutes { + if tc.featureFlags.ExpressionRoutes { require.Equal(t, `(http.path ^= "/") && ((net.protocol == "http") || (net.protocol == "https"))`, *route.Expression) require.Equal(t, subtranslator.IngressDefaultBackendPriority, *route.Priority) } else { diff --git a/internal/util/builder/ingress.go b/internal/util/builder/ingress.go index 0a74decc1c..e9e945c888 100644 --- a/internal/util/builder/ingress.go +++ b/internal/util/builder/ingress.go @@ -64,3 +64,8 @@ func (b *IngressBuilder) WithAnnotations(annotations map[string]string) *Ingress } return b } + +func (b *IngressBuilder) WithDefaultBackend(backend *netv1.IngressBackend) *IngressBuilder { + b.ingress.Spec.DefaultBackend = backend + return b +} diff --git a/test/consts/magic_numbers.go b/test/consts/magic_numbers.go index 3f0a5da4d0..6398a60343 100644 --- a/test/consts/magic_numbers.go +++ b/test/consts/magic_numbers.go @@ -8,4 +8,8 @@ const ( // PNGMagicNumber is the magic number that identifies a PNG file. This can be used to identify the file type // returned from HTTPBin backends when using `/image/*` endpoints. PNGMagicNumber = "\x89PNG\r\n\x1a\n" + + // SVGMagicNumber is the magic number that identifies a SVG file. This can be used to identify the file type + // returned from HTTPBin backends when using `/image/*` endpoints. + SVGMagicNumber = "