From 3ca877f1df76c094021390aea123f79478ea0eba Mon Sep 17 00:00:00 2001 From: aram price Date: Fri, 11 Dec 2020 20:49:10 -0800 Subject: [PATCH] WIP - preliminary OIDCProviderSecrets controller Tests not yet passing, controller is incomplete and expectations may be incorrect. --- .../generator/oidc_provider_secrets.go | 209 +++++++ .../generator/oidc_provider_secrets_test.go | 590 ++++++++++++++++++ 2 files changed, 799 insertions(+) create mode 100644 internal/controller/supervisorconfig/generator/oidc_provider_secrets.go create mode 100644 internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go new file mode 100644 index 00000000..e1dbab4e --- /dev/null +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets.go @@ -0,0 +1,209 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + pinnipedclientset "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" + configinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions/config/v1alpha1" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/plog" +) + +const ( + // TODO should this live on `provider.OIDCProvider` ? + opcKind = "OIDCProvider" // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go +) + +// jwkController holds the fields necessary for the JWKS controller to communicate with OPC's and +// secrets, both via a cache and via the API. +type oidcProviderSecretsController struct { + secretNameFunc func(*configv1alpha1.OIDCProvider) string + secretLabels map[string]string + secretDataFunc func() (map[string][]byte, error) + pinnipedClient pinnipedclientset.Interface + kubeClient kubernetes.Interface + opcInformer configinformers.OIDCProviderInformer + secretInformer corev1informers.SecretInformer +} + +func NewOIDCProviderSecretsController( + secretNameFunc func(*configv1alpha1.OIDCProvider) string, + secretLabels map[string]string, + secretDataFunc func() (map[string][]byte, error), + kubeClient kubernetes.Interface, + pinnipedClient pinnipedclientset.Interface, + secretInformer corev1informers.SecretInformer, + opcInformer configinformers.OIDCProviderInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ + Name: "JWKSController", + Syncer: &oidcProviderSecretsController{ + secretNameFunc: secretNameFunc, + secretLabels: secretLabels, + secretDataFunc: secretDataFunc, + kubeClient: kubeClient, + pinnipedClient: pinnipedClient, + secretInformer: secretInformer, + opcInformer: opcInformer, + }, + }, + // We want to be notified when a OPC's secret gets updated or deleted. When this happens, we + // should get notified via the corresponding OPC key. + withInformer( + secretInformer, + controllerlib.FilterFuncs{ + ParentFunc: func(obj metav1.Object) controllerlib.Key { + if isOPCControllee(obj) { + controller := metav1.GetControllerOf(obj) + return controllerlib.Key{ + Name: controller.Name, + Namespace: obj.GetNamespace(), + } + } + return controllerlib.Key{} + }, + AddFunc: isOPCControllee, + UpdateFunc: func(oldObj, newObj metav1.Object) bool { + return isOPCControllee(oldObj) || isOPCControllee(newObj) + }, + DeleteFunc: isOPCControllee, + }, + controllerlib.InformerOption{}, + ), + // We want to be notified when anything happens to an OPC. + withInformer( + opcInformer, + pinnipedcontroller.MatchAnythingFilter(nil), // nil parent func is fine because each event is distinct + controllerlib.InformerOption{}, + ), + ) +} + +func (c *oidcProviderSecretsController) Sync(ctx controllerlib.Context) error { + opc, err := c.opcInformer.Lister().OIDCProviders(ctx.Key.Namespace).Get(ctx.Key.Name) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return fmt.Errorf( + "failed to get %s/%s OIDCProvider: %w", + ctx.Key.Namespace, + ctx.Key.Name, + err, + ) + } + + if notFound { + // The corresponding secret to this OPC should have been garbage collected since it should have + // had this OPC as its owner. + plog.Debug( + "oidcprovider deleted", + "oidcprovider", + klog.KRef(ctx.Key.Namespace, ctx.Key.Name), + ) + return nil + } + + secretNeedsUpdate, err := c.secretNeedsUpdate(opc) + if err != nil { + return fmt.Errorf("cannot determine secret status: %w", err) + } + if !secretNeedsUpdate { + // Secret is up to date - we are good to go. + plog.Debug( + "secret is up to date", + "oidcprovider", + klog.KRef(ctx.Key.Namespace, ctx.Key.Name), + ) + return nil + } + + // If the OPC does not have a secret associated with it, that secret does not exist, or the secret + // is invalid, we will generate a new secret (i.e., a JWKS). + secret, err := generateSecret(opc.Namespace, c.secretNameFunc(opc), c.secretDataFunc, opc) + if err != nil { + return fmt.Errorf("cannot generate secret: %w", err) + } + + if err := c.createOrUpdateSecret(ctx.Context, secret); err != nil { + return fmt.Errorf("cannot create or update secret: %w", err) + } + plog.Debug("created/updated secret", "secret", klog.KObj(secret)) + + return nil +} + +func (c *oidcProviderSecretsController) secretNeedsUpdate(opc *configv1alpha1.OIDCProvider) (bool, error) { + // This OPC says it has a secret associated with it. Let's try to get it from the cache. + secret, err := c.secretInformer.Lister().Secrets(opc.Namespace).Get(c.secretNameFunc(opc)) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return false, fmt.Errorf("cannot get secret: %w", err) + } + if notFound { + // If we can't find the secret, let's assume we need to create it. + return true, nil + } + + if !isValid(secret) { + // If this secret is invalid, we need to generate a new one. + return true, nil + } + + return false, nil +} + +func (c *oidcProviderSecretsController) createOrUpdateSecret( + ctx context.Context, + newSecret *corev1.Secret, +) error { + secretClient := c.kubeClient.CoreV1().Secrets(newSecret.Namespace) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + oldSecret, err := secretClient.Get(ctx, newSecret.Name, metav1.GetOptions{}) + notFound := k8serrors.IsNotFound(err) + if err != nil && !notFound { + return fmt.Errorf("cannot get secret: %w", err) + } + + if notFound { + // New secret doesn't exist, so create it. + _, err := secretClient.Create(ctx, newSecret, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("cannot create secret: %w", err) + } + return nil + } + + // New secret already exists, so ensure it is up to date. + if isValid(oldSecret) { + // If the secret already has valid JWK's, then we are good to go and we don't need an update. + return nil + } + + oldSecret.Data = newSecret.Data + _, err = secretClient.Update(ctx, oldSecret, metav1.UpdateOptions{}) + return err + }) +} + +// isOPCControlle returns whether the provided obj is controlled by an OPC. +func isOPCControllee(obj metav1.Object) bool { // TODO: deduplicate - internal/controller/supervisorconfig/jwks_writer.go + controller := metav1.GetControllerOf(obj) + return controller != nil && + controller.APIVersion == configv1alpha1.SchemeGroupVersion.String() && + controller.Kind == opcKind +} diff --git a/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go new file mode 100644 index 00000000..801b481e --- /dev/null +++ b/internal/controller/supervisorconfig/generator/oidc_provider_secrets_test.go @@ -0,0 +1,590 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package generator + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "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" + + configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1" + pinnipedfake "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned/fake" + pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/testutil" +) + +func TestOIDCProviderControllerFilterSecret(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + secret corev1.Secret + wantAdd bool + wantUpdate bool + wantDelete bool + wantParent controllerlib.Key + }{ + { + name: "no owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{}, + }, + }, + { + name: "owner reference without correct APIVersion", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + }, + { + name: "owner reference without correct Kind", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + }, + { + name: "owner reference without controller set to true", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + }, + }, + }, + }, + }, + { + name: "correct owner reference", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"}, + }, + { + name: "multiple owner references", + secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "some-namespace", + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "UnrelatedKind", + }, + { + APIVersion: configv1alpha1.SchemeGroupVersion.String(), + Kind: "OIDCProvider", + Name: "some-name", + Controller: boolPtr(true), + }, + }, + }, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + opcInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCProviders() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCProviderSecretsController( + secretNameFunc, + nil, // labels, not needed + fakeSecretDataFunc, + nil, // kubeClient, not needed + nil, // pinnipedClient, not needed + secretInformer, + opcInformer, + withInformer.WithInformer, + ) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(secretInformer) + require.Equal(t, test.wantAdd, filter.Add(&test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&test.secret, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(&test.secret)) + require.Equal(t, test.wantParent, filter.Parent(&test.secret)) + }) + } +} + +func TestNewOIDCProviderSecretsControllerFilterOPC(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + opc configv1alpha1.OIDCProvider + wantAdd bool + wantUpdate bool + wantDelete bool + wantParent controllerlib.Key + }{ + { + name: "anything goes", + opc: configv1alpha1.OIDCProvider{}, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + wantParent: controllerlib.Key{}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + opcInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCProviders() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCProviderSecretsController( + secretNameFunc, + nil, // labels, not needed + fakeSecretDataFunc, + nil, // kubeClient, not needed + nil, // pinnipedClient, not needed + secretInformer, + opcInformer, + withInformer.WithInformer, + ) + + unrelated := configv1alpha1.OIDCProvider{} + filter := withInformer.GetFilterForInformer(opcInformer) + require.Equal(t, test.wantAdd, filter.Add(&test.opc)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.opc)) + require.Equal(t, test.wantUpdate, filter.Update(&test.opc, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(&test.opc)) + require.Equal(t, test.wantParent, filter.Parent(&test.opc)) + }) + } +} + +func TestNewOIDCProviderSecretsControllerSync(t *testing.T) { + // We shouldn't run this test in parallel since it messes with a global function (generateKey). + + const namespace = "tuna-namespace" + + opcGVR := schema.GroupVersionResource{ + Group: configv1alpha1.SchemeGroupVersion.Group, + Version: configv1alpha1.SchemeGroupVersion.Version, + Resource: "oidcproviders", + } + + goodOPC := &configv1alpha1.OIDCProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "good-opc", + Namespace: namespace, + UID: "good-opc-uid", + }, + Spec: configv1alpha1.OIDCProviderSpec{ + Issuer: "https://some-issuer.com", + }, + } + + expectedSecretName := secretNameFunc(goodOPC) + + secretGVR := schema.GroupVersionResource{ + Group: corev1.SchemeGroupVersion.Group, + Version: corev1.SchemeGroupVersion.Version, + Resource: "secrets", + } + + newSecret := func(secretData map[string][]byte) *corev1.Secret { + s := corev1.Secret{ + Type: symmetricKeySecretType, + ObjectMeta: metav1.ObjectMeta{ + Name: expectedSecretName, + Namespace: namespace, + Labels: map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: opcGVR.GroupVersion().String(), + Kind: "OIDCProvider", + Name: goodOPC.Name, + UID: goodOPC.UID, + BlockOwnerDeletion: boolPtr(true), + Controller: boolPtr(true), + }, + }, + }, + Data: secretData, + } + + return &s + } + + secretData, err := fakeSecretDataFunc() + require.NoError(t, err) + + goodSecret := newSecret(secretData) + + tests := []struct { + name string + key controllerlib.Key + secrets []*corev1.Secret + configKubeClient func(*kubernetesfake.Clientset) + configPinnipedClient func(*pinnipedfake.Clientset) + opcs []*configv1alpha1.OIDCProvider + generateKeyErr error + wantGenerateKeyCount int + wantSecretActions []kubetesting.Action + wantOPCActions []kubetesting.Action + wantError string + }{ + { + name: "new opc with no secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "opc without status with existing secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + goodSecret, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "existing opc with no secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewCreateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "existing opc with existing secret", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + goodSecret, + }, + }, + { + name: "deleted opc", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + // Nothing to do here since Kube will garbage collect our child secret via its OwnerReference. + }, + { + name: "secret data is empty", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: fmt.Sprintf("secret missing key %s", symmetricKeySecretDataKey), + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{"badKey": []byte("some secret - must have at least 32 bytes")}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: fmt.Sprintf("secret data value for key %s", symmetricKeySecretDataKey), + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{symmetricKeySecretDataKey: {}}), + }, + wantGenerateKeyCount: 1, + wantSecretActions: []kubetesting.Action{ + kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name), + kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret), + }, + wantOPCActions: []kubetesting.Action{ + kubetesting.NewGetAction(opcGVR, namespace, goodOPC.Name), + }, + }, + { + name: "generate key fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + generateKeyErr: errors.New("some generate error"), + wantError: "cannot generate secret: cannot generate key: some generate error", + }, + { + name: "get secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantError: "cannot create or update secret: cannot get secret: some get error", + }, + { + name: "create secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some create error") + }) + }, + wantError: "cannot create or update secret: cannot create secret: some create error", + }, + { + name: "update secret fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + secrets: []*corev1.Secret{ + newSecret(map[string][]byte{}), + }, + configKubeClient: func(client *kubernetesfake.Clientset) { + client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some update error") + }) + }, + wantError: "cannot create or update secret: some update error", + }, + { + name: "get opc fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor("get", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some get error") + }) + }, + wantError: "cannot update opc: cannot get opc: some get error", + }, + { + name: "update opc fails", + key: controllerlib.Key{Namespace: goodOPC.Namespace, Name: goodOPC.Name}, + opcs: []*configv1alpha1.OIDCProvider{ + goodOPC, + }, + configPinnipedClient: func(client *pinnipedfake.Clientset) { + client.PrependReactor("update", "oidcproviders", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("some update error") + }) + }, + wantError: "cannot update opc: some update error", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + // We shouldn't run this test in parallel since it messes with a global function (generateKey). + generateKeyCount := 0 + generateKey := func() (map[string][]byte, error) { + generateKeyCount++ + return map[string][]byte{ + symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), + }, nil + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + + kubeAPIClient := kubernetesfake.NewSimpleClientset() + kubeInformerClient := kubernetesfake.NewSimpleClientset() + for _, secret := range test.secrets { + require.NoError(t, kubeAPIClient.Tracker().Add(secret)) + require.NoError(t, kubeInformerClient.Tracker().Add(secret)) + } + if test.configKubeClient != nil { + test.configKubeClient(kubeAPIClient) + } + + pinnipedAPIClient := pinnipedfake.NewSimpleClientset() + pinnipedInformerClient := pinnipedfake.NewSimpleClientset() + for _, opc := range test.opcs { + require.NoError(t, pinnipedAPIClient.Tracker().Add(opc)) + require.NoError(t, pinnipedInformerClient.Tracker().Add(opc)) + } + if test.configPinnipedClient != nil { + test.configPinnipedClient(pinnipedAPIClient) + } + + kubeInformers := kubeinformers.NewSharedInformerFactory( + kubeInformerClient, + 0, + ) + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory( + pinnipedInformerClient, + 0, + ) + + c := NewOIDCProviderSecretsController( + secretNameFunc, + map[string]string{ + "myLabelKey1": "myLabelValue1", + "myLabelKey2": "myLabelValue2", + }, + generateKey, + kubeAPIClient, + pinnipedAPIClient, + kubeInformers.Core().V1().Secrets(), + pinnipedInformers.Config().V1alpha1().OIDCProviders(), + controllerlib.WithInformer, + ) + + // Must start informers before calling TestRunSynchronously(). + kubeInformers.Start(ctx.Done()) + pinnipedInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, c) + + err := controllerlib.TestSync(t, c, controllerlib.Context{ + Context: ctx, + Key: test.key, + }) + if test.wantError != "" { + require.EqualError(t, err, test.wantError) + return + } + require.NoError(t, err) + + require.Equal(t, test.wantGenerateKeyCount, generateKeyCount) + + if test.wantSecretActions != nil { + require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions()) + } + if test.wantOPCActions != nil { + require.Equal(t, test.wantOPCActions, pinnipedAPIClient.Actions()) + } + }) + } +} + +func secretNameFunc(opc *configv1alpha1.OIDCProvider) string { + return fmt.Sprintf("pinniped-%s-%s-test_secret", opc.Kind, opc.UID) +} + +func fakeSecretDataFunc() (map[string][]byte, error) { + return map[string][]byte{ + symmetricKeySecretDataKey: []byte("some secret - must have at least 32 bytes"), + }, nil +} + +func boolPtr(b bool) *bool { return &b }