Files
capsule/internal/cache/impersonation_clients_test.go
cc4fb45d70 feat: upstream enterprise preview (#1841)
feat: upstream enterprise preview

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
2026-05-28 00:58:58 +02:00

308 lines
7.9 KiB
Go

// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package cache_test
import (
"context"
"reflect"
"sync"
"testing"
"time"
"github.com/go-logr/logr"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/projectcapsule/capsule/internal/cache"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
// makeSA builds meta.NamespacedRFC1123ObjectReferenceWithNamespace without depending
// on the concrete field types (they're string aliases in Capsule).
func makeSA(ns, name string) meta.NamespacedRFC1123ObjectReferenceWithNamespace {
var sa meta.NamespacedRFC1123ObjectReferenceWithNamespace
v := reflect.ValueOf(&sa).Elem()
nsField := v.FieldByName("Namespace")
if !nsField.IsValid() || nsField.Kind() != reflect.String || !nsField.CanSet() {
panic("meta.NamespacedRFC1123ObjectReferenceWithNamespace.Namespace is not a settable string-kind field")
}
nsField.SetString(ns)
nameField := v.FieldByName("Name")
if !nameField.IsValid() || nameField.Kind() != reflect.String || !nameField.CanSet() {
panic("meta.NamespacedRFC1123ObjectReferenceWithNamespace.Name is not a settable string-kind field")
}
nameField.SetString(name)
return sa
}
func TestImpersonationCache_Basics(t *testing.T) {
c := cache.NewImpersonationCache()
t.Run("Get on empty cache returns false", func(t *testing.T) {
_, ok := c.Get("ns", "sa")
if ok {
t.Fatalf("expected ok=false on empty cache")
}
})
t.Run("Set(nil) stores entry but Get returns false", func(t *testing.T) {
c.Reset()
c.Set("ns", "sa", nil)
if entries := c.Stats(); entries != 0 {
t.Fatalf("expected Stats()=0 after Set(nil), got %d", entries)
}
_, ok := c.Get("ns", "sa")
if ok {
t.Fatalf("expected ok=false because stored client is nil")
}
})
t.Run("Invalidate removes entry", func(t *testing.T) {
c.Reset()
cl1 := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
c.Set("ns", "sa", cl1)
c.Invalidate("ns", "sa")
if entries := c.Stats(); entries != 0 {
t.Fatalf("expected Stats()=0 after Invalidate, got %d", entries)
}
})
t.Run("Clear drops all entries", func(t *testing.T) {
c.Reset()
cl1 := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
cl2 := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
c.Set("a", "x", cl1)
c.Set("b", "y", cl2)
if entries := c.Stats(); entries != 2 {
t.Fatalf("expected Stats()=2 before Clear, got %d", entries)
}
c.Reset()
if entries := c.Stats(); entries != 0 {
t.Fatalf("expected Stats()=0 after Clear, got %d", entries)
}
})
}
func TestImpersonationCache_LoadOrCreate_SuccessCachesAndReturnsSameInstance(t *testing.T) {
t.Parallel()
cache := cache.NewImpersonationCache()
ctx := context.Background()
log := logr.Discard()
sa := makeSA("monitoring", "alertmanager-sa")
// Provide a REST config that is syntactically valid so controller-runtime client can be constructed
// without needing a live apiserver.
validREST := &rest.Config{
Host: "https://127.0.0.1", // no connectivity required for client object creation
TLSClientConfig: rest.TLSClientConfig{
Insecure: true,
},
}
sch := scheme.Scheme
cl1, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if cl1 == nil {
t.Fatalf("expected non-nil client")
}
// Should be cached now.
if entries := cache.Stats(); entries != 1 {
t.Fatalf("expected 1 cache entry, got %d", entries)
}
cl2, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa)
if err != nil {
t.Fatalf("expected nil error on second LoadOrCreate, got %v", err)
}
if cl2 == nil {
t.Fatalf("expected non-nil client on second LoadOrCreate")
}
// Must return the cached instance.
if cl1 != cl2 {
t.Fatalf("expected same client instance from cache; got different pointers")
}
// Get should return it and ok=true.
got, ok := cache.Get("monitoring", "alertmanager-sa")
if !ok {
t.Fatalf("expected ok=true from Get after LoadOrCreate")
}
if got != cl1 {
t.Fatalf("expected Get to return cached client instance")
}
}
func TestImpersonationCache_LoadOrCreate_ConcurrentOnlyCachesOne(t *testing.T) {
t.Parallel()
cache := cache.NewImpersonationCache()
ctx := context.Background()
log := logr.Discard()
sa := makeSA("monitoring", "alertmanager-sa")
validREST := &rest.Config{
Host: "https://127.0.0.1",
TLSClientConfig: rest.TLSClientConfig{
Insecure: true,
},
}
sch := scheme.Scheme
const goroutines = 25
start := make(chan struct{})
var wg sync.WaitGroup
wg.Add(goroutines)
results := make([]client.Client, goroutines)
errs := make([]error, goroutines)
for i := 0; i < goroutines; i++ {
i := i
go func() {
defer wg.Done()
<-start
cl, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa)
results[i] = cl
errs[i] = err
}()
}
close(start)
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatalf("timeout waiting for goroutines")
}
// All should succeed and return a non-nil client.
var first client.Client
for i := 0; i < goroutines; i++ {
if errs[i] != nil {
t.Fatalf("expected nil error for goroutine %d, got %v", i, errs[i])
}
if results[i] == nil {
t.Fatalf("expected non-nil client for goroutine %d", i)
}
if i == 0 {
first = results[i]
continue
}
// Even with races (double-check store), function returns the stored instance.
if results[i] != first {
t.Fatalf("expected all goroutines to get same cached client instance; got mismatch at %d", i)
}
}
// Cache should have exactly one entry.
if entries := cache.Stats(); entries != 1 {
t.Fatalf("expected exactly 1 cache entry after concurrent LoadOrCreate, got %d", entries)
}
}
func TestImpersonationCache_LoadOrCreate_DifferentKeysCreateDifferentEntries(t *testing.T) {
t.Parallel()
cache := cache.NewImpersonationCache()
ctx := context.Background()
log := logr.Discard()
validREST := &rest.Config{
Host: "https://127.0.0.1",
TLSClientConfig: rest.TLSClientConfig{
Insecure: true,
},
}
sch := scheme.Scheme
sa1 := makeSA("monitoring", "sa-one")
sa2 := makeSA("monitoring", "sa-two")
cl1, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa1)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
cl2, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa2)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if cl1 == nil || cl2 == nil {
t.Fatalf("expected both clients to be non-nil")
}
if cl1 == cl2 {
t.Fatalf("expected different clients for different keys")
}
if entries := cache.Stats(); entries != 2 {
t.Fatalf("expected 2 cache entries, got %d", entries)
}
}
func TestImpersonationCache_InvalidateThenLoadOrCreateCreatesNewInstance(t *testing.T) {
t.Parallel()
cache := cache.NewImpersonationCache()
ctx := context.Background()
log := logr.Discard()
sa := makeSA("monitoring", "alertmanager-sa")
validREST := &rest.Config{
Host: "https://127.0.0.1",
TLSClientConfig: rest.TLSClientConfig{
Insecure: true,
},
}
sch := scheme.Scheme
cl1, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa)
if err != nil || cl1 == nil {
t.Fatalf("expected first LoadOrCreate success, err=%v cl=%v", err, cl1)
}
cache.Invalidate("monitoring", "alertmanager-sa")
if entries := cache.Stats(); entries != 0 {
t.Fatalf("expected 0 entries after Invalidate, got %d", entries)
}
cl2, err := cache.LoadOrCreate(ctx, log, validREST, sch, sa)
if err != nil || cl2 == nil {
t.Fatalf("expected second LoadOrCreate success, err=%v cl=%v", err, cl2)
}
// After invalidation, a new client instance should be created and cached.
if cl1 == cl2 {
t.Fatalf("expected new client instance after Invalidate, got same pointer")
}
if entries := cache.Stats(); entries != 1 {
t.Fatalf("expected 1 entry after re-LoadOrCreate, got %d", entries)
}
}