From 1a15c2fc7a8d0daa1203245e28f9890cbcd45035 Mon Sep 17 00:00:00 2001 From: Menekse Ceylan Date: Wed, 27 May 2026 14:06:20 +0200 Subject: [PATCH 1/5] fix tls bridging missing tlspool config and add tests and fix typo in tls annotations --- pkg/alb/ingress/alb_spec.go | 20 +++ pkg/alb/ingress/alb_spec_test.go | 268 ++++++++++++++++++------------- pkg/alb/ingress/annotations.go | 6 +- pkg/alb/ingress/resources.go | 6 +- 4 files changed, 182 insertions(+), 118 deletions(-) diff --git a/pkg/alb/ingress/alb_spec.go b/pkg/alb/ingress/alb_spec.go index 128bfe71..8df9e4cd 100644 --- a/pkg/alb/ingress/alb_spec.go +++ b/pkg/alb/ingress/alb_spec.go @@ -179,6 +179,26 @@ func (r *IngressClassReconciler) getAlbSpecForResources(ctx context.Context, cla Targets: targets, ActiveHealthCheck: nil, // TODO } + + if targetPool.tlsEnabled { + albsdkTargetPool.TlsConfig = &albsdk.TlsConfig{ + Enabled: new(bool), + SkipCertificateValidation: nil, + CustomCa: nil, + } + *albsdkTargetPool.TlsConfig.Enabled = true + + if targetPool.skipCertificateValidation { + albsdkTargetPool.TlsConfig.SkipCertificateValidation = new(bool) + *albsdkTargetPool.TlsConfig.SkipCertificateValidation = true + } + + if targetPool.customCA != "" { + albsdkTargetPool.TlsConfig.CustomCa = new(string) + *albsdkTargetPool.TlsConfig.CustomCa = targetPool.customCA + } + } + alb.TargetPools = append(alb.TargetPools, albsdkTargetPool) } diff --git a/pkg/alb/ingress/alb_spec_test.go b/pkg/alb/ingress/alb_spec_test.go index cfc47bce..cc74d853 100644 --- a/pkg/alb/ingress/alb_spec_test.go +++ b/pkg/alb/ingress/alb_spec_test.go @@ -32,8 +32,7 @@ var _ = Describe("Node Controller", func() { node corev1.Node reconciler IngressClassReconciler - - albSpec albsdk.CreateLoadBalancerPayload + albSpec albsdk.CreateLoadBalancerPayload ) BeforeEach(func() { @@ -86,57 +85,7 @@ var _ = Describe("Node Controller", func() { ApplicationLoadBalancer: stackitconfig.ApplicationLoadBalancerOpts{NetworkID: networkID}}, } - albSpec = albsdk.CreateLoadBalancerPayload{ - DisableTargetSecurityGroupAssignment: new(true), - Labels: new(map[string]string{labels.LabelIngressClassUID: "test-ingress-class-uid"}), - Listeners: []albsdk.Listener{ - { - Http: new(albsdk.ProtocolOptionsHTTP{ - Hosts: []albsdk.HostConfig{ - { - Host: new("example.com"), - Rules: []albsdk.Rule{ - { - Path: new(albsdk.Path{ - Prefix: new("/"), - }), - TargetPool: new(fmt.Sprintf("port-%d", service.Spec.Ports[0].NodePort)), - WebSocket: new(false), - }, - }, - }, - }, - }), - Name: new("80-http"), - Port: new(int32(80)), - Protocol: new("PROTOCOL_HTTP"), - }, - }, - Name: new(string(ingressClass.UID)), //todo - Networks: []albsdk.Network{ - { - NetworkId: new(reconciler.ALBConfig.ApplicationLoadBalancer.NetworkID), - Role: new("ROLE_LISTENERS_AND_TARGETS"), - }, - }, - Options: new(albsdk.LoadBalancerOptions{ - EphemeralAddress: new(true), - }), - // Region: new(reconciler.ALBConfig.Global.Region), why is there a region in spec? TODO - TargetPools: []albsdk.TargetPool{ - { - Name: new(fmt.Sprintf("port-%d", service.Spec.Ports[0].NodePort)), - TargetPort: new(service.Spec.Ports[0].NodePort), - Targets: []albsdk.Target{ - { - DisplayName: new(node.Name), - Ip: new(node.Status.Addresses[0].Address), - }, - }, - }, - }, - } - + albSpec = getInitialAlbSpec(&ingressClass, &service, &node, reconciler.ALBConfig.ApplicationLoadBalancer.NetworkID) }) Describe("Generate ALB spec", func() { @@ -151,91 +100,133 @@ var _ = Describe("Node Controller", func() { Context("when handling labels", func() { It("should work with ownership labels", func() { - spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) Expect(err).To(Succeed()) Expect(errorEventList).To(BeEmpty()) Expect(spec).ToNot(BeNil()) expectedLabels := map[string]string{ - labels.LabelIngressClassUID: string(ingressClass.UID), // Ownership label must be present + labels.LabelIngressClassUID: string(ingressClass.UID), } Expect(*spec).To(BeEquivalentTo(albSpec)) Expect(*spec.Labels).To(BeEquivalentTo(expectedLabels)) }) - }) - It("should work with certificates", func() { + Context("when certificates are configured", func() { + var ( + targetCertID string + ) + + BeforeEach(func() { + // 1. Properly initialize the mock controller + mockCtrl = gomock.NewController(GinkgoT()) + certClient = stackitmocks.NewMockCertificatesClient(mockCtrl) + reconciler.CertificateClient = certClient + + // 2. Clear state pollution by providing a fresh in-memory cluster API server instance + k8sClient = fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + Build() + reconciler.Client = k8sClient + + // reset necessary shared basic entities into the fresh cluster space + ingressClass.ResourceVersion = "" + service.ResourceVersion = "" + node.ResourceVersion = "" + ingressClass.UID = "test-ingress-class-uid" // Preserve initial constant UID value + + // Re-seed necessary shared basic entities into the fresh cluster space + Expect(k8sClient.Create(context.Background(), &ingressClass)).To(Succeed()) + Expect(k8sClient.Create(context.Background(), &service)).To(Succeed()) + Expect(k8sClient.Create(context.Background(), &node)).To(Succeed()) + + // 3. Seed the k8s secret + certSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret-cert", + UID: "dummy-secret-uid-value-1234567", + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "tls.crt": []byte("mock-public-key"), + "tls.key": []byte("mock-private-key"), + }, + } + Expect(k8sClient.Create(context.Background(), &certSecret)).To(Succeed()) - mockCtrl = gomock.NewController(GinkgoT()) - certClient = stackitmocks.NewMockCertificatesClient(mockCtrl) + actualStoredSecret := &corev1.Secret{} + err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "my-secret-cert"}, actualStoredSecret) + Expect(err).NotTo(HaveOccurred()) - // Bind this mock instance to live reconciler reference context - reconciler.CertificateClient = certClient + expectedGeneratedCertName := getCertName(&ingressClass, actualStoredSecret) + targetCertID = "real-certificate-uuid-abc-123" - certSecret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-secret-cert", - UID: "dummy-secret-uid-value-1234567", - }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "tls.crt": []byte("mock-public-key"), - "tls.key": []byte("mock-private-key"), - }, - } - Expect(k8sClient.Create(context.Background(), &certSecret)).To(Succeed()) + mockResponse := &certsdk.GetCertificateResponse{ + Id: new(targetCertID), + Name: new(expectedGeneratedCertName), + } - actualStoredSecret := &corev1.Secret{} - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "my-secret-cert"}, actualStoredSecret) - Expect(err).NotTo(HaveOccurred()) + // 4. Setup mock client expectation + certClient.EXPECT(). + CreateCertificate(gomock.Any(), "test-project", "test-region", gomock.Any()). + Return(mockResponse, nil). + AnyTimes() - expectedGeneratedCertName := getCertName(&ingressClass, actualStoredSecret) - targetCertID := "real-certificate-uuid-abc-123" + albSpec = getInitialAlbSpec(&ingressClass, &service, &node, reconciler.ALBConfig.ApplicationLoadBalancer.NetworkID) - mockResponse := &certsdk.GetCertificateResponse{ - Id: new(targetCertID), - Name: new(expectedGeneratedCertName), - } + // 5. Reset expected listeners on albSpec template + httpsListener := testHttpsListener(service.Spec.Ports[0].NodePort, targetCertID) + albSpec.Listeners = []albsdk.Listener{ + httpsListener, + } + }) - certClient.EXPECT(). - CreateCertificate( - gomock.Any(), - "test-project", - "test-region", - gomock.Any(), // Intercepts any incoming *certsdk.CreateCertificatePayload matching - ). - Return(mockResponse, nil). - Times(1) - - httpsIngress := testHttpsIngress(&ingressClass, &service) - if httpsIngress.Annotations == nil { - httpsIngress.Annotations = make(map[string]string) - } - httpsIngress.Annotations = map[string]string{"alb.stackit.cloud/https-only": "true"} + AfterEach(func() { + mockCtrl.Finish() + }) - Expect(k8sClient.Create(context.Background(), &httpsIngress)).To(Succeed()) + It("should work with certificates", func() { + httpsIngress := testHttpsIngress(&ingressClass, &service) + if httpsIngress.Annotations == nil { + httpsIngress.Annotations = make(map[string]string) + } + httpsIngress.Annotations = map[string]string{"alb.stackit.cloud/https-only": "true"} - // expected albSpec should include new https listener - httpListener := testHttpListener(service.Spec.Ports[0].NodePort) - httpsListener := testHttpsListener(service.Spec.Ports[0].NodePort, targetCertID) - albSpec.Listeners = []albsdk.Listener{ - httpsListener, - httpListener, - } + Expect(k8sClient.Create(context.Background(), &httpsIngress)).To(Succeed()) - // get the specs and compare - spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) - Expect(err).To(Succeed()) - Expect(errorEventList).To(BeEmpty()) + spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) + Expect(err).To(Succeed()) + Expect(errorEventList).To(BeEmpty()) + Expect(spec).ToNot(BeNil()) + Expect(*spec).To(BeEquivalentTo(albSpec)) + }) - Expect(spec).ToNot(BeNil()) + It("should work with tls bridging", func() { + httpsIngress := testHttpsIngress(&ingressClass, &service) + if httpsIngress.Annotations == nil { + httpsIngress.Annotations = make(map[string]string) + } + httpsIngress.Annotations = map[string]string{ + "alb.stackit.cloud/https-only": "true", + "alb.stackit.cloud/target-pool-tls-enabled": "true", + } - // compare - Expect(*spec).To(BeEquivalentTo(albSpec)) + Expect(k8sClient.Create(context.Background(), &httpsIngress)).To(Succeed()) + + // Corrected Pointer initialization logic to prevent nil dereferences + albSpec.TargetPools[0].TlsConfig = &albsdk.TlsConfig{ + Enabled: new(bool), + } + *albSpec.TargetPools[0].TlsConfig.Enabled = true + spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) + Expect(err).To(Succeed()) + Expect(errorEventList).To(BeEmpty()) + Expect(spec).ToNot(BeNil()) + Expect(*spec).To(BeEquivalentTo(albSpec)) + }) }) It("should work with 2 ingresses different path", func() { @@ -385,3 +376,56 @@ func testHttpsListener(nodePort int32, certID string) albsdk.Listener { }, } } + +// Add this to the bottom of your test file alongside your other helpers +func getInitialAlbSpec(ingressClass *networkingv1.IngressClass, service *corev1.Service, node *corev1.Node, networkID string) albsdk.CreateLoadBalancerPayload { + return albsdk.CreateLoadBalancerPayload{ + DisableTargetSecurityGroupAssignment: new(true), + Labels: new(map[string]string{labels.LabelIngressClassUID: "test-ingress-class-uid"}), + Listeners: []albsdk.Listener{ + { + Http: new(albsdk.ProtocolOptionsHTTP{ + Hosts: []albsdk.HostConfig{ + { + Host: new("example.com"), + Rules: []albsdk.Rule{ + { + Path: new(albsdk.Path{ + Prefix: new("/"), + }), + TargetPool: new(fmt.Sprintf("port-%d", service.Spec.Ports[0].NodePort)), + WebSocket: new(false), + }, + }, + }, + }, + }), + Name: new("80-http"), + Port: new(int32(80)), + Protocol: new("PROTOCOL_HTTP"), + }, + }, + Name: new(string(ingressClass.UID)), + Networks: []albsdk.Network{ + { + NetworkId: new(networkID), + Role: new("ROLE_LISTENERS_AND_TARGETS"), + }, + }, + Options: new(albsdk.LoadBalancerOptions{ + EphemeralAddress: new(true), + }), + TargetPools: []albsdk.TargetPool{ + { + Name: new(fmt.Sprintf("port-%d", service.Spec.Ports[0].NodePort)), + TargetPort: new(service.Spec.Ports[0].NodePort), + Targets: []albsdk.Target{ + { + DisplayName: new(node.Name), + Ip: new(node.Status.Addresses[0].Address), + }, + }, + }, + }, + } +} diff --git a/pkg/alb/ingress/annotations.go b/pkg/alb/ingress/annotations.go index f2f890fe..a3335fec 100644 --- a/pkg/alb/ingress/annotations.go +++ b/pkg/alb/ingress/annotations.go @@ -24,13 +24,13 @@ const ( // AnnotationTargetPoolTLSEnabled If true, the application load balancer enables TLS bridging. // It uses the trusted CAs from the operating system for validation. // Can be set on IngressClass, Ingress and Service. - AnnotationTargetPoolTLSEnabled = "alb.stackit.cloud/traget-pool-tls-enabled" + AnnotationTargetPoolTLSEnabled = "alb.stackit.cloud/target-pool-tls-enabled" // AnnotationTargetPoolTLSCustomCa If set, the application load balancer enables TLS bridging with a custom CA provided as value. // Can be set on IngressClass, Ingress and Service - AnnotationTargetPoolTLSCustomCa = "alb.stackit.cloud/traget-pool-tls-custom-ca" + AnnotationTargetPoolTLSCustomCa = "alb.stackit.cloud/target-pool-tls-custom-ca" // AnnotationTargetPoolTLSSkipCertificateValidation If true, the application load balancer enables TLS bridging but skips validation. // Can be set on IngressClass, Ingress and Service. - AnnotationTargetPoolTLSSkipCertificateValidation = "alb.stackit.cloud/traget-pool-tls-skip-certificate-validation" + AnnotationTargetPoolTLSSkipCertificateValidation = "alb.stackit.cloud/target-pool-tls-skip-certificate-validation" // AnnotationHTTPPort Specifies the HTTP port. // Can be set on IngressClass and Ingress. diff --git a/pkg/alb/ingress/resources.go b/pkg/alb/ingress/resources.go index d53e895d..a9ad6574 100644 --- a/pkg/alb/ingress/resources.go +++ b/pkg/alb/ingress/resources.go @@ -78,21 +78,21 @@ func mergeTargetPools(dst, src albTargetPools) (albTargetPools, []errorEvents) { if dstTargetPool.skipCertificateValidation != srcTargetPool.skipCertificateValidation { mergeErrors = append(mergeErrors, errorEvents{ ingressRef: srcTargetPool.ingressRef, - description: fmt.Sprintf("%s annotation ignored as it already is configred differently in ingress %v", AnnotationTargetPoolTLSSkipCertificateValidation, dstTargetPool.ingressRef), + description: fmt.Sprintf("%s annotation ignored as it already is configured differently in ingress %v", AnnotationTargetPoolTLSSkipCertificateValidation, dstTargetPool.ingressRef), typ: "Warning", }) } if dstTargetPool.tlsEnabled != srcTargetPool.tlsEnabled { mergeErrors = append(mergeErrors, errorEvents{ ingressRef: srcTargetPool.ingressRef, - description: fmt.Sprintf("%s annotation ignored as it already is configred differently in ingress %v", AnnotationTargetPoolTLSEnabled, dstTargetPool.ingressRef), + description: fmt.Sprintf("%s annotation ignored as it already is configured differently in ingress %v", AnnotationTargetPoolTLSEnabled, dstTargetPool.ingressRef), typ: "Warning", }) } if dstTargetPool.customCA != srcTargetPool.customCA { mergeErrors = append(mergeErrors, errorEvents{ ingressRef: srcTargetPool.ingressRef, - description: fmt.Sprintf("%s annotation ignored as it already is configred differently in ingress %v", AnnotationTargetPoolTLSCustomCa, dstTargetPool.ingressRef), + description: fmt.Sprintf("%s annotation ignored as it already is configured differently in ingress %v", AnnotationTargetPoolTLSCustomCa, dstTargetPool.ingressRef), typ: "Warning", }) } From f28b4d8e56257015f32626c2911113419c652fe8 Mon Sep 17 00:00:00 2001 From: Menekse Ceylan Date: Thu, 28 May 2026 10:27:09 +0200 Subject: [PATCH 2/5] updated annotation lookup when sorting ingresses for ingressClass Utilized getSortedIngressesForIngressClass for getAlbSpecForIngressClass --- pkg/alb/ingress/alb_spec.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/alb/ingress/alb_spec.go b/pkg/alb/ingress/alb_spec.go index 8df9e4cd..df6865ff 100644 --- a/pkg/alb/ingress/alb_spec.go +++ b/pkg/alb/ingress/alb_spec.go @@ -19,7 +19,7 @@ import ( ) func (r *IngressClassReconciler) getAlbSpecForIngressClass(ctx context.Context, class *networkingv1.IngressClass) (*albsdk.CreateLoadBalancerPayload, []errorEvents, error) { - ingresses, err := r.getIngressesForIngressClass(ctx, class) + ingresses, err := r.getSortedIngressesForIngressClass(ctx, class) if err != nil { return nil, nil, err } @@ -320,8 +320,10 @@ func (r *IngressClassReconciler) getSortedIngressesForIngressClass(ctx context.C } sort.SliceStable(ingresses, func(i, j int) bool { - prioI := getAnnotation(AnnotationPriority, 0, class, &ingresses[i]) - prioJ := getAnnotation(AnnotationPriority, 0, class, &ingresses[j]) + //prioI := getAnnotation(AnnotationPriority, 0, class, &ingresses[i]) + //prioJ := getAnnotation(AnnotationPriority, 0, class, &ingresses[j]) + prioI := getAnnotation(AnnotationPriority, 0, &ingresses[i], class) + prioJ := getAnnotation(AnnotationPriority, 0, &ingresses[j], class) // Sort by Priority (Highest at the beginning) if prioI != prioJ { From 2428294d64b74bc9c98995dcd9e7ca3075eb4ba6 Mon Sep 17 00:00:00 2001 From: Menekse Ceylan Date: Mon, 1 Jun 2026 16:38:14 +0200 Subject: [PATCH 3/5] updated tests --- pkg/alb/ingress/alb_spec.go | 8 +-- pkg/alb/ingress/alb_spec_test.go | 105 ++++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/pkg/alb/ingress/alb_spec.go b/pkg/alb/ingress/alb_spec.go index df6865ff..8933a21b 100644 --- a/pkg/alb/ingress/alb_spec.go +++ b/pkg/alb/ingress/alb_spec.go @@ -320,10 +320,10 @@ func (r *IngressClassReconciler) getSortedIngressesForIngressClass(ctx context.C } sort.SliceStable(ingresses, func(i, j int) bool { - //prioI := getAnnotation(AnnotationPriority, 0, class, &ingresses[i]) - //prioJ := getAnnotation(AnnotationPriority, 0, class, &ingresses[j]) - prioI := getAnnotation(AnnotationPriority, 0, &ingresses[i], class) - prioJ := getAnnotation(AnnotationPriority, 0, &ingresses[j], class) + prioI := getAnnotation(AnnotationPriority, 0, class, &ingresses[i]) + prioJ := getAnnotation(AnnotationPriority, 0, class, &ingresses[j]) + //prioI := getAnnotation(AnnotationPriority, 0, &ingresses[i], class) + //prioJ := getAnnotation(AnnotationPriority, 0, &ingresses[j], class) // Sort by Priority (Highest at the beginning) if prioI != prioJ { diff --git a/pkg/alb/ingress/alb_spec_test.go b/pkg/alb/ingress/alb_spec_test.go index cc74d853..f9b584dd 100644 --- a/pkg/alb/ingress/alb_spec_test.go +++ b/pkg/alb/ingress/alb_spec_test.go @@ -3,6 +3,7 @@ package ingress import ( "context" "fmt" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -229,34 +230,92 @@ var _ = Describe("Node Controller", func() { }) }) - It("should work with 2 ingresses different path", func() { - ingress2 := testIngress(&ingressClass, &service) - ingress2.Name = "ingress2" - ingress2.Spec.Rules[0].HTTP.Paths[0].Path = "/foobar" + Context("when sorting multiple ingresses", func() { + var ( + ingress2 networkingv1.Ingress + ) - Expect(k8sClient.Create(context.Background(), &ingress2)).To(Succeed()) + It("should prioritize priority annotations if defined", func() { - secTargetPool := *albSpec.Listeners[0].Http.Hosts[0].Rules[0].TargetPool - albSpec.Listeners[0].Http.Hosts[0].Rules = []albsdk.Rule{ - { - Path: &albsdk.Path{Prefix: new("/foobar")}, - TargetPool: new(secTargetPool), - WebSocket: new(false), - }, - { - Path: &albsdk.Path{Prefix: new("/")}, - TargetPool: new(secTargetPool), - WebSocket: new(false), - }, - } + err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: ingress.Namespace, Name: ingress.Name}, &ingress) + Expect(err).NotTo(HaveOccurred()) - spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) - Expect(err).To(Succeed()) - Expect(errorEventList).To(BeEmpty()) + ingress.Annotations = nil // ensuring no priority annotations are lingering + Expect(k8sClient.Update(context.Background(), &ingress)).To(Succeed()) + + ingress2 = testIngress(&ingressClass, &service) + ingress2.Name = "ingress2" + ingress2.Spec.Rules[0].HTTP.Paths[0].Path = "/foobar" + if ingress2.Annotations == nil { + ingress2.Annotations = make(map[string]string) + } + ingress2.Annotations = map[string]string{"alb.stackit.cloud/priority": "100"} + + Expect(k8sClient.Create(context.Background(), &ingress2)).To(Succeed()) + + secTargetPool := *albSpec.Listeners[0].Http.Hosts[0].Rules[0].TargetPool + albSpec.Listeners[0].Http.Hosts[0].Rules = []albsdk.Rule{ + { + Path: &albsdk.Path{Prefix: new("/foobar")}, + TargetPool: new(secTargetPool), + WebSocket: new(false), + }, + { + Path: &albsdk.Path{Prefix: new("/")}, + TargetPool: new(secTargetPool), + WebSocket: new(false), + }, + } + + spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) + Expect(err).To(Succeed()) + Expect(errorEventList).To(BeEmpty()) + + Expect(spec).ToNot(BeNil()) + Expect(*spec).To(BeEquivalentTo(albSpec)) + }) + + It("should sort by creationTimeStamp when priority annotations are missing ", func() { + + // manipulate the base ingress' (created in the top-level BeforeEach) creation timestamp to be the oldest + err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: ingress.Namespace, Name: ingress.Name}, &ingress) + Expect(err).NotTo(HaveOccurred()) + ingress.Annotations = nil // ensuring no priority annotations are lingering + Expect(k8sClient.Update(context.Background(), &ingress)).To(Succeed()) + + time.Sleep(1 * time.Second) + + ingress2 = testIngress(&ingressClass, &service) + ingress2.Name = "ingress2" + ingress2.Spec.Rules[0].HTTP.Paths[0].Path = "/foobar" + ingress2.Annotations = nil // ensuring no priority annotations + + Expect(k8sClient.Create(context.Background(), &ingress2)).To(Succeed()) + + secTargetPool := *albSpec.Listeners[0].Http.Hosts[0].Rules[0].TargetPool + albSpec.Listeners[0].Http.Hosts[0].Rules = []albsdk.Rule{ + { + Path: &albsdk.Path{Prefix: new("/")}, + TargetPool: new(secTargetPool), + WebSocket: new(false), + }, + { + Path: &albsdk.Path{Prefix: new("/foobar")}, + TargetPool: new(secTargetPool), + WebSocket: new(false), + }, + } + + spec, errorEventList, err := reconciler.getAlbSpecForIngressClass(context.Background(), &ingressClass) + Expect(err).To(Succeed()) + Expect(errorEventList).To(BeEmpty()) + + Expect(spec).ToNot(BeNil()) + Expect(*spec).To(BeEquivalentTo(albSpec)) + }) - Expect(spec).ToNot(BeNil()) - Expect(*spec).To(BeEquivalentTo(albSpec)) }) + }) }) From 3930409d498689e73a852e6024cbb0c5c825c2d3 Mon Sep 17 00:00:00 2001 From: Menekse Ceylan Date: Tue, 2 Jun 2026 09:37:21 +0200 Subject: [PATCH 4/5] introduced sequenceIndex to not lose the order of rules when creating alb specs via getAlbSpecForResources at last step --- pkg/alb/ingress/alb_spec.go | 34 +++++++++++++++++++++++++++++--- pkg/alb/ingress/alb_spec_test.go | 9 +++++---- pkg/alb/ingress/resources.go | 1 + 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pkg/alb/ingress/alb_spec.go b/pkg/alb/ingress/alb_spec.go index 8933a21b..135c1d1e 100644 --- a/pkg/alb/ingress/alb_spec.go +++ b/pkg/alb/ingress/alb_spec.go @@ -34,9 +34,9 @@ func (r *IngressClassReconciler) getAlbSpecForIngresses(ctx context.Context, cla certificates := albCertificates{} targetPools := albTargetPools{} - for _, ingress := range ingresses { + for i, ingress := range ingresses { var listenerMergeError, targetPoolMergeError []errorEvents - ingressListeners, ingressCertificates, ingressTargetPools, ingressErrorList := r.getALBResourcesForIngress(ctx, class, &ingress) + ingressListeners, ingressCertificates, ingressTargetPools, ingressErrorList := r.getALBResourcesForIngress(ctx, class, &ingress, i) errorList = append(errorList, ingressErrorList...) certificates = mergeCertificates(certificates, ingressCertificates) @@ -134,6 +134,33 @@ func (r *IngressClassReconciler) getAlbSpecForResources(ctx context.Context, cla albsdkHost.Rules = append(albsdkHost.Rules, albsdkRule) } + + /// menekse + + sort.SliceStable(albsdkHost.Rules, func(i, j int) bool { + pathI := "" + if albsdkHost.Rules[i].Path.Prefix != nil { + pathI = *albsdkHost.Rules[i].Path.Prefix + } else if albsdkHost.Rules[i].Path.ExactMatch != nil { + pathI = *albsdkHost.Rules[i].Path.ExactMatch + } + + pathJ := "" + if albsdkHost.Rules[j].Path.Prefix != nil { + pathJ = *albsdkHost.Rules[j].Path.Prefix + } else if albsdkHost.Rules[j].Path.ExactMatch != nil { + pathJ = *albsdkHost.Rules[j].Path.ExactMatch + } + + ruleI := hostPaths.path[pathI] + ruleJ := hostPaths.path[pathJ] + + // Smaller sequence index means it was processed earlier (higher priority) + return ruleI.sequenceIndex < ruleJ.sequenceIndex + }) + + //// + albsdkHosts = append(albsdkHosts, albsdkHost) albsdkListener.Http = new(albsdk.ProtocolOptionsHTTP{ @@ -205,7 +232,7 @@ func (r *IngressClassReconciler) getAlbSpecForResources(ctx context.Context, cla return alb, errorList, nil } -func (r *IngressClassReconciler) getALBResourcesForIngress(ctx context.Context, class *networkingv1.IngressClass, ingress *networkingv1.Ingress) (albListeners, albCertificates, albTargetPools, []errorEvents) { +func (r *IngressClassReconciler) getALBResourcesForIngress(ctx context.Context, class *networkingv1.IngressClass, ingress *networkingv1.Ingress, sequenceIndex int) (albListeners, albCertificates, albTargetPools, []errorEvents) { ref := getIngressRefForIngress(r.Scheme, ingress) certificates := albCertificates{} @@ -270,6 +297,7 @@ func (r *IngressClassReconciler) getALBResourcesForIngress(ctx context.Context, cookiePersistenceTtlSeconds: getAnnotation(AnnotationCookiePersistenceTTLSeconds, 0, class, ingress), targetPoolName: poolName, websocket: getAnnotation(AnnotationWebSocket, false, class, ingress), + sequenceIndex: sequenceIndex, } } } diff --git a/pkg/alb/ingress/alb_spec_test.go b/pkg/alb/ingress/alb_spec_test.go index f9b584dd..31d48351 100644 --- a/pkg/alb/ingress/alb_spec_test.go +++ b/pkg/alb/ingress/alb_spec_test.go @@ -280,15 +280,16 @@ var _ = Describe("Node Controller", func() { // manipulate the base ingress' (created in the top-level BeforeEach) creation timestamp to be the oldest err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: ingress.Namespace, Name: ingress.Name}, &ingress) Expect(err).NotTo(HaveOccurred()) + ingress.Annotations = nil // ensuring no priority annotations are lingering Expect(k8sClient.Update(context.Background(), &ingress)).To(Succeed()) - - time.Sleep(1 * time.Second) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Namespace: ingress.Namespace, Name: ingress.Name}, &ingress)).To(Succeed()) ingress2 = testIngress(&ingressClass, &service) ingress2.Name = "ingress2" - ingress2.Spec.Rules[0].HTTP.Paths[0].Path = "/foobar" + ingress2.Spec.Rules[0].HTTP.Paths[0].Path = "/menekse" ingress2.Annotations = nil // ensuring no priority annotations + ingress2.ObjectMeta.CreationTimestamp = metav1.NewTime(ingress.CreationTimestamp.Add(1 * time.Hour)) Expect(k8sClient.Create(context.Background(), &ingress2)).To(Succeed()) @@ -300,7 +301,7 @@ var _ = Describe("Node Controller", func() { WebSocket: new(false), }, { - Path: &albsdk.Path{Prefix: new("/foobar")}, + Path: &albsdk.Path{Prefix: new("/menekse")}, TargetPool: new(secTargetPool), WebSocket: new(false), }, diff --git a/pkg/alb/ingress/resources.go b/pkg/alb/ingress/resources.go index a9ad6574..a84410b0 100644 --- a/pkg/alb/ingress/resources.go +++ b/pkg/alb/ingress/resources.go @@ -30,6 +30,7 @@ type albListenerRule struct { pathTyp networkingv1.PathType targetPoolName string websocket bool + sequenceIndex int } type albTargetPools map[string]albTargetPool From 78449dfa540659b19c8e648de00702ebedd43836 Mon Sep 17 00:00:00 2001 From: Menekse Ceylan Date: Wed, 3 Jun 2026 21:35:07 +0200 Subject: [PATCH 5/5] deleted unused comment fixed unit tests and added more for ingress controller --- pkg/alb/ingress/alb_spec.go | 2 - .../ingressclass_controller_unit_test.go | 384 ++++++++++++++---- 2 files changed, 308 insertions(+), 78 deletions(-) diff --git a/pkg/alb/ingress/alb_spec.go b/pkg/alb/ingress/alb_spec.go index 135c1d1e..26d9c9b5 100644 --- a/pkg/alb/ingress/alb_spec.go +++ b/pkg/alb/ingress/alb_spec.go @@ -350,8 +350,6 @@ func (r *IngressClassReconciler) getSortedIngressesForIngressClass(ctx context.C sort.SliceStable(ingresses, func(i, j int) bool { prioI := getAnnotation(AnnotationPriority, 0, class, &ingresses[i]) prioJ := getAnnotation(AnnotationPriority, 0, class, &ingresses[j]) - //prioI := getAnnotation(AnnotationPriority, 0, &ingresses[i], class) - //prioJ := getAnnotation(AnnotationPriority, 0, &ingresses[j], class) // Sort by Priority (Highest at the beginning) if prioI != prioJ { diff --git a/pkg/alb/ingress/ingressclass_controller_unit_test.go b/pkg/alb/ingress/ingressclass_controller_unit_test.go index 5556f032..18d496ba 100644 --- a/pkg/alb/ingress/ingressclass_controller_unit_test.go +++ b/pkg/alb/ingress/ingressclass_controller_unit_test.go @@ -1,8 +1,5 @@ package ingress -/* -TODO - import ( "context" "testing" @@ -11,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" stackitconfig "github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/config" gomock "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" @@ -23,12 +21,15 @@ import ( ) const ( - testProjectID = "test-project" - testRegion = "test-region" - testALBName = "k8s-ingress-test-ingressclass" - testNamespace = "test-namespace" - testPublicIP = "1.2.3.4" - testPrivateIP = "10.0.0.1" + testProjectID = "test-project" + testRegion = "test-region" + testNamespace = "test-namespace" + testPublicIP = "1.2.3.4" + testPrivateIP = "10.0.0.1" + testIngressName = "test-ingress" + testIngressClassName = "k8s-ingress-test-ingress-class" + testIngressClassUID = "11111111-2222-3333-4444-555555555555" + testALBName = testIngressClassUID ) //nolint:funlen // Just many test cases. @@ -36,6 +37,19 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { testIngressClass := &networkingv1.IngressClass{ ObjectMeta: metav1.ObjectMeta{ Name: testIngressClassName, + UID: testIngressClassUID, + }, + } + + testService := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: testNamespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 8080}, + }, }, } @@ -50,9 +64,8 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { { name: "ALB not ready (Terminating), should requeue", mockK8sClient: func(c client.Client) error { - return c.Create(context.Background(), &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}, - }) + return c.Create(context.Background(), new(testIngress(testIngressClass, testService))) + }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). @@ -64,17 +77,13 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { wantResult: reconcile.Result{RequeueAfter: 10 * time.Second}, wantErr: false, }, - // This case only checks the reconcile result, not whether the ingress status was actually updated. - // The actual update logic will be verified in integration tests. { name: "ALB ready, public IP available, ingress status needs update", ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + new(testIngress(testIngressClass, testService)), }, mockK8sClient: func(c client.Client) error { - return c.Create(context.Background(), &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}, - }) + return c.Create(context.Background(), new(testIngress(testIngressClass, testService))) }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). @@ -87,17 +96,13 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { wantResult: reconcile.Result{}, wantErr: false, }, - // This case only checks the reconcile result, not whether the ingress status was actually updated. - // The actual update logic will be verified in integration tests. { name: "ALB ready, private IP available, ingress status needs update", ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + new(testIngress(testIngressClass, testService)), }, mockK8sClient: func(c client.Client) error { - return c.Create(context.Background(), &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}, - }) + return c.Create(context.Background(), new(testIngress(testIngressClass, testService))) }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). @@ -110,36 +115,40 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { wantResult: reconcile.Result{}, wantErr: false, }, - // This case only checks the reconcile result, not whether the ingress status was actually updated. - // The actual update logic will be verified in integration tests. { - name: "ALB ready, IP already correct, no update", + name: "ALB ready, IP already correct, no status update", ingresses: []*networkingv1.Ingress{ - { - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}, - Status: networkingv1.IngressStatus{ + new(func() networkingv1.Ingress { + ing := testIngress(testIngressClass, testService) + ing.Spec.IngressClassName = new(testIngressClassName) + ing.Status = networkingv1.IngressStatus{ LoadBalancer: networkingv1.IngressLoadBalancerStatus{ Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: testPublicIP}}, }, - }, - }, + } + return ing + }()), }, mockK8sClient: func(c client.Client) error { - return c.Create(context.Background(), &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}, - Status: networkingv1.IngressStatus{ - LoadBalancer: networkingv1.IngressLoadBalancerStatus{ - Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: testPublicIP}}, - }, + ing := testIngress(testIngressClass, testService) + ing.Spec.IngressClassName = new(testIngressClassName) + if err := c.Create(context.Background(), &ing); err != nil { + return err + } + + ing.Status = networkingv1.IngressStatus{ + LoadBalancer: networkingv1.IngressLoadBalancerStatus{ + Ingress: []networkingv1.IngressLoadBalancerIngress{{IP: testPublicIP}}, }, - }) + } + return c.Status().Update(context.Background(), &ing) }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). Return(&albsdk.LoadBalancer{ - Status: new("STATUS_READY"), - PrivateAddress: new(testPublicIP), + Status: new("STATUS_READY"), + ExternalAddress: new(testPublicIP), }, nil) }, wantResult: reconcile.Result{}, @@ -148,7 +157,7 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { { name: "failed to get load balancer", ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + new(testIngress(testIngressClass, testService)), }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT().GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName).Return(nil, stackit.ErrorNotFound) @@ -157,65 +166,269 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { wantErr: true, }, { - name: "failed to get latest ingress", + name: "ALB ready, no public or private IP, return error", ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + new(testIngress(testIngressClass, testService)), }, mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). Return(&albsdk.LoadBalancer{ - Status: new("STATUS_READY"), - PrivateAddress: new(testPublicIP), + Status: new("STATUS_READY"), }, nil) }, wantResult: reconcile.Result{}, wantErr: true, }, - // This case only checks the reconcile result, not whether the ingress status was actually updated. - // The actual update logic will be verified in integration tests. + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + + mockAlbClient := stackit.NewMockApplicationLoadBalancerClient(ctrl) + fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + + r := &IngressClassReconciler{ + Client: fakeClient, + ALBClient: mockAlbClient, + ALBConfig: stackitconfig.ALBConfig{ + Global: stackitconfig.GlobalOpts{ + ProjectID: testProjectID, + Region: testRegion, + }, + }, + } + + if tt.mockK8sClient != nil { + if err := tt.mockK8sClient(fakeClient); err != nil { + t.Fatalf("mockK8sClient failed: %v", err) + } + } + + if tt.mockALBClient != nil { + tt.mockALBClient(mockAlbClient) + } + + expectedIngress := testIngress(testIngressClass, testService) + + got, err := r.updateStatus(context.Background(), testIngressClass) + if (err != nil) != tt.wantErr { + t.Fatalf("expected error %v, got %v", tt.wantErr, err) + } + if diff := cmp.Diff(tt.wantResult, got); diff != "" { + t.Fatalf("unexpected result (-want +got):\n%s", diff) + } + + if tt.name == "ALB ready, public IP available, ingress status needs update" { + latestIngress := &networkingv1.Ingress{} + if err := fakeClient.Get(context.Background(), client.ObjectKey{Namespace: expectedIngress.Namespace, Name: expectedIngress.Name}, latestIngress); err != nil { + t.Fatalf("failed to fetch ingress: %v", err) + } + if len(latestIngress.Status.LoadBalancer.Ingress) == 0 || latestIngress.Status.LoadBalancer.Ingress[0].IP != testPublicIP { + t.Errorf("Ingress status was not updated with the expected IP!") + } + } + + }) + } +} + +func TestIngressClassReconciler_Reconcile(t *testing.T) { + + testIngressClass := &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testIngressClassName, + UID: testIngressClassUID, + Finalizers: []string{finalizerName}, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: controllerName, + }, + } + + tests := []struct { + name string + ingressClass *networkingv1.IngressClass + mockALB func(*stackit.MockApplicationLoadBalancerClient) + mockCerts func(*stackit.MockCertificatesClient) + wantResult reconcile.Result + wantErr bool + checkFinalizer bool + }{ { - name: "failed to update ingress status", - ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + name: "existing ingress class, happy", + ingressClass: testIngressClass, + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) { + m.EXPECT(). + GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). + Return(&albsdk.LoadBalancer{ + Status: new("STATUS_READY"), + ExternalAddress: new(testPublicIP), + }, nil).Times(2) }, - mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { + wantResult: reconcile.Result{}, + wantErr: false, + }, + { + name: "ingress class doesn't match the controller, should ignore and exit cleanly", + ingressClass: &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testIngressClassName, + UID: testIngressClassUID, + Finalizers: []string{finalizerName}, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: "unknown-controller", + }, + }, + mockALB: nil, + wantResult: reconcile.Result{}, + wantErr: false, + }, + { + name: "ingress class has emtpy/mismatched controller specs, should ignore and exit cleanly", + ingressClass: &networkingv1.IngressClass{}, + mockALB: nil, + wantResult: reconcile.Result{}, + wantErr: false, + }, + { + name: "ingress class not found, should ignore and exit cleanly", + ingressClass: nil, + mockALB: nil, + wantResult: reconcile.Result{}, + wantErr: false, + }, + { + name: "missing finalizer, should add finalizer", + ingressClass: &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testIngressClassName, + UID: testIngressClassUID, + Finalizers: []string{}, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: controllerName, + }, + }, + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) {}, + wantResult: reconcile.Result{}, + wantErr: false, + checkFinalizer: true, + }, + { + name: "ALB status not ready, should requeue", + ingressClass: &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testIngressClassName, + UID: testIngressClassUID, + Finalizers: []string{finalizerName}, + }, + Spec: networkingv1.IngressClassSpec{ + Controller: controllerName, + }, + }, + + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). Return(&albsdk.LoadBalancer{ - Status: new("STATUS_READY"), - PrivateAddress: new(testPublicIP), + Status: new("STATUS_NOT_READY"), + ExternalAddress: new(testPublicIP), + }, nil).Times(2) + }, + wantResult: reconcile.Result{RequeueAfter: 10 * time.Second}, + wantErr: false, + checkFinalizer: true, + }, + { + name: "ALB does not exist yet, should create a new one successfully", + ingressClass: testIngressClass, + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) { + m.EXPECT(). + GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). + Return(nil, stackit.ErrorNotFound) + m.EXPECT(). + CreateLoadBalancer(gomock.Any(), testProjectID, testRegion, gomock.Any()). + Return(&albsdk.LoadBalancer{}, nil) + m.EXPECT(). + GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). + Return(&albsdk.LoadBalancer{ + Status: new("STATUS_READY"), + ExternalAddress: new(testPublicIP), }, nil) }, wantResult: reconcile.Result{}, - wantErr: true, + wantErr: false, }, { - name: "ALB ready, no public or private IP, should requeue", - ingresses: []*networkingv1.Ingress{ - {ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testIngressName}}, + name: "ingress class has deletion timestamp, should run handleIngressClassDeletion", + ingressClass: &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: testIngressClassName, + UID: testIngressClassUID, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{finalizerName}, + }, + Spec: networkingv1.IngressClassSpec{Controller: controllerName}, }, - mockALBClient: func(m *stackit.MockApplicationLoadBalancerClient) { + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) { + m.EXPECT(). + DeleteLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). + Return(nil) + }, + mockCerts: func(m *stackit.MockCertificatesClient) { + m.EXPECT(). + ListCertificate(gomock.Any(), testProjectID, testRegion). + Return(nil, nil) + }, + wantResult: reconcile.Result{}, + wantErr: false, + }, + { + name: "ALB configuration has changed, should issue an update call", + ingressClass: testIngressClass, + mockALB: func(m *stackit.MockApplicationLoadBalancerClient) { m.EXPECT(). GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). Return(&albsdk.LoadBalancer{ - Status: new("STATUS_READY"), + Status: new("STATUS_READY"), + Version: new("lb-v1"), + Listeners: []albsdk.Listener{{Port: new(int32(80))}}, // add a listener to empty config + }, nil) + + m.EXPECT(). + UpdateLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName, gomock.Any()). + Return(&albsdk.LoadBalancer{}, nil) + + m.EXPECT(). + GetLoadBalancer(gomock.Any(), testProjectID, testRegion, testALBName). + Return(&albsdk.LoadBalancer{ + Status: new("STATUS_READY"), + ExternalAddress: new(testPublicIP), }, nil) }, - wantResult: reconcile.Result{RequeueAfter: 10 * time.Second}, + wantResult: reconcile.Result{}, wantErr: false, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) - mockAlbClient := stackit.NewMockApplicationLoadBalancerClient(ctrl) - fakeClient := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + mockCertsClient := stackit.NewMockCertificatesClient(ctrl) + + clientBuilder := fake.NewClientBuilder().WithScheme(scheme.Scheme) + if tt.ingressClass != nil { + clientBuilder.WithRuntimeObjects(tt.ingressClass) + } + fakeClient := clientBuilder.Build() + r := &IngressClassReconciler{ - Client: fakeClient, - ALBClient: mockAlbClient, + Client: fakeClient, + ALBClient: mockAlbClient, + CertificateClient: mockCertsClient, ALBConfig: stackitconfig.ALBConfig{ Global: stackitconfig.GlobalOpts{ ProjectID: testProjectID, @@ -224,24 +437,43 @@ func TestIngressClassReconciler_updateStatus(t *testing.T) { }, } - if tt.mockK8sClient != nil { - if err := tt.mockK8sClient(fakeClient); err != nil { - t.Fatalf("mockK8sClient failed: %v", err) - } + if tt.mockALB != nil { + tt.mockALB(mockAlbClient) + } + if tt.mockCerts != nil { + tt.mockCerts(mockCertsClient) } - if tt.mockALBClient != nil { - tt.mockALBClient(mockAlbClient) + req := reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: testIngressClassName, + }, } - got, err := r.updateStatus(context.Background(), testIngressClass) + got, err := r.Reconcile(context.Background(), req) if (err != nil) != tt.wantErr { - t.Fatalf("expected error %v, got %v", tt.wantErr, err) + t.Fatalf("Reconcile() - expected error %v, got %v", tt.wantErr, err) } if diff := cmp.Diff(tt.wantResult, got); diff != "" { - t.Fatalf("unexpected result (-want +got):\n%s", diff) + t.Fatalf("Reconcile() - unexpected result (-want +got):\n%s", diff) + } + + if tt.checkFinalizer { + // fetching the absolute latest state of the object directly from the fake K8s API server + latestClass := &networkingv1.IngressClass{} + key := client.ObjectKey{Name: testIngressClassName} + + if fetchErr := fakeClient.Get(context.Background(), key, latestClass); fetchErr != nil { + t.Fatalf("Failed to fetch latest IngressClass state from fake client: %v", fetchErr) + } + + // assertion: the finalizer string list is no longer empty and contains finalizerName + if len(latestClass.Finalizers) == 0 { + t.Errorf("Verification failed: expected IngressClass to have finalizers, but the list is empty") + } else if latestClass.Finalizers[0] != finalizerName { + t.Errorf("Verification failed: expected finalizer %q, but found %q", finalizerName, latestClass.Finalizers[0]) + } } }) } } -*/