mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-21 05:26:56 +00:00
* 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>
326 lines
8.8 KiB
Go
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...)
|
|
}
|