mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
* chore * perfromance improvements Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * perfromance improvements Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat(performance): removed duplicate client calls from all admission paths Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
358 lines
12 KiB
Go
358 lines
12 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package predicates_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
admissionv1 "k8s.io/api/admissionregistration/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
networkingv1 "k8s.io/api/networking/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/predicates"
|
|
)
|
|
|
|
func TestObjectMetadataChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.ObjectMetadataChangedPredicate{}
|
|
oldPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Labels: map[string]string{"a": "b"}}}
|
|
statusOnly := oldPod.DeepCopy()
|
|
statusOnly.Status.Phase = corev1.PodRunning
|
|
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldPod, ObjectNew: statusOnly}) {
|
|
t.Fatal("status-only update must be filtered")
|
|
}
|
|
|
|
metadataChanged := oldPod.DeepCopy()
|
|
metadataChanged.Labels["a"] = "c"
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldPod, ObjectNew: metadataChanged}) {
|
|
t.Fatal("metadata update must be admitted")
|
|
}
|
|
|
|
if !p.Create(event.CreateEvent{Object: oldPod}) {
|
|
t.Fatal("create must be admitted")
|
|
}
|
|
|
|
if p.Delete(event.DeleteEvent{Object: oldPod}) {
|
|
t.Fatal("delete must be filtered")
|
|
}
|
|
}
|
|
|
|
func TestTenantManagedResourceChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.TenantManagedResourceChangedPredicate{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
old client.Object
|
|
new client.Object
|
|
want bool
|
|
}{
|
|
{
|
|
name: "limitrange spec drift without generation change",
|
|
old: &corev1.LimitRange{},
|
|
new: &corev1.LimitRange{Spec: corev1.LimitRangeSpec{Limits: []corev1.LimitRangeItem{{
|
|
Type: corev1.LimitTypeContainer,
|
|
}}}},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "resourcequota spec drift without generation change",
|
|
old: &corev1.ResourceQuota{},
|
|
new: &corev1.ResourceQuota{Spec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{
|
|
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
|
}}},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "resourcequota usage status is filtered",
|
|
old: &corev1.ResourceQuota{},
|
|
new: &corev1.ResourceQuota{Status: corev1.ResourceQuotaStatus{Used: corev1.ResourceList{
|
|
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
|
}}},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "networkpolicy spec drift",
|
|
old: &networkingv1.NetworkPolicy{},
|
|
new: &networkingv1.NetworkPolicy{Spec: networkingv1.NetworkPolicySpec{
|
|
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress},
|
|
}},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "rolebinding subjects drift",
|
|
old: &rbacv1.RoleBinding{},
|
|
new: &rbacv1.RoleBinding{Subjects: []rbacv1.Subject{{
|
|
Kind: rbacv1.UserKind,
|
|
Name: "alice",
|
|
}}},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "managed owner reference drift",
|
|
old: &corev1.LimitRange{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
|
|
Kind: "Tenant", Name: "tenant",
|
|
}}}},
|
|
new: &corev1.LimitRange{},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := p.Update(event.UpdateEvent{ObjectOld: tt.old, ObjectNew: tt.new}); got != tt.want {
|
|
t.Fatalf("Update() = %t, want %t", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidatingAdmissionConfigurationChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.ValidatingAdmissionConfigurationChangedPredicate{}
|
|
withoutHash := &admissionv1.ValidatingWebhookConfiguration{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "capsule"},
|
|
Webhooks: []admissionv1.ValidatingWebhook{{Name: "namespaces.projectcapsule.dev"}},
|
|
}
|
|
obj := withoutHash.DeepCopy()
|
|
obj.Annotations = map[string]string{
|
|
predicates.AdmissionStateHashAnnotation: predicates.ValidatingAdmissionStateHash(obj),
|
|
}
|
|
if p.Update(event.UpdateEvent{ObjectOld: withoutHash, ObjectNew: obj}) {
|
|
t.Fatal("controller hash update must not enqueue itself")
|
|
}
|
|
|
|
unchanged := obj.DeepCopy()
|
|
unchanged.ResourceVersion = "2"
|
|
if p.Update(event.UpdateEvent{ObjectOld: obj, ObjectNew: unchanged}) {
|
|
t.Fatal("controller-authored state must not enqueue itself")
|
|
}
|
|
|
|
drifted := unchanged.DeepCopy()
|
|
drifted.Webhooks[0].Name = "changed.projectcapsule.dev"
|
|
if !p.Update(event.UpdateEvent{ObjectOld: unchanged, ObjectNew: drifted}) {
|
|
t.Fatal("webhook drift must be admitted")
|
|
}
|
|
|
|
driftedWithHash := unchanged.DeepCopy()
|
|
driftedWithHash.Webhooks[0].Name = "changed-with-hash.projectcapsule.dev"
|
|
driftedWithHash.Annotations[predicates.AdmissionStateHashAnnotation] = "changed"
|
|
if !p.Update(event.UpdateEvent{ObjectOld: unchanged, ObjectNew: driftedWithHash}) {
|
|
t.Fatal("webhook drift combined with a hash annotation update must be admitted")
|
|
}
|
|
|
|
caOnly := unchanged.DeepCopy()
|
|
caOnly.Webhooks[0].ClientConfig.CABundle = []byte("rotated")
|
|
if p.Update(event.UpdateEvent{ObjectOld: unchanged, ObjectNew: caOnly}) {
|
|
t.Fatal("CA rotation is owned by the TLS controller and must be filtered")
|
|
}
|
|
}
|
|
|
|
func TestMutatingAdmissionConfigurationChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.MutatingAdmissionConfigurationChangedPredicate{}
|
|
withoutHash := &admissionv1.MutatingWebhookConfiguration{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "capsule"},
|
|
Webhooks: []admissionv1.MutatingWebhook{{Name: "pods.projectcapsule.dev"}},
|
|
}
|
|
obj := withoutHash.DeepCopy()
|
|
obj.Annotations = map[string]string{
|
|
predicates.AdmissionStateHashAnnotation: predicates.MutatingAdmissionStateHash(obj),
|
|
}
|
|
if p.Update(event.UpdateEvent{ObjectOld: withoutHash, ObjectNew: obj}) {
|
|
t.Fatal("controller hash update must not enqueue itself")
|
|
}
|
|
|
|
driftedWithHash := obj.DeepCopy()
|
|
driftedWithHash.Webhooks[0].Name = "changed.projectcapsule.dev"
|
|
driftedWithHash.Annotations[predicates.AdmissionStateHashAnnotation] = "changed"
|
|
if !p.Update(event.UpdateEvent{ObjectOld: obj, ObjectNew: driftedWithHash}) {
|
|
t.Fatal("webhook drift combined with a hash annotation update must be admitted")
|
|
}
|
|
}
|
|
|
|
func TestDependencyStateChangedPredicateNilObjects(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.DependencyStateChangedPredicate{}
|
|
obj := &capsulev1beta2.TenantResource{}
|
|
|
|
for _, e := range []event.UpdateEvent{
|
|
{ObjectNew: obj},
|
|
{ObjectOld: obj},
|
|
{},
|
|
} {
|
|
if p.Update(e) {
|
|
t.Fatal("update with a nil object must be filtered")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuantityLedgerWorkChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.QuantityLedgerWorkChangedPredicate{}
|
|
if p.Create(event.CreateEvent{Object: &capsulev1beta2.QuantityLedger{}}) {
|
|
t.Fatal("settled ledger creation must be filtered; the owning quota create already reconciles it")
|
|
}
|
|
|
|
createdWithWork := &capsulev1beta2.QuantityLedger{
|
|
Status: capsulev1beta2.QuantityLedgerStatus{
|
|
Reservations: []capsulev1beta2.QuantityLedgerReservation{{ID: "request"}},
|
|
},
|
|
}
|
|
if !p.Create(event.CreateEvent{Object: createdWithWork}) {
|
|
t.Fatal("ledger first observed with work must trigger reconciliation")
|
|
}
|
|
|
|
oldLedger := &capsulev1beta2.QuantityLedger{}
|
|
derived := oldLedger.DeepCopy()
|
|
derived.Status.Allocated = resource.MustParse("1")
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldLedger, ObjectNew: derived}) {
|
|
t.Fatal("derived allocation update must be filtered")
|
|
}
|
|
|
|
work := oldLedger.DeepCopy()
|
|
work.Status.Reservations = append(work.Status.Reservations, capsulev1beta2.QuantityLedgerReservation{ID: "request"})
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldLedger, ObjectNew: work}) {
|
|
t.Fatal("reservation update must be admitted")
|
|
}
|
|
|
|
moreWork := work.DeepCopy()
|
|
moreWork.Status.Reservations = append(
|
|
moreWork.Status.Reservations,
|
|
capsulev1beta2.QuantityLedgerReservation{ID: "request-2"},
|
|
)
|
|
if !p.Update(event.UpdateEvent{ObjectOld: work, ObjectNew: moreWork}) {
|
|
t.Fatal("additional work must be admitted in case the informer coalesced an intermediate settled state")
|
|
}
|
|
|
|
replacedWork := oldLedger.DeepCopy()
|
|
replacedWork.Status.PendingDeletes = []capsulev1beta2.QuantityLedgerPendingDelete{{
|
|
ID: "delete-request",
|
|
}}
|
|
if !p.Update(event.UpdateEvent{ObjectOld: work, ObjectNew: replacedWork}) {
|
|
t.Fatal("replacement work must be admitted when an intermediate settled state is not observed")
|
|
}
|
|
|
|
settled := moreWork.DeepCopy()
|
|
settled.Status.Reservations = nil
|
|
if p.Update(event.UpdateEvent{ObjectOld: moreWork, ObjectNew: settled}) {
|
|
t.Fatal("controller settlement updates must be filtered")
|
|
}
|
|
|
|
partiallySettled := moreWork.DeepCopy()
|
|
partiallySettled.Status.Reservations = partiallySettled.Status.Reservations[:1]
|
|
if p.Update(event.UpdateEvent{ObjectOld: moreWork, ObjectNew: partiallySettled}) {
|
|
t.Fatal("partial controller settlement updates must be filtered")
|
|
}
|
|
|
|
unchangedWork := work.DeepCopy()
|
|
unchangedWork.Status.Allocated = resource.MustParse("1")
|
|
if p.Update(event.UpdateEvent{ObjectOld: work, ObjectNew: unchangedWork}) {
|
|
t.Fatal("derived updates with unchanged work must be filtered")
|
|
}
|
|
}
|
|
|
|
func TestResourcePoolNamespacesChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.ResourcePoolNamespacesChangedPredicate{}
|
|
oldPool := &capsulev1beta2.ResourcePool{}
|
|
statusOnly := oldPool.DeepCopy()
|
|
statusOnly.Status.ClaimSize = 1
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldPool, ObjectNew: statusOnly}) {
|
|
t.Fatal("unrelated pool status update must be filtered")
|
|
}
|
|
|
|
namespacesChanged := oldPool.DeepCopy()
|
|
namespacesChanged.Status.Namespaces = []string{"tenant-a"}
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldPool, ObjectNew: namespacesChanged}) {
|
|
t.Fatal("namespace allocation update must be admitted")
|
|
}
|
|
|
|
deleting := oldPool.DeepCopy()
|
|
now := metav1.Now()
|
|
deleting.DeletionTimestamp = &now
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldPool, ObjectNew: deleting}) {
|
|
t.Fatal("deletion transition must be admitted")
|
|
}
|
|
}
|
|
|
|
func TestResourceQuotaUsageChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.ResourceQuotaUsageChangedPredicate{}
|
|
oldQuota := &corev1.ResourceQuota{}
|
|
|
|
hardOnly := oldQuota.DeepCopy()
|
|
hardOnly.Status.Hard = corev1.ResourceList{
|
|
corev1.ResourceLimitsCPU: resource.MustParse("2"),
|
|
}
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldQuota, ObjectNew: hardOnly}) {
|
|
t.Fatal("status.hard-only update must be filtered")
|
|
}
|
|
|
|
used := hardOnly.DeepCopy()
|
|
used.Status.Used = corev1.ResourceList{
|
|
corev1.ResourceLimitsCPU: resource.MustParse("1"),
|
|
}
|
|
if !p.Update(event.UpdateEvent{ObjectOld: hardOnly, ObjectNew: used}) {
|
|
t.Fatal("status.used update must be admitted")
|
|
}
|
|
|
|
usageReleased := used.DeepCopy()
|
|
usageReleased.Status.Used[corev1.ResourceLimitsCPU] = resource.MustParse("0")
|
|
if !p.Update(event.UpdateEvent{ObjectOld: used, ObjectNew: usageReleased}) {
|
|
t.Fatal("released usage update must be admitted")
|
|
}
|
|
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldQuota, ObjectNew: &corev1.ConfigMap{}}) {
|
|
t.Fatal("non-ResourceQuota update must be filtered")
|
|
}
|
|
}
|
|
|
|
func TestDeletionChangedPredicate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.DeletionChangedPredicate{}
|
|
oldObject := &corev1.ConfigMap{}
|
|
statusOnly := oldObject.DeepCopy()
|
|
if p.Update(event.UpdateEvent{ObjectOld: oldObject, ObjectNew: statusOnly}) {
|
|
t.Fatal("ordinary update must be filtered")
|
|
}
|
|
|
|
deleting := oldObject.DeepCopy()
|
|
now := metav1.Now()
|
|
deleting.DeletionTimestamp = &now
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldObject, ObjectNew: deleting}) {
|
|
t.Fatal("deletion timestamp transition must be admitted")
|
|
}
|
|
}
|
|
|
|
func TestClassChangedAdmitsDeletionTransition(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := predicates.ClassChanged()
|
|
oldTenant := &capsulev1beta2.Tenant{}
|
|
deleting := oldTenant.DeepCopy()
|
|
now := metav1.Now()
|
|
deleting.DeletionTimestamp = &now
|
|
|
|
if !p.Update(event.UpdateEvent{ObjectOld: oldTenant, ObjectNew: deleting}) {
|
|
t.Fatal("controller primary predicate must admit deletion transition")
|
|
}
|
|
}
|