// Copyright 2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package secrets import ( "bytes" "context" "errors" "fmt" "testing" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" kubeinformers "k8s.io/client-go/informers" kubernetesfake "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/testutil" ) func TestNewServiceAccountTokenCleanupController(t *testing.T) { namespace := "a-namespace" legacySecretName := "a-secret" observableWithInformerOption := testutil.NewObservableWithInformerOption() secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets() var log bytes.Buffer _ = NewServiceAccountTokenCleanupController( namespace, legacySecretName, nil, // not needed for this test secretsInformer, observableWithInformerOption.WithInformer, plog.TestLogger(t, &log), ) secretsInformerFilter := observableWithInformerOption.GetFilterForInformer(secretsInformer) legacySecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: legacySecretName, Namespace: namespace}, Type: corev1.SecretTypeServiceAccountToken} wrongName := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrongName", Namespace: namespace}, Type: corev1.SecretTypeServiceAccountToken} wrongType := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrongType", Namespace: namespace}, Type: "other-type"} wrongNamespace := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrongNamespace", Namespace: "wrong-namespace"}, Type: corev1.SecretTypeServiceAccountToken} wrongObject := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-map", Namespace: namespace}} require.False(t, secretsInformerFilter.Add(wrongName)) require.False(t, secretsInformerFilter.Update(wrongName, wrongNamespace)) require.False(t, secretsInformerFilter.Update(wrongNamespace, wrongName)) require.False(t, secretsInformerFilter.Delete(wrongName)) require.False(t, secretsInformerFilter.Add(wrongObject)) require.False(t, secretsInformerFilter.Update(wrongObject, wrongNamespace)) require.False(t, secretsInformerFilter.Update(wrongNamespace, wrongObject)) require.False(t, secretsInformerFilter.Delete(wrongObject)) require.False(t, secretsInformerFilter.Add(wrongNamespace)) require.False(t, secretsInformerFilter.Update(wrongNamespace, wrongObject)) require.False(t, secretsInformerFilter.Update(wrongObject, wrongNamespace)) require.False(t, secretsInformerFilter.Delete(wrongNamespace)) require.False(t, secretsInformerFilter.Add(wrongType)) require.False(t, secretsInformerFilter.Update(wrongType, wrongNamespace)) require.False(t, secretsInformerFilter.Update(wrongNamespace, wrongType)) require.False(t, secretsInformerFilter.Delete(wrongType)) require.True(t, secretsInformerFilter.Add(legacySecret)) require.True(t, secretsInformerFilter.Update(legacySecret, wrongNamespace)) require.True(t, secretsInformerFilter.Update(wrongNamespace, legacySecret)) require.True(t, secretsInformerFilter.Delete(legacySecret)) } func TestSync(t *testing.T) { kubeAPIClient := kubernetesfake.NewSimpleClientset() kubeInformerClient := kubernetesfake.NewSimpleClientset() kubeInformers := kubeinformers.NewSharedInformerFactory( kubeInformerClient, 0, ) namespace := "some-namespace" secretToDelete := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret-to-delete", Namespace: namespace, }, Type: corev1.SecretTypeServiceAccountToken, } secretWithWrongName := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "wrong-name", Namespace: namespace, }, Type: corev1.SecretTypeServiceAccountToken, } secretWithWrongType := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret-to-leave-alone", Namespace: namespace, }, } secretWithWrongNamespace := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret-to-delete", Namespace: "other", }, Type: corev1.SecretTypeServiceAccountToken, } require.NoError(t, kubeAPIClient.Tracker().Add(secretToDelete)) require.NoError(t, kubeInformerClient.Tracker().Add(secretToDelete)) require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongName)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongName)) require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongType)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongType)) require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongNamespace)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongNamespace)) var log bytes.Buffer controller := NewServiceAccountTokenCleanupController( namespace, "secret-to-delete", kubeAPIClient, kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, plog.TestLogger(t, &log), ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Must start informers before calling TestRunSynchronously(). kubeInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) err := controllerlib.TestSync(t, controller, controllerlib.Context{ Context: ctx, }) require.NoError(t, err) t.Log(log.String()) expectedActions := []kubetesting.Action{kubetesting.NewDeleteAction( schema.GroupVersionResource{ Group: "", Version: "v1", Resource: "secrets", }, namespace, "secret-to-delete", )} actualActions := kubeAPIClient.Actions() require.Equal(t, expectedActions, actualActions) } func TestSync_NoSecretToDelete(t *testing.T) { kubeAPIClient := kubernetesfake.NewSimpleClientset() kubeInformerClient := kubernetesfake.NewSimpleClientset() kubeInformers := kubeinformers.NewSharedInformerFactory( kubeInformerClient, 0, ) namespace := "some-namespace" secretWithWrongName := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "wrong-name", Namespace: namespace, }, Type: corev1.SecretTypeServiceAccountToken, } secretWithWrongType := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret-to-leave-alone", Namespace: namespace, }, } secretWithWrongNamespace := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "secret-to-delete", Namespace: "other", }, Type: corev1.SecretTypeServiceAccountToken, } require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongName)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongName)) require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongType)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongType)) require.NoError(t, kubeAPIClient.Tracker().Add(secretWithWrongNamespace)) require.NoError(t, kubeInformerClient.Tracker().Add(secretWithWrongNamespace)) var log bytes.Buffer controller := NewServiceAccountTokenCleanupController( namespace, "secret-to-delete", kubeAPIClient, kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, plog.TestLogger(t, &log), ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Must start informers before calling TestRunSynchronously(). kubeInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) err := controllerlib.TestSync(t, controller, controllerlib.Context{ Context: ctx, }) require.NoError(t, err) t.Log(log.String()) actualActions := kubeAPIClient.Actions() require.Empty(t, actualActions) } func TestSync_ReturnsAPIErrors(t *testing.T) { kubeAPIClient := kubernetesfake.NewSimpleClientset() kubeInformerClient := kubernetesfake.NewSimpleClientset() kubeInformers := kubeinformers.NewSharedInformerFactory( kubeInformerClient, 0, ) errorMessage := "error from API client" kubeAPIClient.PrependReactor( "delete", "secrets", func(a kubetesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New(errorMessage) }, ) namespace := "some-namespace" var log bytes.Buffer legacySecretName := "secret-to-delete" secretToDelete := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: legacySecretName, Namespace: namespace, }, Type: corev1.SecretTypeServiceAccountToken, } require.NoError(t, kubeAPIClient.Tracker().Add(secretToDelete)) require.NoError(t, kubeInformerClient.Tracker().Add(secretToDelete)) controller := NewServiceAccountTokenCleanupController( namespace, legacySecretName, kubeAPIClient, kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, plog.TestLogger(t, &log), ) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Must start informers before calling TestRunSynchronously(). kubeInformers.Start(ctx.Done()) controllerlib.TestRunSynchronously(t, controller) err := controllerlib.TestSync(t, controller, controllerlib.Context{ Context: ctx, }) require.ErrorContains(t, err, fmt.Sprintf("unable to delete secret %s in namespace %s: %s", legacySecretName, namespace, errorMessage)) t.Log(log.String()) expectedActions := []kubetesting.Action{kubetesting.NewDeleteAction( schema.GroupVersionResource{ Group: "", Version: "v1", Resource: "secrets", }, namespace, legacySecretName, )} actualActions := kubeAPIClient.Actions() require.Equal(t, expectedActions, actualActions) }