diff --git a/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner.go b/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner.go new file mode 100644 index 00000000..dd14de88 --- /dev/null +++ b/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package webhookcachecleaner implements a controller for garbage collectting webhook IDPs from an IDP cache. +package webhookcachecleaner + +import ( + "fmt" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/klog/v2" + + idpv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/idp/v1alpha1" + idpinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions/idp/v1alpha1" + pinnipedcontroller "github.com/suzerain-io/pinniped/internal/controller" + "github.com/suzerain-io/pinniped/internal/controller/identityprovider/idpcache" + "github.com/suzerain-io/pinniped/internal/controllerlib" +) + +// New instantiates a new controllerlib.Controller which will garbage collect webhooks from the provided Cache. +func New(cache *idpcache.Cache, webhookIDPs idpinformers.WebhookIdentityProviderInformer, log logr.Logger) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ + Name: "webhookcachecleaner-controller", + Syncer: &controller{ + cache: cache, + webhookIDPs: webhookIDPs, + log: log.WithName("webhookcachecleaner-controller"), + }, + }, + controllerlib.WithInformer( + webhookIDPs, + pinnipedcontroller.NoOpFilter(), + controllerlib.InformerOption{}, + ), + ) +} + +type controller struct { + cache *idpcache.Cache + webhookIDPs idpinformers.WebhookIdentityProviderInformer + log logr.Logger +} + +// Sync implements controllerlib.Syncer. +func (c *controller) Sync(ctx controllerlib.Context) error { + webhooks, err := c.webhookIDPs.Lister().List(labels.Everything()) + if err != nil { + return fmt.Errorf("failed to list WebhookIdentityProviders: %w", err) + } + + // Index the current webhooks by key. + webhooksByKey := map[controllerlib.Key]*idpv1alpha1.WebhookIdentityProvider{} + for _, webhook := range webhooks { + key := controllerlib.Key{Namespace: webhook.Namespace, Name: webhook.Name} + webhooksByKey[key] = webhook + } + + // Delete any entries from the cache which are no longer in the cluster. + for _, key := range c.cache.Keys() { + if _, exists := webhooksByKey[key]; !exists { + c.log.WithValues("idp", klog.KRef(key.Namespace, key.Name)).Info("deleting webhook IDP from cache") + c.cache.Delete(key) + } + } + return nil +} diff --git a/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner_test.go b/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner_test.go new file mode 100644 index 00000000..56a29bc6 --- /dev/null +++ b/internal/controller/identityprovider/webhookcachecleaner/webhookcachecleaner_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package webhookcachecleaner + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/authenticator" + + idpv1alpha "github.com/suzerain-io/pinniped/generated/1.19/apis/idp/v1alpha1" + pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake" + pinnipedinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions" + "github.com/suzerain-io/pinniped/internal/controller/identityprovider/idpcache" + "github.com/suzerain-io/pinniped/internal/controllerlib" + "github.com/suzerain-io/pinniped/internal/testutil/testlogger" +) + +func TestController(t *testing.T) { + t.Parallel() + + testKey1 := controllerlib.Key{Namespace: "test-namespace", Name: "test-name-one"} + testKey2 := controllerlib.Key{Namespace: "test-namespace", Name: "test-name-two"} + + tests := []struct { + name string + syncKey controllerlib.Key + webhookIDPs []runtime.Object + initialCache map[controllerlib.Key]authenticator.Token + wantErr string + wantLogs []string + wantCacheKeys []controllerlib.Key + }{ + { + name: "no change", + syncKey: testKey1, + initialCache: map[controllerlib.Key]authenticator.Token{testKey1: nil}, + webhookIDPs: []runtime.Object{ + &idpv1alpha.WebhookIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testKey1.Namespace, + Name: testKey1.Name, + }, + }, + }, + wantCacheKeys: []controllerlib.Key{testKey1}, + }, + { + name: "IDPs not yet added", + syncKey: testKey1, + initialCache: nil, + webhookIDPs: []runtime.Object{ + &idpv1alpha.WebhookIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testKey1.Namespace, + Name: testKey1.Name, + }, + }, + &idpv1alpha.WebhookIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testKey2.Namespace, + Name: testKey2.Name, + }, + }, + }, + wantCacheKeys: []controllerlib.Key{}, + }, + { + name: "successful cleanup", + syncKey: testKey1, + initialCache: map[controllerlib.Key]authenticator.Token{ + testKey1: nil, + testKey2: nil, + }, + webhookIDPs: []runtime.Object{ + &idpv1alpha.WebhookIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testKey1.Namespace, + Name: testKey1.Name, + }, + }, + }, + wantLogs: []string{ + `webhookcachecleaner-controller "level"=0 "msg"="deleting webhook IDP from cache" "idp"={"name":"test-name-two","namespace":"test-namespace"}`, + }, + wantCacheKeys: []controllerlib.Key{testKey1}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + fakeClient := pinnipedfake.NewSimpleClientset(tt.webhookIDPs...) + informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0) + cache := idpcache.New() + for k, v := range tt.initialCache { + cache.Store(k, v) + } + testLog := testlogger.New(t) + + controller := New(cache, informers.IDP().V1alpha1().WebhookIdentityProviders(), testLog) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + informers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey} + + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantLogs, testLog.Lines()) + require.ElementsMatch(t, tt.wantCacheKeys, cache.Keys()) + }) + } +}