Files
Oliver BählerandGitHub 62d8d623ed fix: revert pv labeling to dircet api reads (#2085)
* fix: revert pv labeling to dircet api reads

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: revert pv labeling to dircet api reads

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-08-17 10:52:24 +02:00

326 lines
8.8 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package pv
import (
"context"
"errors"
"fmt"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/meta"
)
func TestReconcileRepairsPersistentVolumeTenantLabelFromNamespace(t *testing.T) {
t.Parallel()
tests := []struct {
name string
labels map[string]string
wantUpdates int
}{
{
name: "missing label",
wantUpdates: 1,
},
{
name: "empty label",
labels: map[string]string{meta.TenantLabel: ""},
wantUpdates: 1,
},
{
name: "stale label",
labels: map[string]string{meta.TenantLabel: "another-tenant"},
wantUpdates: 1,
},
{
name: "current label",
labels: map[string]string{meta.TenantLabel: "tenant-a"},
wantUpdates: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
scheme := persistentVolumeTestScheme(t)
tnt := persistentVolumeTestTenant()
ns := persistentVolumeTestNamespace(tnt)
pv := persistentVolumeTestVolume(ns.Name, tt.labels)
base := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tnt, ns, pv).Build()
counting := &updateCountingClient{Client: base}
controller := &Controller{
client: counting,
reader: base,
label: meta.TenantLabel,
}
if _, err := controller.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: pv.Name},
}); err != nil {
t.Fatalf("Reconcile() error = %v", err)
}
updated := &corev1.PersistentVolume{}
if err := base.Get(context.Background(), client.ObjectKeyFromObject(pv), updated); err != nil {
t.Fatalf("get PersistentVolume: %v", err)
}
if got := updated.GetLabels()[meta.TenantLabel]; got != tnt.Name {
t.Fatalf("tenant label = %q, want %q", got, tnt.Name)
}
if counting.updates != tt.wantUpdates {
t.Fatalf("updates = %d, want %d", counting.updates, tt.wantUpdates)
}
})
}
}
func TestReconcileSkipsPersistentVolumeFromUnmanagedOrDeletedNamespace(t *testing.T) {
t.Parallel()
tests := []struct {
name string
namespace *corev1.Namespace
}{
{
name: "unmanaged namespace",
namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
Name: "unmanaged",
}},
},
{
name: "deleted namespace",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
scheme := persistentVolumeTestScheme(t)
objects := []client.Object{}
namespace := "deleted"
if tt.namespace != nil {
objects = append(objects, tt.namespace)
namespace = tt.namespace.Name
}
pv := persistentVolumeTestVolume(namespace, nil)
objects = append(objects, pv)
base := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
counting := &updateCountingClient{Client: base}
controller := &Controller{client: counting, reader: base, label: meta.TenantLabel}
if _, err := controller.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: pv.Name},
}); err != nil {
t.Fatalf("Reconcile() error = %v", err)
}
if counting.updates != 0 {
t.Fatalf("updates = %d, want 0", counting.updates)
}
})
}
}
func TestReconcileRetriesInconsistentOrFailedNamespaceResolution(t *testing.T) {
t.Parallel()
t.Run("inconsistent ownership", func(t *testing.T) {
t.Parallel()
scheme := persistentVolumeTestScheme(t)
tnt := persistentVolumeTestTenant()
ns := persistentVolumeTestNamespace(tnt)
delete(ns.Labels, meta.TenantLabel)
pv := persistentVolumeTestVolume(ns.Name, nil)
base := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tnt, ns, pv).Build()
controller := &Controller{client: base, reader: base, label: meta.TenantLabel}
_, err := controller.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: pv.Name},
})
if err == nil {
t.Fatal("Reconcile() error = nil, want inconsistent ownership error")
}
})
t.Run("reader failure", func(t *testing.T) {
t.Parallel()
scheme := persistentVolumeTestScheme(t)
pv := persistentVolumeTestVolume("tenant-a-ns", nil)
base := fake.NewClientBuilder().WithScheme(scheme).WithObjects(pv).Build()
controller := &Controller{
client: base,
reader: &namespaceFailingReader{
Reader: base,
err: errors.New("temporary API failure"),
},
label: meta.TenantLabel,
}
_, err := controller.Reconcile(context.Background(), reconcile.Request{
NamespacedName: types.NamespacedName{Name: pv.Name},
})
if err == nil || !errors.Is(err, controller.reader.(*namespaceFailingReader).err) {
t.Fatalf("Reconcile() error = %v, want temporary API failure", err)
}
})
}
func TestPersistentVolumePredicate(t *testing.T) {
t.Parallel()
pred := persistentVolumePredicate(meta.TenantLabel)
claimed := persistentVolumeTestVolume("tenant-a-ns", map[string]string{meta.TenantLabel: "tenant-a"})
unclaimed := claimed.DeepCopy()
unclaimed.Spec.ClaimRef = nil
if !pred.Create(event.CreateEvent{Object: claimed}) {
t.Fatal("claimed PersistentVolume create should reconcile")
}
if pred.Create(event.CreateEvent{Object: unclaimed}) {
t.Fatal("unclaimed PersistentVolume create should not reconcile")
}
if pred.Delete(event.DeleteEvent{Object: claimed}) {
t.Fatal("PersistentVolume delete should not reconcile")
}
if pred.Generic(event.GenericEvent{Object: claimed}) {
t.Fatal("PersistentVolume generic event should not reconcile")
}
labelChanged := claimed.DeepCopy()
labelChanged.Labels[meta.TenantLabel] = ""
if !pred.Update(event.UpdateEvent{ObjectOld: claimed, ObjectNew: labelChanged}) {
t.Fatal("tenant label change should reconcile")
}
missingLabel := claimed.DeepCopy()
delete(missingLabel.Labels, meta.TenantLabel)
emptyLabel := missingLabel.DeepCopy()
emptyLabel.Labels[meta.TenantLabel] = ""
if !pred.Update(event.UpdateEvent{ObjectOld: missingLabel, ObjectNew: emptyLabel}) {
t.Fatal("adding an empty tenant label should reconcile")
}
if !pred.Update(event.UpdateEvent{ObjectOld: emptyLabel, ObjectNew: missingLabel}) {
t.Fatal("removing an empty tenant label should reconcile")
}
claimChanged := claimed.DeepCopy()
claimChanged.Spec.ClaimRef = claimChanged.Spec.ClaimRef.DeepCopy()
claimChanged.Spec.ClaimRef.Namespace = "tenant-b-ns"
if !pred.Update(event.UpdateEvent{ObjectOld: claimed, ObjectNew: claimChanged}) {
t.Fatal("claim reference change should reconcile")
}
unrelatedChange := claimed.DeepCopy()
unrelatedChange.Status.Phase = corev1.VolumeBound
if pred.Update(event.UpdateEvent{ObjectOld: claimed, ObjectNew: unrelatedChange}) {
t.Fatal("unrelated PersistentVolume update should not reconcile")
}
}
func persistentVolumeTestScheme(t *testing.T) *runtime.Scheme {
t.Helper()
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatalf("add core scheme: %v", err)
}
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
t.Fatalf("add Capsule scheme: %v", err)
}
return scheme
}
func persistentVolumeTestTenant() *capsulev1beta2.Tenant {
return &capsulev1beta2.Tenant{ObjectMeta: metav1.ObjectMeta{
Name: "tenant-a",
UID: types.UID("tenant-a-uid"),
}}
}
func persistentVolumeTestNamespace(tnt *capsulev1beta2.Tenant) *corev1.Namespace {
return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
Name: "tenant-a-ns",
Labels: map[string]string{meta.TenantLabel: tnt.Name},
OwnerReferences: []metav1.OwnerReference{{
APIVersion: capsulev1beta2.GroupVersion.String(),
Kind: "Tenant",
Name: tnt.Name,
UID: tnt.UID,
}},
}}
}
func persistentVolumeTestVolume(namespace string, labels map[string]string) *corev1.PersistentVolume {
return &corev1.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{
Name: "pv-" + namespace,
Labels: labels,
},
Spec: corev1.PersistentVolumeSpec{ClaimRef: &corev1.ObjectReference{
Namespace: namespace,
Name: "claim",
}},
}
}
type updateCountingClient struct {
client.Client
updates int
}
func (c *updateCountingClient) Update(
ctx context.Context,
obj client.Object,
opts ...client.UpdateOption,
) error {
c.updates++
return c.Client.Update(ctx, obj, opts...)
}
type namespaceFailingReader struct {
client.Reader
err error
}
func (r *namespaceFailingReader) Get(
ctx context.Context,
key client.ObjectKey,
obj client.Object,
opts ...client.GetOption,
) error {
if _, ok := obj.(*corev1.Namespace); ok {
return fmt.Errorf("get namespace: %w", r.err)
}
return r.Reader.Get(ctx, key, obj, opts...)
}