mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
feat: upstream enterprise preview --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: CorentinPtrl <pitrel.corentin@gmail.com>
308 lines
7.9 KiB
Go
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)
|
|
}
|
|
}
|