WIP - preliminary OIDCProviderSecrets controller
Tests not yet passing, controller is incomplete and expectations may be incorrect.
This commit is contained in:
parent
3e31668eb0
commit
3ca877f1df
@ -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
|
||||||
|
}
|
@ -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 }
|
Loading…
Reference in New Issue
Block a user