diff --git a/CHANGELOG.md b/CHANGELOG.md index 7278abe7b7..9dd0ff2cdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ Adding a new version? You'll need three changes: When installed, it has to be enabled with `ServiceFacade` feature gate. [#5220](https://github.com/Kong/kubernetes-ingress-controller/pull/5220) [#5234](https://github.com/Kong/kubernetes-ingress-controller/pull/5234) + [#5282](https://github.com/Kong/kubernetes-ingress-controller/pull/5282) - Added support for GRPC over HTTP (without TLS) in Gateway API. [#5128](https://github.com/Kong/kubernetes-ingress-controller/pull/5128) - Added `-init-cache-sync-duration` CLI flag. This flag configures how long the controller waits for Kubernetes resources to populate at startup before generating the initial Kong configuration. It also fixes a bug that removed the default 5 second wait period. diff --git a/internal/dataplane/translator/ingressrules.go b/internal/dataplane/translator/ingressrules.go index 3161fe117a..8f826b6f0d 100644 --- a/internal/dataplane/translator/ingressrules.go +++ b/internal/dataplane/translator/ingressrules.go @@ -61,7 +61,14 @@ func (ir *ingressRules) populateServices(logger logr.Logger, s store.Storer, fai // collect all the Kubernetes services configured for the service backends, // and all annotations with our prefix in use across all services (when applicable). - k8sServices, seenKongAnnotations := getK8sServicesForBackends(logger, s, service.Namespace, service.Backends) + serviceParent := ir.ServiceNameToParent[key] + k8sServices, seenKongAnnotations := getK8sServicesForBackends( + s, + service.Namespace, + service.Backends, + failuresCollector, + serviceParent, + ) // if the Kubernetes services have been deemed invalid, log an error message // and skip the current service. @@ -258,10 +265,11 @@ func (s SNIs) Hosts() []string { } func getK8sServicesForBackends( - log logr.Logger, storer store.Storer, namespace string, backends kongstate.ServiceBackends, + failuresCollector *failures.ResourceFailuresCollector, + parent client.Object, ) ([]*corev1.Service, map[string]string) { // we collect all annotations seen for this group of services so that these // can be later validated. @@ -273,7 +281,7 @@ func getK8sServicesForBackends( for _, backend := range backends { k8sService, err := resolveKubernetesServiceForBackend(storer, namespace, backend) if err != nil { - log.Error(err, "Failed to resolve Kubernetes Service for backend") + failuresCollector.PushResourceFailure(fmt.Sprintf("failed to resolve Kubernetes Service for backend: %s", err), parent) continue } if k8sService != nil { diff --git a/internal/dataplane/translator/ingressrules_test.go b/internal/dataplane/translator/ingressrules_test.go index 2610144978..01ea970d48 100644 --- a/internal/dataplane/translator/ingressrules_test.go +++ b/internal/dataplane/translator/ingressrules_test.go @@ -3,6 +3,7 @@ package translator import ( "testing" + "github.com/go-logr/logr" "github.com/go-logr/zapr" "github.com/kong/go-kong/kong" "github.com/samber/lo" @@ -252,7 +253,7 @@ func TestGetK8sServicesForBackends(t *testing.T) { services []*corev1.Service expectedServices []*corev1.Service expectedAnnotations map[string]string - expectedLogEntries []string + expectedFailures []string }{ { name: "if all backends have a service then all services will be returned and their annotations recorded", @@ -333,24 +334,29 @@ func TestGetK8sServicesForBackends(t *testing.T) { }, }}, expectedAnnotations: map[string]string{}, - expectedLogEntries: []string{ - "Failed to resolve Kubernetes Service for backend", + expectedFailures: []string{ + "failed to resolve Kubernetes Service for backend: failed to fetch Service default/test-service2: Service default/test-service2 not found", }, }, } { t.Run(tt.name, func(t *testing.T) { + parent := &netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{Name: "ingress", Namespace: tt.namespace}, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: netv1.SchemeGroupVersion.String()}, + } storer, err := store.NewFakeStore(store.FakeObjects{Services: tt.services}) require.NoError(t, err) - core, logs := observer.New(zap.InfoLevel) - logger := zapr.NewLogger(zap.New(core)) + failuresCollector := failures.NewResourceFailuresCollector(logr.Discard()) - services, annotations := getK8sServicesForBackends(logger, storer, tt.namespace, tt.backends) + services, annotations := getK8sServicesForBackends(storer, tt.namespace, tt.backends, failuresCollector, parent) assert.Equal(t, tt.expectedServices, services) assert.Equal(t, tt.expectedAnnotations, annotations) - for i, expectedLogEntry := range tt.expectedLogEntries { - assert.Contains(t, logs.All()[i].Entry.Message, expectedLogEntry) + var collectedFailures []string + for _, failure := range failuresCollector.PopResourceFailures() { + collectedFailures = append(collectedFailures, failure.Message()) } + assert.Equal(t, tt.expectedFailures, collectedFailures) }) } } diff --git a/internal/dataplane/translator/subtranslator/ingress.go b/internal/dataplane/translator/subtranslator/ingress.go index 11152c1d73..00143f0d3a 100644 --- a/internal/dataplane/translator/subtranslator/ingress.go +++ b/internal/dataplane/translator/subtranslator/ingress.go @@ -184,7 +184,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 @@ -210,7 +210,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/testdata/golden/kong-service-facade/expression-routes-on_golden.yaml b/internal/dataplane/translator/testdata/golden/kong-service-facade/expression-routes-on_golden.yaml index fb9e30f1ca..3effa245da 100644 --- a/internal/dataplane/translator/testdata/golden/kong-service-facade/expression-routes-on_golden.yaml +++ b/internal/dataplane/translator/testdata/golden/kong-service-facade/expression-routes-on_golden.yaml @@ -17,6 +17,38 @@ plugins: - k8s-group:configuration.konghq.com - k8s-version:v1 services: +- connect_timeout: 60000 + host: default.svc-facade-default.svc.facade + id: 3f003a09-c285-52aa-a944-c6e156c71e36 + name: default.svc-facade-default.svc.facade + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + routes: + - expression: (http.path ^= "/") && ((net.protocol == "http") || (net.protocol == + "https")) + https_redirect_status_code: 426 + id: 95255daa-88f8-504b-9098-9300d404c741 + name: default.beta + preserve_host: true + priority: 0 + request_buffering: true + response_buffering: true + strip_path: false + tags: + - k8s-name:beta + - k8s-namespace:default + - k8s-kind:Ingress + - k8s-group:networking.k8s.io + - k8s-version:v1 + tags: + - k8s-name:svc-facade-default + - k8s-namespace:default + - k8s-kind:KongServiceFacade + - k8s-group:incubator.konghq.com + - k8s-version:v1alpha1 + write_timeout: 60000 - connect_timeout: 60000 host: default.svc-facade-beta.svc.facade id: 0487ced6-2552-5ccb-905d-db086a449a6c @@ -82,6 +114,16 @@ services: - k8s-version:v1alpha1 write_timeout: 60000 upstreams: +- algorithm: round-robin + name: default.svc-facade-default.svc.facade + tags: + - k8s-name:svc-facade-default + - k8s-namespace:default + - k8s-kind:KongServiceFacade + - k8s-group:incubator.konghq.com + - k8s-version:v1alpha1 + targets: + - target: 10.244.0.5:80 - algorithm: round-robin name: default.svc-facade-beta.svc.facade tags: diff --git a/internal/dataplane/translator/testdata/golden/kong-service-facade/feature-flag-on_golden.yaml b/internal/dataplane/translator/testdata/golden/kong-service-facade/feature-flag-on_golden.yaml index e31bedb1f6..75582b1d37 100644 --- a/internal/dataplane/translator/testdata/golden/kong-service-facade/feature-flag-on_golden.yaml +++ b/internal/dataplane/translator/testdata/golden/kong-service-facade/feature-flag-on_golden.yaml @@ -17,6 +17,42 @@ plugins: - k8s-group:configuration.konghq.com - k8s-version:v1 services: +- connect_timeout: 60000 + host: default.svc-facade-default.svc.facade + id: 3f003a09-c285-52aa-a944-c6e156c71e36 + name: default.svc-facade-default.svc.facade + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + routes: + - https_redirect_status_code: 426 + id: 95255daa-88f8-504b-9098-9300d404c741 + name: default.beta + path_handling: v0 + paths: + - / + preserve_host: true + protocols: + - http + - https + regex_priority: 0 + request_buffering: true + response_buffering: true + strip_path: false + tags: + - k8s-name:beta + - k8s-namespace:default + - k8s-kind:Ingress + - k8s-group:networking.k8s.io + - k8s-version:v1 + tags: + - k8s-name:svc-facade-default + - k8s-namespace:default + - k8s-kind:KongServiceFacade + - k8s-group:incubator.konghq.com + - k8s-version:v1alpha1 + write_timeout: 60000 - connect_timeout: 60000 host: default.svc-facade-beta.svc.facade id: 0487ced6-2552-5ccb-905d-db086a449a6c @@ -92,6 +128,16 @@ services: - k8s-version:v1alpha1 write_timeout: 60000 upstreams: +- algorithm: round-robin + name: default.svc-facade-default.svc.facade + tags: + - k8s-name:svc-facade-default + - k8s-namespace:default + - k8s-kind:KongServiceFacade + - k8s-group:incubator.konghq.com + - k8s-version:v1alpha1 + targets: + - target: 10.244.0.5:80 - algorithm: round-robin name: default.svc-facade-beta.svc.facade tags: diff --git a/internal/dataplane/translator/testdata/golden/kong-service-facade/in.yaml b/internal/dataplane/translator/testdata/golden/kong-service-facade/in.yaml index aebdad05d4..16285a77ab 100644 --- a/internal/dataplane/translator/testdata/golden/kong-service-facade/in.yaml +++ b/internal/dataplane/translator/testdata/golden/kong-service-facade/in.yaml @@ -34,6 +34,11 @@ spec: name: svc-facade-beta path: /beta pathType: Exact + defaultBackend: + resource: + apiGroup: incubator.konghq.com + kind: KongServiceFacade + name: svc-facade-default --- apiVersion: v1 kind: Service @@ -92,6 +97,18 @@ spec: name: svc port: 80 --- +apiVersion: incubator.konghq.com/v1alpha1 +kind: KongServiceFacade +metadata: + annotations: + kubernetes.io/ingress.class: kong + name: svc-facade-default + namespace: default +spec: + backendRef: + name: svc + port: 80 +--- apiVersion: configuration.konghq.com/v1 kind: KongPlugin metadata: diff --git a/internal/dataplane/translator/translate_ingress.go b/internal/dataplane/translator/translate_ingress.go index 4bd8c5b939..85b2874162 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,136 @@ 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: unsupported 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), + // We do not populate Service's Tags field here because it would get overridden anyway later in the + // Translator pipeline (see ingressRules.generateKongServiceTags). + }, + 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), + // We do not populate Service's Tags field here because it would get overridden anyway later in the + // Translator pipeline (see ingressRules.generateKongServiceTags). + }, + 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..09df29f401 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,122 @@ 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`}, + }, + { + name: "ingress with default backend resource unknown", + ingresses: []netv1.Ingress{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo-namespace", + }, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"}, + Spec: netv1.IngressSpec{ + DefaultBackend: &netv1.IngressBackend{ + Resource: &corev1.TypedLocalObjectReference{ + APIGroup: lo.ToPtr("unknown.group.com"), + Kind: "UnknownKind", + }, + }, + }, + }}, + expectedHaveBackendService: false, + expectedFailures: []string{"default backend: unsupported resource type unknown.group.com/UnknownKind"}, + }, } 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/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go index f344023a11..482dc75b03 100644 --- a/internal/dataplane/translator/translator_test.go +++ b/internal/dataplane/translator/translator_test.go @@ -31,6 +31,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/util/builder" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" + incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1" "github.com/kong/kubernetes-ingress-controller/v3/test/helpers/certificate" ) @@ -2363,7 +2364,6 @@ func TestKongServiceAnnotations(t *testing.T) { } func TestDefaultBackend(t *testing.T) { - assert := assert.New(t) t.Run("default backend is processed correctly", func(t *testing.T) { ingresses := []*netv1.Ingress{ { @@ -2374,6 +2374,7 @@ func TestDefaultBackend(t *testing.T) { annotations.IngressClassKey: annotations.DefaultIngressClass, }, }, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: netv1.SchemeGroupVersion.String()}, Spec: netv1.IngressSpec{ DefaultBackend: &netv1.IngressBackend{ Service: &netv1.IngressServiceBackend{ @@ -2393,6 +2394,14 @@ func TestDefaultBackend(t *testing.T) { Name: "default-svc", Namespace: "default", }, + TypeMeta: metav1.TypeMeta{Kind: "Service", APIVersion: corev1.SchemeGroupVersion.String()}, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, }, } store, err := store.NewFakeStore(store.FakeObjects{ @@ -2406,12 +2415,20 @@ func TestDefaultBackend(t *testing.T) { state := result.KongState require.NotNil(t, state) require.Len(t, state.Services, 1, "expected one service to be rendered") - assert.Equal("default.default-svc.80", *state.Services[0].Name) - assert.Equal("default-svc.default.80.svc", *state.Services[0].Host) - assert.Equal(1, len(state.Services[0].Routes), + service := state.Services[0] + assert.Equal(t, "default.default-svc.80", *service.Name) + assert.Equal(t, "default-svc.default.80.svc", *service.Host) + assert.Equal(t, 1, len(service.Routes), "expected one routes to be rendered") - assert.Equal("default.ing-with-default-backend", *state.Services[0].Routes[0].Name) - assert.Equal("/", *state.Services[0].Routes[0].Paths[0]) + route := service.Routes[0] + assert.Equal(t, "default.ing-with-default-backend", *route.Name) + assert.Equal(t, "/", *route.Paths[0]) + assert.ElementsMatch(t, []*string{ + lo.ToPtr("k8s-name:default-svc"), + lo.ToPtr("k8s-namespace:default"), + lo.ToPtr("k8s-kind:Service"), + lo.ToPtr("k8s-version:v1"), + }, service.Tags, "tags are populated with Service as a parent") }) t.Run("client-cert secret doesn't exist", func(t *testing.T) { @@ -2479,11 +2496,67 @@ func TestDefaultBackend(t *testing.T) { require.Len(t, result.TranslationFailures, 1) state := result.KongState require.NotNil(t, state) - assert.Equal(0, len(state.Certificates), + assert.Equal(t, 0, len(state.Certificates), "expected no certificates to be rendered") - assert.Equal(1, len(state.Services)) - assert.Nil(state.Services[0].ClientCertificate) + assert.Equal(t, 1, len(state.Services)) + assert.Nil(t, state.Services[0].ClientCertificate) + }) + + t.Run("KongServiceFacade used as a backend", func(t *testing.T) { + storer := lo.Must(store.NewFakeStore(store.FakeObjects{ + IngressesV1: []*netv1.Ingress{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: netv1.SchemeGroupVersion.String()}, + Spec: netv1.IngressSpec{ + IngressClassName: lo.ToPtr(annotations.DefaultIngressClass), + DefaultBackend: &netv1.IngressBackend{ + Resource: &corev1.TypedLocalObjectReference{ + APIGroup: lo.ToPtr(incubatorv1alpha1.GroupVersion.Group), + Kind: incubatorv1alpha1.KongServiceFacadeKind, + Name: "foo-facade", + }, + }, + }, + }}, + Services: []*corev1.Service{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc", + Namespace: "default", + }, + }}, + KongServiceFacades: []*incubatorv1alpha1.KongServiceFacade{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-facade", + Namespace: "default", + }, + TypeMeta: metav1.TypeMeta{Kind: incubatorv1alpha1.KongServiceFacadeKind, APIVersion: incubatorv1alpha1.GroupVersion.String()}, + Spec: incubatorv1alpha1.KongServiceFacadeSpec{ + Backend: incubatorv1alpha1.KongServiceFacadeBackend{ + Name: "foo-svc", + Port: 80, + }, + }, + }}, + })) + + translator := mustNewTranslator(t, storer) + translator.featureFlags.KongServiceFacade = true + result := translator.BuildKongConfig() + require.Empty(t, result.TranslationFailures) + require.Len(t, result.KongState.Services, 1) + service := result.KongState.Services[0] + assert.Equal(t, "default.foo-facade.svc.facade", *service.Name) + assert.ElementsMatch(t, []*string{ + lo.ToPtr("k8s-name:foo-facade"), + lo.ToPtr("k8s-namespace:default"), + lo.ToPtr("k8s-kind:KongServiceFacade"), + lo.ToPtr("k8s-group:incubator.konghq.com"), + lo.ToPtr("k8s-version:v1alpha1"), + }, service.Tags, "tags are populated with KongServiceFacade as a parent") }) } @@ -4744,6 +4817,7 @@ func mustNewTranslator(t *testing.T, storer store.Storer) *Translator { // We'll assume these are true for all tests. FillIDs: true, ReportConfiguredKubernetesObjects: true, + KongServiceFacade: true, }, ) require.NoError(t, err) 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/integration/isolated/ingress_test.go b/test/integration/isolated/ingress_test.go index 59dd28961e..d1aedd71a6 100644 --- a/test/integration/isolated/ingress_test.go +++ b/test/integration/isolated/ingress_test.go @@ -277,6 +277,23 @@ func TestIngress_KongServiceFacadeAsBackend(t *testing.T) { }, }, }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "httpbin-facade-svg", + Annotations: map[string]string{ + // We'll use this annotation to verify that the default backend is used when no path matches. + // httpbin's /anything should return anything passed in the request data. + annotations.AnnotationPrefix + annotations.PathKey: "/anything", + annotations.IngressClassKey: ingressClass, + }, + }, + Spec: incubatorv1alpha1.KongServiceFacadeSpec{ + Backend: incubatorv1alpha1.KongServiceFacadeBackend{ + Name: service.Name, + Port: test.HTTPBinPort, + }, + }, + }, } clients := GetFromCtxForT[*clientset.Clientset](ctx, t) for _, serviceFacade := range serviceFacades { @@ -319,7 +336,15 @@ func TestIngress_KongServiceFacadeAsBackend(t *testing.T) { }). WithAnnotations(map[string]string{ annotations.AnnotationPrefix + annotations.StripPathKey: "true", - }).Build() + }). + WithDefaultBackend(&netv1.IngressBackend{ + Resource: &corev1.TypedLocalObjectReference{ + APIGroup: lo.ToPtr(incubatorv1alpha1.SchemeGroupVersion.Group), + Kind: incubatorv1alpha1.KongServiceFacadeKind, + Name: serviceFacades[2].Name, + }, + }). + Build() ingress, err = cluster.Client().NetworkingV1().Ingresses(namespace).Create(ctx, ingress, metav1.CreateOptions{}) require.NoError(t, err) cleaner.Add(ingress) @@ -346,6 +371,11 @@ func TestIngress_KongServiceFacadeAsBackend(t *testing.T) { expectContent(jpegIngressPath, testconsts.JPEGMagicNumber) expectContent(pngIngressPath, testconsts.PNGMagicNumber) + // This is testing that the default backend is used when no path matches. httpbin's /anything should return + // the used path in response. + const nonExistingPath = "/path/that/does/not/exist" + expectContent(nonExistingPath, nonExistingPath) + return ctx }). Teardown(featureTeardown()) diff --git a/test/integration/isolated/suite_test.go b/test/integration/isolated/suite_test.go index d66271e9af..acb2f43291 100644 --- a/test/integration/isolated/suite_test.go +++ b/test/integration/isolated/suite_test.go @@ -243,7 +243,7 @@ func featureSetup(opts ...featureSetupOpt) func(ctx context.Context, t *testing. } return ok - }, time.Minute, 100*time.Millisecond, "failed waiting for kong addon to become ready") { + }, time.Minute*3, 100*time.Millisecond, "failed waiting for kong addon to become ready") { return ctx } diff --git a/test/internal/helpers/http.go b/test/internal/helpers/http.go index cf19762a6a..ed786eb9df 100644 --- a/test/internal/helpers/http.go +++ b/test/internal/helpers/http.go @@ -118,7 +118,11 @@ func EventuallyGETPath( n, err := b.ReadFrom(resp.Body) require.NoError(t, err) require.True(t, n > 0) - return strings.Contains(b.String(), bodyContents) + if !strings.Contains(b.String(), bodyContents) { + t.Logf("WARNING: http response body does not contain expected contents: %s, actual: \n%s", bodyContents, b.String()) + return false + } + return true } return false }, waitDuration, waitTick)