mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-06 08:07:08 +00:00
Add JWTAuthenticator controller
See https://github.com/vmware-tanzu/pinniped/issues/260 for UX bummer. Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package cachecleaner implements a controller for garbage collecting authenticators from an authenticator cache.
|
||||
package cachecleaner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
auth1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
authinformers "go.pinniped.dev/generated/1.19/client/concierge/informers/externalversions/authentication/v1alpha1"
|
||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
)
|
||||
|
||||
// closable is used to detect when a cache value has a Close() method on it. We use this to
|
||||
// determine if we should call the Close() method on a cache value upon deleting it from the cache.
|
||||
type closable interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
// New instantiates a new controllerlib.Controller which will garbage collect authenticators from the provided Cache.
|
||||
func New(
|
||||
cache *authncache.Cache,
|
||||
webhooks authinformers.WebhookAuthenticatorInformer,
|
||||
jwtAuthenticators authinformers.JWTAuthenticatorInformer,
|
||||
log logr.Logger,
|
||||
) controllerlib.Controller {
|
||||
return controllerlib.New(
|
||||
controllerlib.Config{
|
||||
Name: "cachecleaner-controller",
|
||||
Syncer: &controller{
|
||||
cache: cache,
|
||||
webhooks: webhooks,
|
||||
jwtAuthenticators: jwtAuthenticators,
|
||||
log: log.WithName("cachecleaner-controller"),
|
||||
},
|
||||
},
|
||||
controllerlib.WithInformer(
|
||||
webhooks,
|
||||
pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
controllerlib.WithInformer(
|
||||
jwtAuthenticators,
|
||||
pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
|
||||
controllerlib.InformerOption{},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
cache *authncache.Cache
|
||||
webhooks authinformers.WebhookAuthenticatorInformer
|
||||
jwtAuthenticators authinformers.JWTAuthenticatorInformer
|
||||
log logr.Logger
|
||||
}
|
||||
|
||||
// Sync implements controllerlib.Syncer.
|
||||
func (c *controller) Sync(_ controllerlib.Context) error {
|
||||
webhooks, err := c.webhooks.Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list WebhookAuthenticators: %w", err)
|
||||
}
|
||||
|
||||
jwtAuthenticators, err := c.jwtAuthenticators.Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list JWTAuthenticators: %w", err)
|
||||
}
|
||||
|
||||
// Index the current authenticators by cache key.
|
||||
authenticatorSet := map[authncache.Key]bool{}
|
||||
for _, webhook := range webhooks {
|
||||
key := authncache.Key{
|
||||
Namespace: webhook.Namespace,
|
||||
Name: webhook.Name,
|
||||
Kind: "WebhookAuthenticator",
|
||||
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
||||
}
|
||||
authenticatorSet[key] = true
|
||||
}
|
||||
for _, jwtAuthenticator := range jwtAuthenticators {
|
||||
key := authncache.Key{
|
||||
Namespace: jwtAuthenticator.Namespace,
|
||||
Name: jwtAuthenticator.Name,
|
||||
Kind: "JWTAuthenticator",
|
||||
APIGroup: auth1alpha1.SchemeGroupVersion.Group,
|
||||
}
|
||||
authenticatorSet[key] = true
|
||||
}
|
||||
|
||||
// Delete any entries from the cache which are no longer in the cluster.
|
||||
for _, key := range c.cache.Keys() {
|
||||
if key.APIGroup != auth1alpha1.SchemeGroupVersion.Group || (key.Kind != "WebhookAuthenticator" && key.Kind != "JWTAuthenticator") {
|
||||
continue
|
||||
}
|
||||
if _, exists := authenticatorSet[key]; !exists {
|
||||
c.log.WithValues(
|
||||
"authenticator",
|
||||
klog.KRef(key.Namespace, key.Name),
|
||||
"kind",
|
||||
key.Kind,
|
||||
).Info("deleting authenticator from cache")
|
||||
|
||||
value := c.cache.Get(key)
|
||||
if closable, ok := value.(closable); ok {
|
||||
closable.Close()
|
||||
}
|
||||
|
||||
c.cache.Delete(key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cachecleaner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
authv1alpha "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
||||
pinnipedfake "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned/fake"
|
||||
pinnipedinformers "go.pinniped.dev/generated/1.19/client/concierge/informers/externalversions"
|
||||
"go.pinniped.dev/internal/controller/authenticator/authncache"
|
||||
"go.pinniped.dev/internal/controllerlib"
|
||||
"go.pinniped.dev/internal/testutil/testlogger"
|
||||
)
|
||||
|
||||
func TestController(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testWebhookKey1 := authncache.Key{
|
||||
APIGroup: "authentication.concierge.pinniped.dev",
|
||||
Kind: "WebhookAuthenticator",
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-webhook-name-one",
|
||||
}
|
||||
testWebhookKey2 := authncache.Key{
|
||||
APIGroup: "authentication.concierge.pinniped.dev",
|
||||
Kind: "WebhookAuthenticator",
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-webhook-name-two",
|
||||
}
|
||||
testJWTAuthenticatorKey1 := authncache.Key{
|
||||
APIGroup: "authentication.concierge.pinniped.dev",
|
||||
Kind: "JWTAuthenticator",
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-jwt-authenticator-name-one",
|
||||
}
|
||||
testJWTAuthenticatorKey2 := authncache.Key{
|
||||
APIGroup: "authentication.concierge.pinniped.dev",
|
||||
Kind: "JWTAuthenticator",
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-jwt-authenticator-name-two",
|
||||
}
|
||||
testKeyUnknownType := authncache.Key{
|
||||
APIGroup: "authentication.concierge.pinniped.dev",
|
||||
Kind: "SomeOtherAuthenticator",
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-name-one",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
objects []runtime.Object
|
||||
initialCache map[authncache.Key]authncache.Value
|
||||
wantErr string
|
||||
wantLogs []string
|
||||
wantCacheKeys []authncache.Key
|
||||
}{
|
||||
{
|
||||
name: "no change",
|
||||
initialCache: map[authncache.Key]authncache.Value{
|
||||
testWebhookKey1: nil,
|
||||
testJWTAuthenticatorKey1: nil,
|
||||
},
|
||||
objects: []runtime.Object{
|
||||
&authv1alpha.WebhookAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testWebhookKey1.Namespace,
|
||||
Name: testWebhookKey1.Name,
|
||||
},
|
||||
},
|
||||
&authv1alpha.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testJWTAuthenticatorKey1.Namespace,
|
||||
Name: testJWTAuthenticatorKey1.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCacheKeys: []authncache.Key{testWebhookKey1, testJWTAuthenticatorKey1},
|
||||
},
|
||||
{
|
||||
name: "authenticators not yet added",
|
||||
initialCache: nil,
|
||||
objects: []runtime.Object{
|
||||
&authv1alpha.WebhookAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testWebhookKey1.Namespace,
|
||||
Name: testWebhookKey1.Name,
|
||||
},
|
||||
},
|
||||
&authv1alpha.WebhookAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testWebhookKey2.Namespace,
|
||||
Name: testWebhookKey2.Name,
|
||||
},
|
||||
},
|
||||
&authv1alpha.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testJWTAuthenticatorKey1.Namespace,
|
||||
Name: testJWTAuthenticatorKey1.Name,
|
||||
},
|
||||
},
|
||||
&authv1alpha.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testJWTAuthenticatorKey2.Namespace,
|
||||
Name: testJWTAuthenticatorKey2.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCacheKeys: []authncache.Key{},
|
||||
},
|
||||
{
|
||||
name: "successful cleanup",
|
||||
initialCache: map[authncache.Key]authncache.Value{
|
||||
testWebhookKey1: nil,
|
||||
testWebhookKey2: nil,
|
||||
testJWTAuthenticatorKey1: newClosableCacheValue(t, "closable1", 0),
|
||||
testJWTAuthenticatorKey2: newClosableCacheValue(t, "closable2", 1),
|
||||
testKeyUnknownType: nil,
|
||||
},
|
||||
objects: []runtime.Object{
|
||||
&authv1alpha.WebhookAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testWebhookKey1.Namespace,
|
||||
Name: testWebhookKey1.Name,
|
||||
},
|
||||
},
|
||||
&authv1alpha.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: testJWTAuthenticatorKey1.Namespace,
|
||||
Name: testJWTAuthenticatorKey1.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantLogs: []string{
|
||||
`cachecleaner-controller "level"=0 "msg"="deleting authenticator from cache" "authenticator"={"name":"test-jwt-authenticator-name-two","namespace":"test-namespace"} "kind"="JWTAuthenticator"`,
|
||||
`cachecleaner-controller "level"=0 "msg"="deleting authenticator from cache" "authenticator"={"name":"test-webhook-name-two","namespace":"test-namespace"} "kind"="WebhookAuthenticator"`,
|
||||
},
|
||||
wantCacheKeys: []authncache.Key{testWebhookKey1, testJWTAuthenticatorKey1, testKeyUnknownType},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// When we have t.Parallel() here, this test blocks pretty consistently...y tho?
|
||||
|
||||
fakeClient := pinnipedfake.NewSimpleClientset(tt.objects...)
|
||||
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
||||
cache := authncache.New()
|
||||
for k, v := range tt.initialCache {
|
||||
cache.Store(k, v)
|
||||
}
|
||||
testLog := testlogger.New(t)
|
||||
|
||||
webhooks := informers.Authentication().V1alpha1().WebhookAuthenticators()
|
||||
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
||||
controller := New(cache, webhooks, jwtAuthenticators, testLog)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
informers.Start(ctx.Done())
|
||||
informers.WaitForCacheSync(ctx.Done())
|
||||
controllerlib.TestRunSynchronously(t, controller)
|
||||
|
||||
syncCtx := controllerlib.Context{
|
||||
Context: ctx,
|
||||
Key: controllerlib.Key{
|
||||
Namespace: "test-namespace",
|
||||
Name: "test-webhook-name-one",
|
||||
},
|
||||
}
|
||||
|
||||
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.ElementsMatch(t, tt.wantLogs, testLog.Lines())
|
||||
require.ElementsMatch(t, tt.wantCacheKeys, cache.Keys())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newClosableCacheValue(t *testing.T, name string, wantCloses int) authncache.Value {
|
||||
t.Helper()
|
||||
c := &closableCacheValue{}
|
||||
t.Cleanup(func() {
|
||||
require.Equalf(t, wantCloses, c.closeCallCount, "expected %s.Close() to be called %d times", name, wantCloses)
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
type closableCacheValue struct {
|
||||
authncache.Value
|
||||
closeCallCount int
|
||||
}
|
||||
|
||||
func (c *closableCacheValue) Close() {
|
||||
c.closeCallCount++
|
||||
}
|
||||
Reference in New Issue
Block a user