github.com/cilium/cilium@v1.16.2/operator/pkg/secretsync/secretsync_reconcile_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package secretsync_test
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"testing"
    10  
    11  	"github.com/sirupsen/logrus"
    12  	"github.com/stretchr/testify/require"
    13  	corev1 "k8s.io/api/core/v1"
    14  	networkingv1 "k8s.io/api/networking/v1"
    15  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/runtime"
    18  	"k8s.io/apimachinery/pkg/types"
    19  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    20  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    21  	ctrl "sigs.k8s.io/controller-runtime"
    22  	"sigs.k8s.io/controller-runtime/pkg/client"
    23  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    24  	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
    25  
    26  	gateway_api "github.com/cilium/cilium/operator/pkg/gateway-api"
    27  	"github.com/cilium/cilium/operator/pkg/ingress"
    28  	"github.com/cilium/cilium/operator/pkg/model"
    29  	"github.com/cilium/cilium/operator/pkg/secretsync"
    30  	ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    31  )
    32  
    33  var secretsNamespace = "cilium-secrets-test"
    34  
    35  var secretFixture = []client.Object{
    36  	&corev1.Secret{
    37  		ObjectMeta: metav1.ObjectMeta{
    38  			Namespace: secretsNamespace,
    39  			Name:      "test-synced-secret-no-source",
    40  			Labels: map[string]string{
    41  				secretsync.OwningSecretNamespace: "test",
    42  				secretsync.OwningSecretName:      "synced-secret-no-source",
    43  			},
    44  		},
    45  	},
    46  	&corev1.Secret{
    47  		ObjectMeta: metav1.ObjectMeta{
    48  			Namespace: "test",
    49  			Name:      "synced-secret-no-reference",
    50  		},
    51  	},
    52  	&corev1.Secret{
    53  		ObjectMeta: metav1.ObjectMeta{
    54  			Namespace: secretsNamespace,
    55  			Name:      "test-synced-secret-no-reference",
    56  			Labels: map[string]string{
    57  				secretsync.OwningSecretNamespace: "test",
    58  				secretsync.OwningSecretName:      "syced-secret-no-reference",
    59  			},
    60  		},
    61  	},
    62  	&corev1.Secret{
    63  		ObjectMeta: metav1.ObjectMeta{
    64  			Namespace: "test",
    65  			Name:      "synced-secret-with-source-and-ref",
    66  		},
    67  	},
    68  	&corev1.Secret{
    69  		ObjectMeta: metav1.ObjectMeta{
    70  			Namespace: secretsNamespace,
    71  			Name:      "test-synced-secret-with-source-and-ref",
    72  			Labels: map[string]string{
    73  				secretsync.OwningSecretNamespace: "test",
    74  				secretsync.OwningSecretName:      "synced-secret-with-source-and-ref",
    75  			},
    76  		},
    77  	},
    78  	&gatewayv1.GatewayClass{
    79  		ObjectMeta: metav1.ObjectMeta{
    80  			Name: "cilium",
    81  		},
    82  		Spec: gatewayv1.GatewayClassSpec{
    83  			ControllerName: "io.cilium/gateway-controller",
    84  		},
    85  	},
    86  	&gatewayv1.Gateway{
    87  		ObjectMeta: metav1.ObjectMeta{
    88  			Namespace: "test",
    89  			Name:      "valid-gateway",
    90  		},
    91  		Spec: gatewayv1.GatewaySpec{
    92  			GatewayClassName: "cilium",
    93  			Listeners: []gatewayv1.Listener{
    94  				{
    95  					Name:     "https",
    96  					Port:     443,
    97  					Hostname: model.AddressOf[gatewayv1.Hostname]("*.cilium.io"),
    98  					Protocol: "HTTPS",
    99  					TLS: &gatewayv1.GatewayTLSConfig{
   100  						CertificateRefs: []gatewayv1.SecretObjectReference{
   101  							{
   102  								Name: "synced-secret-with-source-and-ref",
   103  							},
   104  							{
   105  								Name: "secret-with-ref-not-synced",
   106  							},
   107  						},
   108  					},
   109  				},
   110  			},
   111  		},
   112  	},
   113  	&corev1.Secret{
   114  		ObjectMeta: metav1.ObjectMeta{
   115  			Namespace: "test",
   116  			Name:      "secret-with-ref-not-synced",
   117  		},
   118  	},
   119  	&gatewayv1.GatewayClass{
   120  		ObjectMeta: metav1.ObjectMeta{
   121  			Name: "third-party",
   122  		},
   123  		Spec: gatewayv1.GatewayClassSpec{
   124  			ControllerName: "third-party",
   125  		},
   126  	},
   127  	&gatewayv1.Gateway{
   128  		ObjectMeta: metav1.ObjectMeta{
   129  			Namespace: "test",
   130  			Name:      "valid-gateway-non-cilium",
   131  		},
   132  		Spec: gatewayv1.GatewaySpec{
   133  			GatewayClassName: "third-party",
   134  			Listeners: []gatewayv1.Listener{
   135  				{
   136  					Name:     "https",
   137  					Port:     443,
   138  					Hostname: model.AddressOf[gatewayv1.Hostname]("*.acme.io"),
   139  					Protocol: "HTTPS",
   140  					TLS: &gatewayv1.GatewayTLSConfig{
   141  						CertificateRefs: []gatewayv1.SecretObjectReference{
   142  							{
   143  								Name: "secret-with-non-cilium-ref",
   144  							},
   145  						},
   146  					},
   147  				},
   148  			},
   149  		},
   150  	},
   151  	&corev1.Secret{
   152  		ObjectMeta: metav1.ObjectMeta{
   153  			Namespace: "test",
   154  			Name:      "secret-shared-not-synced",
   155  		},
   156  	},
   157  	&gatewayv1.Gateway{
   158  		ObjectMeta: metav1.ObjectMeta{
   159  			Namespace: "test",
   160  			Name:      "valid-gateway-shared",
   161  		},
   162  		Spec: gatewayv1.GatewaySpec{
   163  			GatewayClassName: "cilium",
   164  			Listeners: []gatewayv1.Listener{
   165  				{
   166  					Name:     "https",
   167  					Port:     443,
   168  					Hostname: model.AddressOf[gatewayv1.Hostname]("*.cilium.io"),
   169  					Protocol: "HTTPS",
   170  					TLS: &gatewayv1.GatewayTLSConfig{
   171  						CertificateRefs: []gatewayv1.SecretObjectReference{
   172  							{
   173  								Name: "secret-shared-not-synced",
   174  							},
   175  						},
   176  					},
   177  				},
   178  			},
   179  		},
   180  	},
   181  	&networkingv1.Ingress{
   182  		ObjectMeta: metav1.ObjectMeta{
   183  			Namespace: "test",
   184  			Name:      "valid-ingress-cilium",
   185  		},
   186  		Spec: networkingv1.IngressSpec{
   187  			IngressClassName: model.AddressOf("cilium"),
   188  			TLS: []networkingv1.IngressTLS{
   189  				{
   190  					Hosts:      []string{"*.cilium.io"},
   191  					SecretName: "secret-shared-not-synced",
   192  				},
   193  			},
   194  		},
   195  	},
   196  }
   197  
   198  func Test_SecretSync_Reconcile(t *testing.T) {
   199  	logger := logrus.New()
   200  	logger.SetOutput(io.Discard)
   201  
   202  	c := fake.NewClientBuilder().
   203  		WithScheme(testScheme()).
   204  		WithObjects(secretFixture...).
   205  		Build()
   206  
   207  	r := secretsync.NewSecretSyncReconciler(c, logger, []*secretsync.SecretSyncRegistration{
   208  		{
   209  			RefObject:            &gatewayv1.Gateway{},
   210  			RefObjectEnqueueFunc: gateway_api.EnqueueTLSSecrets(c, logger),
   211  			RefObjectCheckFunc:   gateway_api.IsReferencedByCiliumGateway,
   212  			SecretsNamespace:     secretsNamespace,
   213  		},
   214  		{
   215  			RefObject:            &networkingv1.Ingress{},
   216  			RefObjectEnqueueFunc: ingress.EnqueueReferencedTLSSecrets(c, logger),
   217  			RefObjectCheckFunc:   ingress.IsReferencedByCiliumIngress,
   218  			SecretsNamespace:     secretsNamespace + "-2",
   219  		},
   220  	})
   221  
   222  	t.Run("delete synced secret if source secret doesn't exist", func(t *testing.T) {
   223  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   224  			NamespacedName: types.NamespacedName{
   225  				Namespace: "test",
   226  				Name:      "synced-secret-no-source",
   227  			},
   228  		})
   229  		require.NoError(t, err)
   230  		require.Equal(t, ctrl.Result{}, result)
   231  
   232  		secret := &corev1.Secret{}
   233  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-synced-secret-no-source"}, secret)
   234  
   235  		require.Error(t, err)
   236  		require.ErrorContains(t, err, "secrets \"test-synced-secret-no-source\" not found")
   237  	})
   238  
   239  	t.Run("delete synced secret if source secret isn't referenced by a CIlium Gateway resource", func(t *testing.T) {
   240  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   241  			NamespacedName: types.NamespacedName{
   242  				Namespace: "test",
   243  				Name:      "synced-secret-no-reference",
   244  			},
   245  		})
   246  		require.NoError(t, err)
   247  		require.Equal(t, ctrl.Result{}, result)
   248  
   249  		secret := &corev1.Secret{}
   250  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-synced-secret-no-reference"}, secret)
   251  
   252  		require.Error(t, err)
   253  		require.ErrorContains(t, err, "secrets \"test-synced-secret-no-reference\" not found")
   254  	})
   255  
   256  	t.Run("keep synced secret if source secret exists and is referenced by a Gateway resource", func(t *testing.T) {
   257  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   258  			NamespacedName: types.NamespacedName{
   259  				Namespace: "test",
   260  				Name:      "synced-secret-with-source-and-ref",
   261  			},
   262  		})
   263  		require.NoError(t, err)
   264  		require.Equal(t, ctrl.Result{}, result)
   265  
   266  		secret := &corev1.Secret{}
   267  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-synced-secret-with-source-and-ref"}, secret)
   268  		require.NoError(t, err)
   269  	})
   270  
   271  	t.Run("don't create synced secret for source secret that is referenced by a non Cilium Gateway resource", func(t *testing.T) {
   272  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   273  			NamespacedName: types.NamespacedName{
   274  				Namespace: "test",
   275  				Name:      "secret-with-non-cilium-ref",
   276  			},
   277  		})
   278  		require.NoError(t, err)
   279  		require.Equal(t, ctrl.Result{}, result)
   280  
   281  		secret := &corev1.Secret{}
   282  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-synced-secret-non-cilium-ref"}, secret)
   283  
   284  		require.Error(t, err)
   285  		require.ErrorContains(t, err, "secrets \"test-synced-secret-non-cilium-ref\" not found")
   286  	})
   287  
   288  	t.Run("create synced secret for source secret that is referenced by a Cilium Gateway resource", func(t *testing.T) {
   289  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   290  			NamespacedName: types.NamespacedName{
   291  				Namespace: "test",
   292  				Name:      "secret-with-ref-not-synced",
   293  			},
   294  		})
   295  		require.NoError(t, err)
   296  		require.Equal(t, ctrl.Result{}, result)
   297  
   298  		secret := &corev1.Secret{}
   299  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-secret-with-ref-not-synced"}, secret)
   300  		require.NoError(t, err)
   301  	})
   302  
   303  	t.Run("create synced secret in multiple namespaces for source secret that is referenced by a Gateway and Ingress", func(t *testing.T) {
   304  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   305  			NamespacedName: types.NamespacedName{
   306  				Namespace: "test",
   307  				Name:      "secret-shared-not-synced",
   308  			},
   309  		})
   310  		require.NoError(t, err)
   311  		require.Equal(t, ctrl.Result{}, result)
   312  
   313  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-secret-shared-not-synced"}, &corev1.Secret{})
   314  		require.NoError(t, err)
   315  
   316  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace + "-2", Name: "test-secret-shared-not-synced"}, &corev1.Secret{})
   317  		require.NoError(t, err)
   318  	})
   319  
   320  	t.Run("delete synced secret in ingress namespace after deleting the Ingress resource", func(t *testing.T) {
   321  		ingress := networkingv1.Ingress{
   322  			ObjectMeta: metav1.ObjectMeta{
   323  				Namespace: "test",
   324  				Name:      "valid-ingress-cilium",
   325  			},
   326  		}
   327  
   328  		var err error
   329  		err = c.Delete(context.Background(), &ingress)
   330  		require.NoError(t, err)
   331  
   332  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   333  			NamespacedName: types.NamespacedName{
   334  				Namespace: "test",
   335  				Name:      "secret-shared-not-synced",
   336  			},
   337  		})
   338  		require.NoError(t, err)
   339  		require.Equal(t, ctrl.Result{}, result)
   340  
   341  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-secret-shared-not-synced"}, &corev1.Secret{})
   342  		require.NoError(t, err)
   343  
   344  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace + "-2", Name: "test-secret-shared-not-synced"}, &corev1.Secret{})
   345  		require.True(t, k8sErrors.IsNotFound(err))
   346  	})
   347  }
   348  
   349  var secretFixtureDefaultSecret = []client.Object{
   350  	&corev1.Secret{
   351  		ObjectMeta: metav1.ObjectMeta{
   352  			Namespace: "test",
   353  			Name:      "unsynced-secret-no-reference",
   354  		},
   355  	},
   356  }
   357  
   358  func Test_SecretSync_Reconcile_WithDefaultSecret(t *testing.T) {
   359  	logger := logrus.New()
   360  	logger.SetOutput(io.Discard)
   361  
   362  	c := fake.NewClientBuilder().
   363  		WithScheme(testScheme()).
   364  		WithObjects(secretFixtureDefaultSecret...).
   365  		Build()
   366  	r := secretsync.NewSecretSyncReconciler(c, logger, []*secretsync.SecretSyncRegistration{
   367  		{
   368  			RefObject:            &gatewayv1.Gateway{},
   369  			RefObjectEnqueueFunc: gateway_api.EnqueueTLSSecrets(c, logger),
   370  			RefObjectCheckFunc:   gateway_api.IsReferencedByCiliumGateway,
   371  			SecretsNamespace:     secretsNamespace,
   372  			DefaultSecret: &secretsync.DefaultSecret{
   373  				Namespace: "test",
   374  				Name:      "unsynced-secret-no-reference",
   375  			},
   376  		},
   377  	})
   378  
   379  	t.Run("create synced secret for source secret that is the default secret and therefore doesn't need to be referenced by any Cilium Gateway resource", func(t *testing.T) {
   380  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   381  			NamespacedName: types.NamespacedName{
   382  				Namespace: "test",
   383  				Name:      "unsynced-secret-no-reference",
   384  			},
   385  		})
   386  		require.NoError(t, err)
   387  		require.Equal(t, ctrl.Result{}, result)
   388  
   389  		secret := &corev1.Secret{}
   390  		err = c.Get(context.Background(), types.NamespacedName{Namespace: secretsNamespace, Name: "test-unsynced-secret-no-reference"}, secret)
   391  		require.NoError(t, err)
   392  	})
   393  }
   394  
   395  func testScheme() *runtime.Scheme {
   396  	scheme := runtime.NewScheme()
   397  
   398  	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
   399  	utilruntime.Must(ciliumv2.AddToScheme(scheme))
   400  	utilruntime.Must(gatewayv1.AddToScheme(scheme))
   401  
   402  	return scheme
   403  }