github.com/cilium/cilium@v1.16.2/operator/pkg/ingress/ingress_reconcile_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ingress
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/sirupsen/logrus"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"google.golang.org/protobuf/types/known/anypb"
    16  	corev1 "k8s.io/api/core/v1"
    17  	networkingv1 "k8s.io/api/networking/v1"
    18  	k8sApiErrors "k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/types"
    21  	"sigs.k8s.io/controller-runtime/pkg/client"
    22  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    23  	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
    24  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    25  
    26  	"github.com/cilium/cilium/operator/pkg/model"
    27  	"github.com/cilium/cilium/operator/pkg/model/translation"
    28  	ingressTranslation "github.com/cilium/cilium/operator/pkg/model/translation/ingress"
    29  	"github.com/cilium/cilium/pkg/envoy"
    30  	ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    31  )
    32  
    33  const (
    34  	testCiliumNamespace                 = "cilium"
    35  	testUseProxyProtocol                = true
    36  	testCiliumSecretsNamespace          = "cilium-secrets"
    37  	testDefaultLoadbalancingServiceName = "cilium-ingress"
    38  	testDefaultSecretNamespace          = ""
    39  	testDefaultSecretName               = ""
    40  	testDefaultTimeout                  = 60
    41  	testIngressDefaultRequestTimeout    = time.Duration(0)
    42  )
    43  
    44  func TestReconcile(t *testing.T) {
    45  	logger := logrus.New()
    46  	logger.SetOutput(io.Discard)
    47  
    48  	t.Run("Reconcile of Cilium Ingress without explicit loadbalancing mode will create the resources for the default loadbalancing mode if they don't exist", func(t *testing.T) {
    49  		fakeClient := fake.NewClientBuilder().
    50  			WithScheme(testScheme()).
    51  			WithObjects(
    52  				&networkingv1.Ingress{
    53  					ObjectMeta: metav1.ObjectMeta{
    54  						Namespace: "test",
    55  						Name:      "test",
    56  					},
    57  					Spec: networkingv1.IngressSpec{
    58  						IngressClassName: model.AddressOf("cilium"),
    59  						DefaultBackend:   defaultBackend(),
    60  					},
    61  				},
    62  			).
    63  			Build()
    64  
    65  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
    66  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
    67  
    68  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
    69  
    70  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
    71  			NamespacedName: types.NamespacedName{
    72  				Namespace: "test",
    73  				Name:      "test",
    74  			},
    75  		})
    76  		require.NoError(t, err)
    77  		require.NotNil(t, result)
    78  
    79  		svc := corev1.Service{}
    80  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
    81  		require.NoError(t, err, "Dedicated loadbalancer service should exist")
    82  
    83  		ep := corev1.Endpoints{}
    84  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
    85  		require.NoError(t, err, "Dedicated loadbalancer service endpoints should exist")
    86  
    87  		cec := ciliumv2.CiliumEnvoyConfig{}
    88  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
    89  		require.NoError(t, err, "Dedicated CiliumEnvoyConfig should exist")
    90  
    91  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
    92  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
    93  		require.Error(t, err, "Empty CiliumEnvoyConfig must be removed")
    94  		require.True(t, k8sApiErrors.IsNotFound(err))
    95  	})
    96  
    97  	t.Run("Reconcile of Ingress without specific IngressClassName will create resources if cilium IngressClass is the default", func(t *testing.T) {
    98  		fakeClient := fake.NewClientBuilder().
    99  			WithScheme(testScheme()).
   100  			WithObjects(
   101  				&networkingv1.Ingress{
   102  					ObjectMeta: metav1.ObjectMeta{
   103  						Namespace: "test",
   104  						Name:      "test",
   105  					},
   106  					Spec: networkingv1.IngressSpec{
   107  						DefaultBackend: defaultBackend(),
   108  					},
   109  				},
   110  				&networkingv1.IngressClass{
   111  					ObjectMeta: metav1.ObjectMeta{
   112  						Name: "cilium",
   113  						Annotations: map[string]string{
   114  							"ingressclass.kubernetes.io/is-default-class": "true",
   115  						},
   116  					},
   117  				},
   118  			).
   119  			Build()
   120  
   121  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   122  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   123  
   124  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   125  
   126  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   127  			NamespacedName: types.NamespacedName{
   128  				Namespace: "test",
   129  				Name:      "test",
   130  			},
   131  		})
   132  		require.NoError(t, err)
   133  		require.NotNil(t, result)
   134  
   135  		svc := corev1.Service{}
   136  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   137  		require.NoError(t, err, "Dedicated loadbalancer service should exist")
   138  
   139  		ep := corev1.Endpoints{}
   140  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   141  		require.NoError(t, err, "Dedicated loadbalancer service endpoints should exist")
   142  
   143  		cec := ciliumv2.CiliumEnvoyConfig{}
   144  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   145  		require.NoError(t, err, "Dedicated CiliumEnvoyConfig should exist")
   146  
   147  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   148  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   149  		require.Error(t, err, "Empty CiliumEnvoyConfig must be removed")
   150  		require.True(t, k8sApiErrors.IsNotFound(err))
   151  	})
   152  
   153  	t.Run("Reconcile of Ingress without specific IngressClassName won't create resources if cilium IngressClass is not the default", func(t *testing.T) {
   154  		fakeClient := fake.NewClientBuilder().
   155  			WithScheme(testScheme()).
   156  			WithObjects(
   157  				&networkingv1.Ingress{
   158  					ObjectMeta: metav1.ObjectMeta{
   159  						Namespace: "test",
   160  						Name:      "test",
   161  					},
   162  					Spec: networkingv1.IngressSpec{
   163  						DefaultBackend: defaultBackend(),
   164  					},
   165  				},
   166  				&networkingv1.IngressClass{
   167  					ObjectMeta: metav1.ObjectMeta{
   168  						Name: "cilium",
   169  						Annotations: map[string]string{
   170  							"ingressclass.kubernetes.io/is-default-class": "false",
   171  						},
   172  					},
   173  				},
   174  			).
   175  			Build()
   176  
   177  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   178  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   179  
   180  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   181  
   182  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   183  			NamespacedName: types.NamespacedName{
   184  				Namespace: "test",
   185  				Name:      "test",
   186  			},
   187  		})
   188  		require.NoError(t, err)
   189  		require.NotNil(t, result)
   190  
   191  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Service{})
   192  		require.True(t, k8sApiErrors.IsNotFound(err), "Service should not be created")
   193  
   194  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Endpoints{})
   195  		require.True(t, k8sApiErrors.IsNotFound(err), "Endpoints should not be created")
   196  
   197  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &ciliumv2.CiliumEnvoyConfig{})
   198  		require.True(t, k8sApiErrors.IsNotFound(err), "CiliumEnvoyConfig should not be created")
   199  	})
   200  
   201  	t.Run("Reconcile of shared Cilium Ingress will create the shared CiliumEnvoyConfig in the cilium namespace", func(t *testing.T) {
   202  		fakeClient := fake.NewClientBuilder().
   203  			WithScheme(testScheme()).
   204  			WithObjects(
   205  				&networkingv1.Ingress{
   206  					ObjectMeta: metav1.ObjectMeta{
   207  						Namespace: "test",
   208  						Name:      "test",
   209  						Annotations: map[string]string{
   210  							"ingress.cilium.io/loadbalancer-mode": "shared",
   211  						},
   212  					},
   213  					Spec: networkingv1.IngressSpec{
   214  						IngressClassName: model.AddressOf("cilium"),
   215  						DefaultBackend:   defaultBackend(),
   216  					},
   217  				},
   218  			).
   219  			Build()
   220  
   221  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   222  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   223  
   224  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   225  
   226  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   227  			NamespacedName: types.NamespacedName{
   228  				Namespace: "test",
   229  				Name:      "test",
   230  			},
   231  		})
   232  		require.NoError(t, err)
   233  		require.NotNil(t, result)
   234  
   235  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   236  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   237  		require.NoError(t, err, "Shared CiliumEnvoyConfig should exist for shared Ingress")
   238  		require.NotEmpty(t, sharedCEC.Spec.Resources)
   239  
   240  		svc := corev1.Service{}
   241  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   242  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer service should not exist for shared Ingress")
   243  
   244  		ep := corev1.Endpoints{}
   245  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   246  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer endpoints should not exist for shared Ingress")
   247  
   248  		cec := ciliumv2.CiliumEnvoyConfig{}
   249  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   250  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated CiliumEnvoyConfig should not exist for shared Ingress")
   251  	})
   252  
   253  	t.Run("Reconcile of Cilium Ingress will cleanup any potentially existing resources of the other loadbalancing mode (changing from dedicated -> shared)", func(t *testing.T) {
   254  		fakeClient := fake.NewClientBuilder().
   255  			WithScheme(testScheme()).
   256  			WithObjects(
   257  				&networkingv1.Ingress{
   258  					ObjectMeta: metav1.ObjectMeta{
   259  						Namespace: "test",
   260  						Name:      "test",
   261  						Annotations: map[string]string{
   262  							"ingress.cilium.io/loadbalancer-mode": "shared",
   263  						},
   264  					},
   265  					Spec: networkingv1.IngressSpec{
   266  						IngressClassName: model.AddressOf("cilium"),
   267  						DefaultBackend:   defaultBackend(),
   268  					},
   269  				},
   270  				&corev1.Service{
   271  					ObjectMeta: metav1.ObjectMeta{
   272  						Namespace: "test",
   273  						Name:      "cilium-ingress-test",
   274  					},
   275  				},
   276  				&corev1.Endpoints{
   277  					ObjectMeta: metav1.ObjectMeta{
   278  						Namespace: "test",
   279  						Name:      "cilium-ingress-test",
   280  					},
   281  				},
   282  				&ciliumv2.CiliumEnvoyConfig{
   283  					ObjectMeta: metav1.ObjectMeta{
   284  						Namespace: "test",
   285  						Name:      "cilium-ingress-test-test",
   286  					},
   287  				},
   288  			).
   289  			Build()
   290  
   291  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   292  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   293  
   294  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   295  
   296  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   297  			NamespacedName: types.NamespacedName{
   298  				Namespace: "test",
   299  				Name:      "test",
   300  			},
   301  		})
   302  		require.NoError(t, err)
   303  		require.NotNil(t, result)
   304  
   305  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   306  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   307  		require.NoError(t, err, "Shared CiliumEnvoyConfig should exist for shared Ingress")
   308  		require.NotEmpty(t, sharedCEC.Spec.Resources)
   309  
   310  		svc := corev1.Service{}
   311  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   312  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer service should not exist for shared Ingress")
   313  
   314  		ep := corev1.Endpoints{}
   315  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   316  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer endpoints should not exist for shared Ingress")
   317  
   318  		cec := ciliumv2.CiliumEnvoyConfig{}
   319  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   320  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated CiliumEnvoyConfig should not exist for shared Ingress")
   321  	})
   322  
   323  	t.Run("Reconcile of Cilium Ingress will cleanup any potentially existing resources of the other loadbalancing mode (changing from shared -> dedicated)", func(t *testing.T) {
   324  		fakeClient := fake.NewClientBuilder().
   325  			WithScheme(testScheme()).
   326  			WithObjects(
   327  				&networkingv1.Ingress{
   328  					ObjectMeta: metav1.ObjectMeta{
   329  						Namespace: "test",
   330  						Name:      "test",
   331  						Annotations: map[string]string{
   332  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   333  						},
   334  					},
   335  					Spec: networkingv1.IngressSpec{
   336  						IngressClassName: model.AddressOf("cilium"),
   337  						DefaultBackend:   defaultBackend(),
   338  					},
   339  				},
   340  				&ciliumv2.CiliumEnvoyConfig{
   341  					ObjectMeta: metav1.ObjectMeta{
   342  						Namespace: testCiliumNamespace,
   343  						Name:      testDefaultLoadbalancingServiceName,
   344  					},
   345  					Spec: ciliumv2.CiliumEnvoyConfigSpec{
   346  						Resources: []ciliumv2.XDSResource{
   347  							{
   348  								Any: &anypb.Any{
   349  									TypeUrl: envoy.ListenerTypeURL,
   350  								},
   351  							},
   352  						},
   353  					},
   354  				},
   355  			).
   356  			Build()
   357  
   358  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   359  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   360  
   361  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   362  
   363  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   364  			NamespacedName: types.NamespacedName{
   365  				Namespace: "test",
   366  				Name:      "test",
   367  			},
   368  		})
   369  		require.NoError(t, err)
   370  		require.NotNil(t, result)
   371  
   372  		svc := corev1.Service{}
   373  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   374  		require.NoError(t, err, "Dedicated loadbalancer service should exist")
   375  
   376  		ep := corev1.Endpoints{}
   377  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   378  		require.NoError(t, err, "Dedicated loadbalancer service endpoints should exist")
   379  
   380  		cec := ciliumv2.CiliumEnvoyConfig{}
   381  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   382  		require.NoError(t, err, "Dedicated CiliumEnvoyConfig should exist")
   383  
   384  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   385  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   386  		require.Error(t, err, "Empty CiliumEnvoyConfig must be removed")
   387  		require.True(t, k8sApiErrors.IsNotFound(err))
   388  	})
   389  
   390  	t.Run("Reconcile of a non-existent, potentially deleted, Cilium Ingress will try to cleanup any potentially existing shared resources", func(t *testing.T) {
   391  		fakeClient := fake.NewClientBuilder().
   392  			WithScheme(testScheme()).
   393  			WithObjects(
   394  				&ciliumv2.CiliumEnvoyConfig{
   395  					ObjectMeta: metav1.ObjectMeta{
   396  						Namespace: testCiliumNamespace,
   397  						Name:      testDefaultLoadbalancingServiceName,
   398  					},
   399  					Spec: ciliumv2.CiliumEnvoyConfigSpec{
   400  						Resources: []ciliumv2.XDSResource{
   401  							{
   402  								Any: &anypb.Any{
   403  									TypeUrl: envoy.ListenerTypeURL,
   404  								},
   405  							},
   406  						},
   407  					},
   408  				},
   409  			).
   410  			Build()
   411  
   412  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   413  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   414  
   415  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   416  
   417  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   418  			NamespacedName: types.NamespacedName{
   419  				Namespace: "test",
   420  				Name:      "test",
   421  			},
   422  		})
   423  		require.NoError(t, err)
   424  		require.NotNil(t, result)
   425  
   426  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   427  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   428  		require.Error(t, err, "Empty CiliumEnvoyConfig must be removed")
   429  		require.True(t, k8sApiErrors.IsNotFound(err))
   430  	})
   431  
   432  	t.Run("Reconcile of non Cilium Ingress will cleanup any potentially existing resources (dedicated and shared) and reset the Ingress status", func(t *testing.T) {
   433  		fakeClient := fake.NewClientBuilder().
   434  			WithScheme(testScheme()).
   435  			WithObjects(
   436  				&networkingv1.Ingress{
   437  					ObjectMeta: metav1.ObjectMeta{
   438  						Namespace: "test",
   439  						Name:      "test",
   440  						Annotations: map[string]string{
   441  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   442  						},
   443  					},
   444  					Spec: networkingv1.IngressSpec{
   445  						IngressClassName: model.AddressOf("other"),
   446  						DefaultBackend:   defaultBackend(),
   447  					},
   448  					Status: networkingv1.IngressStatus{
   449  						LoadBalancer: networkingv1.IngressLoadBalancerStatus{
   450  							Ingress: []networkingv1.IngressLoadBalancerIngress{
   451  								{
   452  									IP: "172.21.255.202",
   453  								},
   454  							},
   455  						},
   456  					},
   457  				},
   458  				&corev1.Service{
   459  					ObjectMeta: metav1.ObjectMeta{
   460  						Namespace: "test",
   461  						Name:      "cilium-ingress-test",
   462  					},
   463  				},
   464  				&corev1.Endpoints{
   465  					ObjectMeta: metav1.ObjectMeta{
   466  						Namespace: "test",
   467  						Name:      "cilium-ingress-test",
   468  					},
   469  				},
   470  				&ciliumv2.CiliumEnvoyConfig{
   471  					ObjectMeta: metav1.ObjectMeta{
   472  						Namespace: "test",
   473  						Name:      "cilium-ingress-test-test",
   474  					},
   475  				},
   476  				&ciliumv2.CiliumEnvoyConfig{
   477  					ObjectMeta: metav1.ObjectMeta{
   478  						Namespace: testCiliumNamespace,
   479  						Name:      testDefaultLoadbalancingServiceName,
   480  					},
   481  					Spec: ciliumv2.CiliumEnvoyConfigSpec{
   482  						Resources: []ciliumv2.XDSResource{
   483  							{
   484  								Any: &anypb.Any{
   485  									TypeUrl: envoy.ListenerTypeURL,
   486  								},
   487  							},
   488  						},
   489  					},
   490  				},
   491  			).
   492  			Build()
   493  
   494  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   495  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   496  
   497  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   498  
   499  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   500  			NamespacedName: types.NamespacedName{
   501  				Namespace: "test",
   502  				Name:      "test",
   503  			},
   504  		})
   505  		require.NoError(t, err)
   506  		require.NotNil(t, result)
   507  
   508  		svc := corev1.Service{}
   509  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   510  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer service should be cleaned up")
   511  
   512  		ep := corev1.Endpoints{}
   513  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   514  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer endpoints should be cleaned up")
   515  
   516  		cec := ciliumv2.CiliumEnvoyConfig{}
   517  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   518  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated CiliumEnvoyConfig should be cleaned up")
   519  
   520  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   521  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   522  		require.Error(t, err, "Empty CiliumEnvoyConfig must be removed")
   523  		require.True(t, k8sApiErrors.IsNotFound(err))
   524  
   525  		ingress := networkingv1.Ingress{}
   526  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "test"}, &ingress)
   527  		require.NoError(t, err)
   528  		require.Empty(t, ingress.Status.LoadBalancer.Ingress, "Loadbalancer status of Ingress should be reset")
   529  	})
   530  
   531  	t.Run("Reconcile of dedicated Cilium Ingress with loadbalancer class will create the dedicated loadbalancer service with the specified class", func(t *testing.T) {
   532  		fakeClient := fake.NewClientBuilder().
   533  			WithScheme(testScheme()).
   534  			WithObjects(
   535  				&networkingv1.Ingress{
   536  					ObjectMeta: metav1.ObjectMeta{
   537  						Namespace: "test",
   538  						Name:      "test",
   539  						Annotations: map[string]string{
   540  							"ingress.cilium.io/loadbalancer-mode":  "dedicated",
   541  							"ingress.cilium.io/loadbalancer-class": "dummy",
   542  						},
   543  					},
   544  					Spec: networkingv1.IngressSpec{
   545  						IngressClassName: model.AddressOf("cilium"),
   546  						DefaultBackend:   defaultBackend(),
   547  					},
   548  				},
   549  			).
   550  			Build()
   551  
   552  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   553  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   554  
   555  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   556  
   557  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   558  			NamespacedName: types.NamespacedName{
   559  				Namespace: "test",
   560  				Name:      "test",
   561  			},
   562  		})
   563  		require.NoError(t, err)
   564  		require.NotNil(t, result)
   565  
   566  		svc := corev1.Service{}
   567  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   568  		require.NoError(t, err, "Dedicated loadbalancer service should exist")
   569  		require.Equal(t, "dummy", *svc.Spec.LoadBalancerClass, "Dedicated loadbalancer service should haver the specified class")
   570  	})
   571  
   572  	t.Run("Reconcile of shared Cilium Ingress with loadbalancer class will not create a dedicated load balancer", func(t *testing.T) {
   573  		fakeClient := fake.NewClientBuilder().
   574  			WithScheme(testScheme()).
   575  			WithObjects(
   576  				&networkingv1.Ingress{
   577  					ObjectMeta: metav1.ObjectMeta{
   578  						Namespace: "test",
   579  						Name:      "test",
   580  						Annotations: map[string]string{
   581  							"ingress.cilium.io/loadbalancer-mode":  "shared",
   582  							"ingress.cilium.io/loadbalancer-class": "dummy",
   583  						},
   584  					},
   585  					Spec: networkingv1.IngressSpec{
   586  						IngressClassName: model.AddressOf("cilium"),
   587  						DefaultBackend:   defaultBackend(),
   588  					},
   589  				},
   590  			).
   591  			Build()
   592  
   593  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   594  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   595  
   596  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   597  
   598  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   599  			NamespacedName: types.NamespacedName{
   600  				Namespace: "test",
   601  				Name:      "test",
   602  			},
   603  		})
   604  		require.NoError(t, err)
   605  		require.NotNil(t, result)
   606  
   607  		sharedCEC := ciliumv2.CiliumEnvoyConfig{}
   608  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: testCiliumNamespace, Name: testDefaultLoadbalancingServiceName}, &sharedCEC)
   609  		require.NoError(t, err, "Shared CiliumEnvoyConfig should exist for shared Ingress")
   610  		require.NotEmpty(t, sharedCEC.Spec.Resources)
   611  
   612  		svc := corev1.Service{}
   613  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   614  		require.True(t, k8sApiErrors.IsNotFound(err), "Dedicated loadbalancer service should not exist for shared Ingress")
   615  	})
   616  
   617  	t.Run("Reconcile of dedicated Cilium Ingress will update the status according to the IP of the dedicated loadbalancer service", func(t *testing.T) {
   618  		fakeClient := fake.NewClientBuilder().
   619  			WithScheme(testScheme()).
   620  			WithObjects(
   621  				&networkingv1.Ingress{
   622  					ObjectMeta: metav1.ObjectMeta{
   623  						Namespace: "test",
   624  						Name:      "test",
   625  						Annotations: map[string]string{
   626  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   627  						},
   628  					},
   629  					Spec: networkingv1.IngressSpec{
   630  						IngressClassName: model.AddressOf("cilium"),
   631  						DefaultBackend:   defaultBackend(),
   632  					},
   633  				},
   634  				&corev1.Service{
   635  					ObjectMeta: metav1.ObjectMeta{
   636  						Namespace: "test",
   637  						Name:      "cilium-ingress-test",
   638  					},
   639  					Status: corev1.ServiceStatus{
   640  						LoadBalancer: corev1.LoadBalancerStatus{
   641  							Ingress: []corev1.LoadBalancerIngress{
   642  								{
   643  									IP: "172.21.255.202",
   644  								},
   645  							},
   646  						},
   647  					},
   648  				},
   649  			).
   650  			Build()
   651  
   652  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   653  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   654  
   655  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   656  
   657  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   658  			NamespacedName: types.NamespacedName{
   659  				Namespace: "test",
   660  				Name:      "test",
   661  			},
   662  		})
   663  		require.NoError(t, err)
   664  		require.NotNil(t, result)
   665  
   666  		ingress := networkingv1.Ingress{}
   667  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "test"}, &ingress)
   668  		require.NoError(t, err)
   669  		require.Len(t, ingress.Status.LoadBalancer.Ingress, 1, "Loadbalancer status should contain the IP of the dedicated loadbalancer service")
   670  		require.Equal(t, "172.21.255.202", ingress.Status.LoadBalancer.Ingress[0].IP, "Loadbalancer status should contain the IP of the dedicated loadbalancer service")
   671  	})
   672  
   673  	t.Run("Reconcile of shared Cilium Ingress will update the status according to the IP of the shared loadbalancer service", func(t *testing.T) {
   674  		fakeClient := fake.NewClientBuilder().
   675  			WithScheme(testScheme()).
   676  			WithObjects(
   677  				&networkingv1.Ingress{
   678  					ObjectMeta: metav1.ObjectMeta{
   679  						Namespace: "test",
   680  						Name:      "test",
   681  						Annotations: map[string]string{
   682  							"ingress.cilium.io/loadbalancer-mode": "shared",
   683  						},
   684  					},
   685  					Spec: networkingv1.IngressSpec{
   686  						IngressClassName: model.AddressOf("cilium"),
   687  						DefaultBackend:   defaultBackend(),
   688  					},
   689  				},
   690  				&corev1.Service{
   691  					ObjectMeta: metav1.ObjectMeta{
   692  						Namespace: testCiliumNamespace,
   693  						Name:      "cilium-ingress",
   694  					},
   695  					Status: corev1.ServiceStatus{
   696  						LoadBalancer: corev1.LoadBalancerStatus{
   697  							Ingress: []corev1.LoadBalancerIngress{
   698  								{
   699  									IP: "172.21.255.200",
   700  								},
   701  							},
   702  						},
   703  					},
   704  				},
   705  			).
   706  			Build()
   707  
   708  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   709  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   710  
   711  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   712  
   713  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   714  			NamespacedName: types.NamespacedName{
   715  				Namespace: "test",
   716  				Name:      "test",
   717  			},
   718  		})
   719  		require.NoError(t, err)
   720  		require.NotNil(t, result)
   721  
   722  		ingress := networkingv1.Ingress{}
   723  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "test"}, &ingress)
   724  		require.NoError(t, err)
   725  		require.Len(t, ingress.Status.LoadBalancer.Ingress, 1, "Loadbalancer status should contain the IP of the shared loadbalancer service")
   726  		require.Equal(t, "172.21.255.200", ingress.Status.LoadBalancer.Ingress[0].IP, "Loadbalancer status should contain the IP of the shared loadbalancer service")
   727  	})
   728  
   729  	t.Run("Errors during the model translation are reported via error and result in re-enqueuing the reconcile request", func(t *testing.T) {
   730  		fakeClient := fake.NewClientBuilder().
   731  			WithScheme(testScheme()).
   732  			WithObjects(
   733  				&networkingv1.Ingress{
   734  					ObjectMeta: metav1.ObjectMeta{
   735  						Namespace: "test",
   736  						Name:      "test",
   737  						Annotations: map[string]string{
   738  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   739  						},
   740  					},
   741  					Spec: networkingv1.IngressSpec{
   742  						IngressClassName: model.AddressOf("cilium"),
   743  					},
   744  				},
   745  			).
   746  			Build()
   747  
   748  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   749  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   750  
   751  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   752  
   753  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   754  			NamespacedName: types.NamespacedName{
   755  				Namespace: "test",
   756  				Name:      "test",
   757  			},
   758  		})
   759  		require.ErrorContains(t, err, "model source can't be empty")
   760  		require.NotNil(t, result)
   761  	})
   762  
   763  	t.Run("Annotations and labels from the Ingress resource should be propagated to the Service if they match the configured prefixes", func(t *testing.T) {
   764  		fakeClient := fake.NewClientBuilder().
   765  			WithScheme(testScheme()).
   766  			WithObjects(
   767  				&networkingv1.Ingress{
   768  					ObjectMeta: metav1.ObjectMeta{
   769  						Namespace: "test",
   770  						Name:      "test",
   771  						Annotations: map[string]string{
   772  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   773  							"test.acme.io/test-annotation":        "test",
   774  							"other.acme.io/test":                  "test",
   775  						},
   776  						Labels: map[string]string{
   777  							"test.acme.io/test-label": "test",
   778  							"other.acme.io/test":      "test",
   779  						},
   780  					},
   781  					Spec: networkingv1.IngressSpec{
   782  						IngressClassName: model.AddressOf("cilium"),
   783  						DefaultBackend:   defaultBackend(),
   784  					},
   785  				},
   786  			).
   787  			Build()
   788  
   789  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   790  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   791  
   792  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{"test.acme.io/"}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   793  
   794  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   795  			NamespacedName: types.NamespacedName{
   796  				Namespace: "test",
   797  				Name:      "test",
   798  			},
   799  		})
   800  		require.NoError(t, err)
   801  		require.NotNil(t, result)
   802  
   803  		svc := corev1.Service{}
   804  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   805  		require.NoError(t, err)
   806  
   807  		require.Equal(t, map[string]string{"cilium.io/ingress": "true", "test.acme.io/test-label": "test"}, svc.Labels)
   808  		require.Equal(t, map[string]string{"test.acme.io/test-annotation": "test"}, svc.Annotations)
   809  	})
   810  
   811  	t.Run("Additional existing annotations and labels on the Service, Endpoints & CEC should be preserved", func(t *testing.T) {
   812  		fakeClient := fake.NewClientBuilder().
   813  			WithScheme(testScheme()).
   814  			WithObjects(
   815  				&networkingv1.Ingress{
   816  					ObjectMeta: metav1.ObjectMeta{
   817  						Namespace: "test",
   818  						Name:      "test",
   819  						Annotations: map[string]string{
   820  							"ingress.cilium.io/loadbalancer-mode": "dedicated",
   821  							"test.acme.io/test-annotation":        "test",
   822  							"other.acme.io/test-annotation":       "test",
   823  						},
   824  						Labels: map[string]string{
   825  							"test.acme.io/test-label":  "test",
   826  							"other.acme.io/test-label": "test",
   827  						},
   828  					},
   829  					Spec: networkingv1.IngressSpec{
   830  						IngressClassName: model.AddressOf("cilium"),
   831  						DefaultBackend:   defaultBackend(),
   832  					},
   833  				},
   834  				&corev1.Service{
   835  					ObjectMeta: metav1.ObjectMeta{
   836  						Namespace: "test",
   837  						Name:      "cilium-ingress-test",
   838  						Annotations: map[string]string{
   839  							"additional.annotation/test-annotation": "test",
   840  						},
   841  						Labels: map[string]string{
   842  							"cilium.io/ingress":           "false",
   843  							"additional.label/test-label": "test",
   844  						},
   845  					},
   846  				},
   847  				&corev1.Endpoints{
   848  					ObjectMeta: metav1.ObjectMeta{
   849  						Namespace: "test",
   850  						Name:      "cilium-ingress-test",
   851  						Annotations: map[string]string{
   852  							"additional.annotation/test-annotation": "test",
   853  						},
   854  						Labels: map[string]string{
   855  							"additional.label/test-label": "test",
   856  						},
   857  					},
   858  				},
   859  				&ciliumv2.CiliumEnvoyConfig{
   860  					ObjectMeta: metav1.ObjectMeta{
   861  						Namespace: "test",
   862  						Name:      "cilium-ingress-test-test",
   863  						Annotations: map[string]string{
   864  							"additional.annotation/test-annotation": "test",
   865  						},
   866  						Labels: map[string]string{
   867  							"additional.label/test-label": "test",
   868  						},
   869  					},
   870  				},
   871  			).
   872  			Build()
   873  
   874  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   875  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   876  
   877  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   878  
   879  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   880  			NamespacedName: types.NamespacedName{
   881  				Namespace: "test",
   882  				Name:      "test",
   883  			},
   884  		})
   885  		require.NoError(t, err)
   886  		require.NotNil(t, result)
   887  
   888  		svc := corev1.Service{}
   889  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   890  		require.NoError(t, err)
   891  
   892  		require.Contains(t, svc.Labels, "cilium.io/ingress", "Existing labels should be overwritten if they have the same key")
   893  		require.Equal(t, "true", svc.Labels["cilium.io/ingress"], "Existing label should be overwritten if they have the same key")
   894  
   895  		require.Contains(t, svc.Labels, "additional.label/test-label", "Existing labels should not be deleted")
   896  		require.Contains(t, svc.Annotations, "additional.annotation/test-annotation", "Existing annotations should not be deleted")
   897  
   898  		ep := corev1.Endpoints{}
   899  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &ep)
   900  		require.NoError(t, err)
   901  
   902  		require.Contains(t, ep.Labels, "additional.label/test-label", "Existing labels should not be deleted")
   903  		require.Contains(t, ep.Annotations, "additional.annotation/test-annotation", "Existing annotations should not be deleted")
   904  
   905  		cec := ciliumv2.CiliumEnvoyConfig{}
   906  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &cec)
   907  		require.NoError(t, err)
   908  
   909  		require.Contains(t, cec.Labels, "additional.label/test-label", "Existing labels should not be deleted")
   910  		require.Contains(t, cec.Annotations, "additional.annotation/test-annotation", "Existing annotations should not be deleted")
   911  	})
   912  
   913  	t.Run("Existing loadBalancerClass on Service should not be overwritten (e.g. scenarios where this gets set by a mutating webhook)", func(t *testing.T) {
   914  		fakeClient := fake.NewClientBuilder().
   915  			WithScheme(testScheme()).
   916  			WithObjects(
   917  				&networkingv1.Ingress{
   918  					ObjectMeta: metav1.ObjectMeta{
   919  						Namespace: "test",
   920  						Name:      "test",
   921  					},
   922  					Spec: networkingv1.IngressSpec{
   923  						IngressClassName: model.AddressOf("cilium"),
   924  						DefaultBackend:   defaultBackend(),
   925  					},
   926  				},
   927  				&corev1.Service{
   928  					ObjectMeta: metav1.ObjectMeta{
   929  						Namespace: "test",
   930  						Name:      "cilium-ingress-test",
   931  					},
   932  					Spec: corev1.ServiceSpec{
   933  						LoadBalancerClass: model.AddressOf("service.k8s.aws/nlb"),
   934  					},
   935  				},
   936  			).
   937  			Build()
   938  
   939  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   940  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   941  
   942  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   943  
   944  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   945  			NamespacedName: types.NamespacedName{
   946  				Namespace: "test",
   947  				Name:      "test",
   948  			},
   949  		})
   950  		require.NoError(t, err)
   951  		require.NotNil(t, result)
   952  
   953  		svc := corev1.Service{}
   954  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &svc)
   955  		require.NoError(t, err)
   956  
   957  		require.Equal(t, model.AddressOf("service.k8s.aws/nlb"), svc.Spec.LoadBalancerClass, "LoadbalancerClass should be preserved during reconciliation")
   958  	})
   959  
   960  	t.Run("If the deletionTimestamp is set (foreground deletion), no dependent objects should be modified or created", func(t *testing.T) {
   961  		fakeClient := fake.NewClientBuilder().
   962  			WithScheme(testScheme()).
   963  			WithObjects(
   964  				&networkingv1.Ingress{
   965  					ObjectMeta: metav1.ObjectMeta{
   966  						Namespace:         "test",
   967  						Name:              "test",
   968  						DeletionTimestamp: model.AddressOf(metav1.Now()),
   969  						Finalizers: []string{
   970  							"foregroundDeletion",
   971  						},
   972  					},
   973  					Spec: networkingv1.IngressSpec{
   974  						IngressClassName: model.AddressOf("cilium"),
   975  						DefaultBackend:   defaultBackend(),
   976  					},
   977  				},
   978  			).
   979  			Build()
   980  
   981  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
   982  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
   983  
   984  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
   985  
   986  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
   987  			NamespacedName: types.NamespacedName{
   988  				Namespace: "test",
   989  				Name:      "test",
   990  			},
   991  		})
   992  		require.NoError(t, err)
   993  		require.NotNil(t, result)
   994  
   995  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Service{})
   996  		require.True(t, k8sApiErrors.IsNotFound(err), "Service should not be created")
   997  
   998  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Endpoints{})
   999  		require.True(t, k8sApiErrors.IsNotFound(err), "Endpoints should not be created")
  1000  
  1001  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &ciliumv2.CiliumEnvoyConfig{})
  1002  		require.True(t, k8sApiErrors.IsNotFound(err), "CiliumEnvoyConfig should not be created")
  1003  	})
  1004  
  1005  	t.Run("If create operations fail due to namespace termination, no error should be reported", func(t *testing.T) {
  1006  		fakeClient := fake.NewClientBuilder().
  1007  			WithScheme(testScheme()).
  1008  			WithObjects(
  1009  				&networkingv1.Ingress{
  1010  					ObjectMeta: metav1.ObjectMeta{
  1011  						Namespace: "test",
  1012  						Name:      "test",
  1013  					},
  1014  					Spec: networkingv1.IngressSpec{
  1015  						IngressClassName: model.AddressOf("cilium"),
  1016  						DefaultBackend:   defaultBackend(),
  1017  					},
  1018  				},
  1019  			).
  1020  			WithInterceptorFuncs(interceptor.Funcs{
  1021  				Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
  1022  					return &k8sApiErrors.StatusError{
  1023  						ErrStatus: metav1.Status{
  1024  							Message: "unable to create new content in namespace test because it is being terminated",
  1025  							Reason:  metav1.StatusReasonForbidden,
  1026  							Details: &metav1.StatusDetails{
  1027  								Causes: []metav1.StatusCause{
  1028  									{
  1029  										Type: corev1.NamespaceTerminatingCause,
  1030  									},
  1031  								},
  1032  							},
  1033  						},
  1034  					}
  1035  				},
  1036  			}).
  1037  			Build()
  1038  
  1039  		cecTranslator := translation.NewCECTranslator(testCiliumSecretsNamespace, testUseProxyProtocol, false, false, testDefaultTimeout, false, nil, false, false, 0)
  1040  		dedicatedIngressTranslator := ingressTranslation.NewDedicatedIngressTranslator(cecTranslator, false)
  1041  
  1042  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, false, 0)
  1043  
  1044  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
  1045  			NamespacedName: types.NamespacedName{
  1046  				Namespace: "test",
  1047  				Name:      "test",
  1048  			},
  1049  		})
  1050  		require.NoError(t, err)
  1051  		require.NotNil(t, result)
  1052  
  1053  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Service{})
  1054  		require.True(t, k8sApiErrors.IsNotFound(err), "Service should not be created")
  1055  
  1056  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test"}, &corev1.Endpoints{})
  1057  		require.True(t, k8sApiErrors.IsNotFound(err), "Endpoints should not be created")
  1058  
  1059  		err = fakeClient.Get(context.Background(), types.NamespacedName{Namespace: "test", Name: "cilium-ingress-test-test"}, &ciliumv2.CiliumEnvoyConfig{})
  1060  		require.True(t, k8sApiErrors.IsNotFound(err), "CiliumEnvoyConfig should not be created")
  1061  	})
  1062  	t.Run("Reconcile of shared Cilium Ingress with external LB support will pass the configured port via model to the CEC Translator", func(t *testing.T) {
  1063  		fakeClient := fake.NewClientBuilder().
  1064  			WithScheme(testScheme()).
  1065  			WithObjects(
  1066  				&networkingv1.Ingress{
  1067  					ObjectMeta: metav1.ObjectMeta{
  1068  						Namespace: "test",
  1069  						Name:      "test",
  1070  						Annotations: map[string]string{
  1071  							"ingress.cilium.io/loadbalancer-mode": "shared",
  1072  						},
  1073  					},
  1074  					Spec: networkingv1.IngressSpec{
  1075  						IngressClassName: model.AddressOf("cilium"),
  1076  						DefaultBackend:   defaultBackend(),
  1077  					},
  1078  				},
  1079  			).
  1080  			Build()
  1081  
  1082  		cecTranslator := &fakeCECTranslator{}
  1083  
  1084  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, nil, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, true, 55555)
  1085  
  1086  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
  1087  			NamespacedName: types.NamespacedName{
  1088  				Namespace: "test",
  1089  				Name:      "test",
  1090  			},
  1091  		})
  1092  		require.NoError(t, err)
  1093  		require.NotNil(t, result)
  1094  
  1095  		assert.Empty(t, cecTranslator.model.TLSPassthrough)
  1096  		assert.Len(t, cecTranslator.model.HTTP, 1)
  1097  		assert.Equal(t, uint32(55555), cecTranslator.model.HTTP[0].Port)
  1098  	})
  1099  
  1100  	t.Run("Reconcile of dedicated Cilium Ingress with external LB support will pass the annotated port to the CEC Translator", func(t *testing.T) {
  1101  		fakeClient := fake.NewClientBuilder().
  1102  			WithScheme(testScheme()).
  1103  			WithObjects(
  1104  				&networkingv1.Ingress{
  1105  					ObjectMeta: metav1.ObjectMeta{
  1106  						Namespace: "test",
  1107  						Name:      "test",
  1108  						Annotations: map[string]string{
  1109  							"ingress.cilium.io/loadbalancer-mode":  "dedicated",
  1110  							"ingress.cilium.io/host-listener-port": "55555",
  1111  						},
  1112  					},
  1113  					Spec: networkingv1.IngressSpec{
  1114  						IngressClassName: model.AddressOf("cilium"),
  1115  						DefaultBackend:   defaultBackend(),
  1116  					},
  1117  				},
  1118  			).
  1119  			Build()
  1120  
  1121  		cecTranslator := &fakeCECTranslator{}
  1122  		dedicatedIngressTranslator := &fakeDedicatedIngressTranslator{}
  1123  
  1124  		reconciler := newIngressReconciler(logger, fakeClient, cecTranslator, dedicatedIngressTranslator, testCiliumNamespace, []string{}, testDefaultLoadbalancingServiceName, "dedicated", testDefaultSecretNamespace, testDefaultSecretName, false, testIngressDefaultRequestTimeout, true, 0)
  1125  
  1126  		result, err := reconciler.Reconcile(context.Background(), reconcile.Request{
  1127  			NamespacedName: types.NamespacedName{
  1128  				Namespace: "test",
  1129  				Name:      "test",
  1130  			},
  1131  		})
  1132  		require.NoError(t, err)
  1133  		require.NotNil(t, result)
  1134  
  1135  		assert.Empty(t, dedicatedIngressTranslator.model.TLSPassthrough)
  1136  		assert.Len(t, dedicatedIngressTranslator.model.HTTP, 1)
  1137  		assert.Equal(t, uint32(55555), dedicatedIngressTranslator.model.HTTP[0].Port)
  1138  	})
  1139  }
  1140  
  1141  var _ translation.CECTranslator = &fakeCECTranslator{}
  1142  
  1143  type fakeCECTranslator struct {
  1144  	model *model.Model
  1145  }
  1146  
  1147  func (r *fakeCECTranslator) WithUseAlpn(useAlpn bool) {
  1148  }
  1149  
  1150  func (r *fakeCECTranslator) Translate(namespace string, name string, model *model.Model) (*ciliumv2.CiliumEnvoyConfig, error) {
  1151  	r.model = model
  1152  
  1153  	return &ciliumv2.CiliumEnvoyConfig{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "test"}}, nil
  1154  }
  1155  
  1156  var _ translation.Translator = &fakeDedicatedIngressTranslator{}
  1157  
  1158  type fakeDedicatedIngressTranslator struct {
  1159  	model *model.Model
  1160  }
  1161  
  1162  func (r *fakeDedicatedIngressTranslator) Translate(model *model.Model) (*ciliumv2.CiliumEnvoyConfig, *corev1.Service, *corev1.Endpoints, error) {
  1163  	r.model = model
  1164  
  1165  	return &ciliumv2.CiliumEnvoyConfig{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "test"}},
  1166  		&corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "test"}},
  1167  		&corev1.Endpoints{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "test"}},
  1168  		nil
  1169  }
  1170  
  1171  func defaultBackend() *networkingv1.IngressBackend {
  1172  	return &networkingv1.IngressBackend{
  1173  		Service: &networkingv1.IngressServiceBackend{
  1174  			Name: "test",
  1175  			Port: networkingv1.ServiceBackendPort{
  1176  				Number: 8080,
  1177  			},
  1178  		},
  1179  	}
  1180  }
  1181  
  1182  func TestGetSharedListenerPorts(t *testing.T) {
  1183  	testCases := []struct {
  1184  		desc                     string
  1185  		hostNetworkEnabled       bool
  1186  		hostNetworkSharedPort    uint32
  1187  		expectedPassthroughPort  uint32
  1188  		expectedInsecureHTTPPort uint32
  1189  		expectedSecureHTTPPort   uint32
  1190  	}{
  1191  		{
  1192  			desc:                     "no external loadbalancer",
  1193  			hostNetworkEnabled:       false,
  1194  			expectedPassthroughPort:  443,
  1195  			expectedInsecureHTTPPort: 80,
  1196  			expectedSecureHTTPPort:   443,
  1197  		},
  1198  		{
  1199  			desc:                     "external loadbalancer with port 0",
  1200  			hostNetworkEnabled:       true,
  1201  			hostNetworkSharedPort:    0,
  1202  			expectedPassthroughPort:  8080,
  1203  			expectedInsecureHTTPPort: 8080,
  1204  			expectedSecureHTTPPort:   8080,
  1205  		},
  1206  		{
  1207  			desc:                     "external loadbalancer with port 55555",
  1208  			hostNetworkEnabled:       true,
  1209  			hostNetworkSharedPort:    55555,
  1210  			expectedPassthroughPort:  55555,
  1211  			expectedInsecureHTTPPort: 55555,
  1212  			expectedSecureHTTPPort:   55555,
  1213  		},
  1214  	}
  1215  	for _, tC := range testCases {
  1216  		t.Run(tC.desc, func(t *testing.T) {
  1217  			ir := ingressReconciler{
  1218  				hostNetworkEnabled:    tC.hostNetworkEnabled,
  1219  				hostNetworkSharedPort: tC.hostNetworkSharedPort,
  1220  			}
  1221  
  1222  			passthrough, insecureHTTP, secureHTTP := ir.getSharedListenerPorts()
  1223  
  1224  			assert.Equal(t, tC.expectedPassthroughPort, passthrough)
  1225  			assert.Equal(t, tC.expectedInsecureHTTPPort, insecureHTTP)
  1226  			assert.Equal(t, tC.expectedSecureHTTPPort, secureHTTP)
  1227  		})
  1228  	}
  1229  }
  1230  
  1231  func TestGetDedicatedListenerPorts(t *testing.T) {
  1232  	testCases := []struct {
  1233  		desc                     string
  1234  		hostNetworkEnabled       bool
  1235  		ingressAnnotations       map[string]string
  1236  		expectedPassthroughPort  uint32
  1237  		expectedInsecureHTTPPort uint32
  1238  		expectedSecureHTTPPort   uint32
  1239  	}{
  1240  		{
  1241  			desc:                     "no hostnetwork mode",
  1242  			hostNetworkEnabled:       false,
  1243  			expectedPassthroughPort:  443,
  1244  			expectedInsecureHTTPPort: 80,
  1245  			expectedSecureHTTPPort:   443,
  1246  		},
  1247  		{
  1248  			desc:               "hostnetwork without port annotation",
  1249  			hostNetworkEnabled: true,
  1250  			ingressAnnotations: map[string]string{
  1251  				"ingress.cilium.io/host-listener-port": "55555",
  1252  			},
  1253  			expectedPassthroughPort:  55555,
  1254  			expectedInsecureHTTPPort: 55555,
  1255  			expectedSecureHTTPPort:   55555,
  1256  		},
  1257  		{
  1258  			desc:               "hostnetwork with port annotation of value 0",
  1259  			hostNetworkEnabled: true,
  1260  			ingressAnnotations: map[string]string{
  1261  				"ingress.cilium.io/host-listener-port": "0",
  1262  			},
  1263  			expectedPassthroughPort:  8080,
  1264  			expectedInsecureHTTPPort: 8080,
  1265  			expectedSecureHTTPPort:   8080,
  1266  		},
  1267  		{
  1268  			desc:               "hostnetwork with invalid value",
  1269  			hostNetworkEnabled: true,
  1270  			ingressAnnotations: map[string]string{
  1271  				"ingress.cilium.io/host-listener-port": "invalid",
  1272  			},
  1273  			expectedPassthroughPort:  8080,
  1274  			expectedInsecureHTTPPort: 8080,
  1275  			expectedSecureHTTPPort:   8080,
  1276  		},
  1277  	}
  1278  	for _, tC := range testCases {
  1279  		t.Run(tC.desc, func(t *testing.T) {
  1280  			logger := logrus.New()
  1281  			logger.SetOutput(io.Discard)
  1282  			ir := ingressReconciler{
  1283  				logger:             logger,
  1284  				hostNetworkEnabled: tC.hostNetworkEnabled,
  1285  			}
  1286  
  1287  			ingress := &networkingv1.Ingress{
  1288  				ObjectMeta: metav1.ObjectMeta{
  1289  					Annotations: tC.ingressAnnotations,
  1290  				},
  1291  			}
  1292  			passthrough, insecureHTTP, secureHTTP := ir.getDedicatedListenerPorts(ingress)
  1293  
  1294  			assert.Equal(t, tC.expectedPassthroughPort, passthrough)
  1295  			assert.Equal(t, tC.expectedInsecureHTTPPort, insecureHTTP)
  1296  			assert.Equal(t, tC.expectedSecureHTTPPort, secureHTTP)
  1297  		})
  1298  	}
  1299  }