mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-21 13:36:41 +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>
928 lines
26 KiB
Go
928 lines
26 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package customquota
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
admissionv1 "k8s.io/api/admission/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"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/webhook/admission"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/internal/cache"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
caprunt "github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/quota"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
|
|
)
|
|
|
|
func TestReserveCreateOnLedgerDryRunDoesNotMutate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "pods"}
|
|
ledger := ledgerForTest(key, "2")
|
|
cl := ledgerClientForTest(t, ledger)
|
|
|
|
usage := resource.MustParse("1")
|
|
reservation := capsulev1beta2.QuantityLedgerReservation{
|
|
ID: "dry-run",
|
|
Usage: usage.DeepCopy(),
|
|
Delta: quantityPtr(usage),
|
|
}
|
|
|
|
allowed, effective, _, err := reserveCreateOnLedger(
|
|
ctx,
|
|
cl,
|
|
cl,
|
|
evaluatedQuota{MatchedQuota: quota.MatchedQuota{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Limit: resource.MustParse("3"),
|
|
}},
|
|
&reservation,
|
|
true,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("reserveCreateOnLedger() error = %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Fatal("reserveCreateOnLedger() dry-run unexpectedly denied")
|
|
}
|
|
if effective.Cmp(resource.MustParse("3")) != 0 {
|
|
t.Fatalf("effective allocation = %s, want 3", effective.String())
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if got.Status.Allocated.Cmp(resource.MustParse("2")) != 0 {
|
|
t.Fatalf("persisted allocation = %s, want 2", got.Status.Allocated.String())
|
|
}
|
|
if len(got.Status.Reservations) != 0 {
|
|
t.Fatalf("persisted reservations = %d, want 0", len(got.Status.Reservations))
|
|
}
|
|
}
|
|
|
|
func TestReserveCreateOnLedgerReleasesExpiredReservation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "pods"}
|
|
ledger := ledgerForTest(key, "2")
|
|
expiredAt := metav1.NewTime(time.Now().Add(-time.Minute))
|
|
expiredUsage := resource.MustParse("1")
|
|
ledger.Status.Reserved = expiredUsage.DeepCopy()
|
|
ledger.Status.Reservations = []capsulev1beta2.QuantityLedgerReservation{
|
|
{
|
|
ID: "expired",
|
|
Usage: expiredUsage.DeepCopy(),
|
|
Delta: quantityPtr(expiredUsage),
|
|
ExpiresAt: &expiredAt,
|
|
},
|
|
}
|
|
cl := ledgerClientForTest(t, ledger)
|
|
|
|
usage := resource.MustParse("1")
|
|
reservation := capsulev1beta2.QuantityLedgerReservation{
|
|
ID: "current",
|
|
Usage: usage.DeepCopy(),
|
|
Delta: quantityPtr(usage),
|
|
}
|
|
|
|
allowed, effective, _, err := reserveCreateOnLedger(
|
|
ctx,
|
|
cl,
|
|
cl,
|
|
evaluatedQuota{MatchedQuota: quota.MatchedQuota{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Limit: resource.MustParse("2"),
|
|
}},
|
|
&reservation,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("reserveCreateOnLedger() error = %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Fatal("reserveCreateOnLedger() denied after an expired reservation")
|
|
}
|
|
if effective.Cmp(resource.MustParse("2")) != 0 {
|
|
t.Fatalf("effective allocation = %s, want 2", effective.String())
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if len(got.Status.Reservations) != 1 || got.Status.Reservations[0].ID != "current" {
|
|
t.Fatalf("active reservations = %#v, want only current", got.Status.Reservations)
|
|
}
|
|
}
|
|
|
|
func TestReplaceUsageOnLedgerDoesNotReleaseDecreaseBeforePersistence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "cpu"}
|
|
ledger := ledgerForTest(key, "10")
|
|
cl := ledgerClientForTest(t, ledger)
|
|
|
|
newUsage := resource.MustParse("1")
|
|
zero := resource.MustParse("0")
|
|
reservation := capsulev1beta2.QuantityLedgerReservation{
|
|
ID: "decrease",
|
|
Usage: newUsage.DeepCopy(),
|
|
Delta: quantityPtr(zero),
|
|
}
|
|
|
|
allowed, _, _, err := replaceUsageOnLedger(
|
|
ctx,
|
|
cl,
|
|
cl,
|
|
evaluatedQuota{MatchedQuota: quota.MatchedQuota{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Limit: resource.MustParse("10"),
|
|
}},
|
|
resource.MustParse("10"),
|
|
newUsage,
|
|
&reservation,
|
|
nil,
|
|
true,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("replaceUsageOnLedger() error = %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Fatal("replaceUsageOnLedger() unexpectedly denied a decrease")
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if got.Status.Allocated.Cmp(resource.MustParse("10")) != 0 {
|
|
t.Fatalf("allocation was released before persistence: got %s, want 10", got.Status.Allocated.String())
|
|
}
|
|
if len(got.Status.Reservations) != 1 {
|
|
t.Fatalf("reservations = %d, want 1", len(got.Status.Reservations))
|
|
}
|
|
if delta := reservationDelta(got.Status.Reservations[0]); !delta.IsZero() {
|
|
t.Fatalf("decrease reservation delta = %s, want 0", delta.String())
|
|
}
|
|
}
|
|
|
|
func TestReplaceUsageOnLedgerReservesOnlyPositiveDelta(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "cpu"}
|
|
ledger := ledgerForTest(key, "5")
|
|
cl := ledgerClientForTest(t, ledger)
|
|
|
|
newUsage := resource.MustParse("8")
|
|
delta := resource.MustParse("3")
|
|
reservation := capsulev1beta2.QuantityLedgerReservation{
|
|
ID: "increase",
|
|
Usage: newUsage.DeepCopy(),
|
|
Delta: quantityPtr(delta),
|
|
}
|
|
|
|
allowed, _, _, err := replaceUsageOnLedger(
|
|
ctx,
|
|
cl,
|
|
cl,
|
|
evaluatedQuota{MatchedQuota: quota.MatchedQuota{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Limit: resource.MustParse("8"),
|
|
}},
|
|
resource.MustParse("5"),
|
|
newUsage,
|
|
&reservation,
|
|
nil,
|
|
true,
|
|
false,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("replaceUsageOnLedger() error = %v", err)
|
|
}
|
|
if !allowed {
|
|
t.Fatal("replaceUsageOnLedger() unexpectedly denied")
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if got.Status.Allocated.Cmp(resource.MustParse("8")) != 0 {
|
|
t.Fatalf("allocation = %s, want 8", got.Status.Allocated.String())
|
|
}
|
|
if got.Status.Reserved.Cmp(delta) != 0 {
|
|
t.Fatalf("reserved = %s, want 3", got.Status.Reserved.String())
|
|
}
|
|
}
|
|
|
|
func TestRollbackUsageReplacementRemovesPendingDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "cpu"}
|
|
ref := capsulev1beta2.QuantityLedgerObjectRef{
|
|
APIVersion: "v1",
|
|
Kind: "Pod",
|
|
Namespace: "tenant-a",
|
|
Name: "pod-a",
|
|
UID: "pod-uid",
|
|
}
|
|
ledger := ledgerForTest(key, "10")
|
|
ledger.Status.PendingDeletes = []capsulev1beta2.QuantityLedgerPendingDelete{
|
|
{ID: "request-a", ObjectRef: ref, CreatedAt: metav1.Now()},
|
|
{ID: "request-b", ObjectRef: ref, CreatedAt: metav1.Now()},
|
|
}
|
|
cl := ledgerClientForTest(t, ledger)
|
|
pendingDelete := &capsulev1beta2.QuantityLedgerPendingDelete{
|
|
ID: "request-a",
|
|
ObjectRef: ref,
|
|
}
|
|
|
|
if err := rollbackUsageReplacementOnLedger(
|
|
ctx,
|
|
cl,
|
|
cl,
|
|
key,
|
|
"",
|
|
resource.MustParse("10"),
|
|
resource.MustParse("1"),
|
|
pendingDelete,
|
|
); err != nil {
|
|
t.Fatalf("rollbackUsageReplacementOnLedger() error = %v", err)
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if len(got.Status.PendingDeletes) != 1 {
|
|
t.Fatalf("pending deletes = %d, want 1", len(got.Status.PendingDeletes))
|
|
}
|
|
if got.Status.PendingDeletes[0].ID != "request-b" {
|
|
t.Fatalf("remaining pending delete = %q, want request-b", got.Status.PendingDeletes[0].ID)
|
|
}
|
|
if got.Status.Allocated.Cmp(resource.MustParse("10")) != 0 {
|
|
t.Fatalf("allocation = %s, want 10", got.Status.Allocated.String())
|
|
}
|
|
}
|
|
|
|
func TestReservationDeltaBackwardsCompatibility(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
usage := resource.MustParse("4")
|
|
got := reservationDelta(capsulev1beta2.QuantityLedgerReservation{Usage: usage})
|
|
if got.Cmp(usage) != 0 {
|
|
t.Fatalf("legacy reservation delta = %s, want %s", got.String(), usage.String())
|
|
}
|
|
}
|
|
|
|
func TestStatusSubresourceUpdateQueuesQuotaReconciliation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core scheme: %v", err)
|
|
}
|
|
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "active-pod-cpu"}
|
|
namespace := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{Name: key.Namespace},
|
|
}
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Generation: 1,
|
|
},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Limit: resource.MustParse("2"),
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpAdd,
|
|
Path: ".spec.containers[*].resources.limits.cpu",
|
|
Selectors: []selectors.SelectorWithFields{
|
|
{
|
|
FieldSelectors: []string{
|
|
".status.phase!=Succeeded",
|
|
".status.phase!=Failed",
|
|
".status.phase!=Unknown",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 1,
|
|
Usage: capsulev1beta2.CustomQuotaStatusUsage{
|
|
Used: resource.MustParse("2"),
|
|
Available: resource.MustParse("0"),
|
|
},
|
|
Conditions: meta.ConditionList{
|
|
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
|
|
},
|
|
},
|
|
}
|
|
ledger := ledgerForTest(key, "2")
|
|
|
|
cl := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithStatusSubresource(&capsulev1beta2.QuantityLedger{}).
|
|
WithObjects(namespace, customQuota, ledger).
|
|
Build()
|
|
|
|
oldPod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "completed",
|
|
Namespace: key.Namespace,
|
|
UID: "pod-uid",
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "main",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
corev1.ResourceCPU: resource.MustParse("1"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: corev1.PodStatus{Phase: corev1.PodRunning},
|
|
}
|
|
newPod := oldPod.DeepCopy()
|
|
newPod.Status.Phase = corev1.PodSucceeded
|
|
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
UID: "status-request",
|
|
Operation: admissionv1.Update,
|
|
Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Pod"},
|
|
Namespace: key.Namespace,
|
|
Name: oldPod.Name,
|
|
SubResource: "status",
|
|
OldObject: runtime.RawExtension{Object: oldPod},
|
|
Object: runtime.RawExtension{Object: newPod},
|
|
}}
|
|
|
|
if resp := handler.OnUpdate(cl, cl, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("status update response = %#v, want allowed", resp)
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if len(got.Status.PendingDeletes) != 1 {
|
|
t.Fatalf("pending deletes = %d, want 1", len(got.Status.PendingDeletes))
|
|
}
|
|
if got.Status.PendingDeletes[0].ObjectRef.UID != oldPod.UID {
|
|
t.Fatalf(
|
|
"pending delete UID = %q, want %q",
|
|
got.Status.PendingDeletes[0].ObjectRef.UID,
|
|
oldPod.UID,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestStatusSubresourceUpdateSkipsNotReadyQuota(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core scheme: %v", err)
|
|
}
|
|
|
|
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "tenant-a"}}
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "not-ready",
|
|
Namespace: namespace.Name,
|
|
Generation: 1,
|
|
},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Limit: resource.MustParse("1"),
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpCount,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 1,
|
|
Conditions: meta.ConditionList{
|
|
{Type: meta.ReadyCondition, Status: metav1.ConditionFalse},
|
|
},
|
|
},
|
|
}
|
|
cl := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithObjects(namespace, customQuota).
|
|
Build()
|
|
|
|
oldPod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pod-a", Namespace: namespace.Name, UID: "pod-uid"},
|
|
Status: corev1.PodStatus{Phase: corev1.PodPending},
|
|
}
|
|
newPod := oldPod.DeepCopy()
|
|
newPod.Status.Phase = corev1.PodRunning
|
|
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
req := statusUpdateRequest("not-ready-status", oldPod, newPod)
|
|
|
|
if resp := handler.OnUpdate(cl, cl, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("status update response = %#v, want fail-open allow", resp)
|
|
}
|
|
}
|
|
|
|
func TestStatusSubresourceUpdateUsesOnePolicySnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core scheme: %v", err)
|
|
}
|
|
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "tracked-pods"}
|
|
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: key.Namespace}}
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Generation: 1,
|
|
},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Limit: resource.MustParse("10"),
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpCount,
|
|
Selectors: []selectors.SelectorWithFields{
|
|
{
|
|
LabelSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"track": "yes"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 1,
|
|
Usage: capsulev1beta2.CustomQuotaStatusUsage{
|
|
Used: resource.MustParse("1"),
|
|
Available: resource.MustParse("9"),
|
|
},
|
|
Conditions: meta.ConditionList{
|
|
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
|
|
},
|
|
},
|
|
}
|
|
ledger := ledgerForTest(key, "1")
|
|
cl := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithStatusSubresource(&capsulev1beta2.QuantityLedger{}).
|
|
WithObjects(namespace, customQuota, ledger).
|
|
Build()
|
|
reader := &readinessFlappingReader{Reader: cl}
|
|
|
|
oldPod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pod-a",
|
|
Namespace: key.Namespace,
|
|
UID: "pod-uid",
|
|
Labels: map[string]string{"track": "yes"},
|
|
},
|
|
Status: corev1.PodStatus{Phase: corev1.PodPending},
|
|
}
|
|
newPod := oldPod.DeepCopy()
|
|
newPod.Status.Phase = corev1.PodRunning
|
|
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
req := statusUpdateRequest("unchanged-label-status", oldPod, newPod)
|
|
|
|
if resp := handler.OnUpdate(cl, reader, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("status update response = %#v, want allowed", resp)
|
|
}
|
|
if reader.customQuotaListCalls != 1 {
|
|
t.Fatalf(
|
|
"CustomQuota policy list calls = %d, want one immutable snapshot for old and new objects",
|
|
reader.customQuotaListCalls,
|
|
)
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if len(got.Status.Reservations) != 0 || len(got.Status.PendingDeletes) != 0 {
|
|
t.Fatalf(
|
|
"unchanged matching status update queued ledger work: reservations=%+v pendingDeletes=%+v",
|
|
got.Status.Reservations,
|
|
got.Status.PendingDeletes,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestStatusSubresourceIncreaseQueuesZeroDeltaWithoutEnforcement(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core scheme: %v", err)
|
|
}
|
|
|
|
key := types.NamespacedName{Namespace: "tenant-a", Name: "running-pods"}
|
|
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: key.Namespace}}
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
Generation: 1,
|
|
},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Limit: resource.MustParse("2"),
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpCount,
|
|
Selectors: []selectors.SelectorWithFields{
|
|
{FieldSelectors: []string{".status.phase=Running"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 1,
|
|
Usage: capsulev1beta2.CustomQuotaStatusUsage{
|
|
Used: resource.MustParse("2"),
|
|
Available: resource.MustParse("0"),
|
|
},
|
|
Conditions: meta.ConditionList{
|
|
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
|
|
},
|
|
},
|
|
}
|
|
ledger := ledgerForTest(key, "2")
|
|
cl := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithStatusSubresource(&capsulev1beta2.QuantityLedger{}).
|
|
WithObjects(namespace, customQuota, ledger).
|
|
Build()
|
|
|
|
oldPod := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pod-a", Namespace: key.Namespace, UID: "pod-uid"},
|
|
Status: corev1.PodStatus{Phase: corev1.PodPending},
|
|
}
|
|
newPod := oldPod.DeepCopy()
|
|
newPod.Status.Phase = corev1.PodRunning
|
|
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
req := statusUpdateRequest("increase-status", oldPod, newPod)
|
|
|
|
if resp := handler.OnUpdate(cl, cl, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("status update response = %#v, want allowed beyond quota limit", resp)
|
|
}
|
|
|
|
got := &capsulev1beta2.QuantityLedger{}
|
|
if err := cl.Get(ctx, key, got); err != nil {
|
|
t.Fatalf("get ledger: %v", err)
|
|
}
|
|
if len(got.Status.Reservations) != 1 {
|
|
t.Fatalf("reservations = %d, want one reconciliation notification", len(got.Status.Reservations))
|
|
}
|
|
if delta := reservationDelta(got.Status.Reservations[0]); !delta.IsZero() {
|
|
t.Fatalf("status notification delta = %s, want 0", delta.String())
|
|
}
|
|
if got.Status.Allocated.Cmp(resource.MustParse("2")) != 0 {
|
|
t.Fatalf("allocated = %s, want unchanged value 2", got.Status.Allocated.String())
|
|
}
|
|
}
|
|
|
|
func TestTerminatingNamespaceBypassesQuotaProcessing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := corev1.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add core scheme: %v", err)
|
|
}
|
|
|
|
now := metav1.Now()
|
|
namespace := &corev1.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "terminating",
|
|
DeletionTimestamp: &now,
|
|
Finalizers: []string{"kubernetes"},
|
|
},
|
|
}
|
|
cl := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithObjects(namespace).
|
|
Build()
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
UID: "terminating-status",
|
|
Operation: admissionv1.Update,
|
|
Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Pod"},
|
|
Namespace: namespace.Name,
|
|
Name: "pod-a",
|
|
SubResource: "status",
|
|
}}
|
|
|
|
if resp := handler.OnUpdate(cl, cl, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("terminating namespace status response = %#v, want allowed", resp)
|
|
}
|
|
|
|
req.Operation = admissionv1.Delete
|
|
req.SubResource = ""
|
|
if resp := handler.OnDelete(cl, cl, nil, nil)(ctx, req); resp != nil {
|
|
t.Fatalf("terminating namespace delete response = %#v, want allowed", resp)
|
|
}
|
|
}
|
|
|
|
func TestCustomQuotaReadyForAdmissionRequiresCurrentGeneration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
status := capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 3,
|
|
Conditions: meta.ConditionList{
|
|
{
|
|
Type: meta.ReadyCondition,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
}
|
|
|
|
if !customQuotaReadyForAdmission(3, status) {
|
|
t.Fatal("current ready generation must be active")
|
|
}
|
|
if customQuotaReadyForAdmission(4, status) {
|
|
t.Fatal("stale ready status must not activate a new generation")
|
|
}
|
|
|
|
status.Conditions[0].Status = metav1.ConditionFalse
|
|
if customQuotaReadyForAdmission(3, status) {
|
|
t.Fatal("non-ready quota must not be active")
|
|
}
|
|
}
|
|
|
|
func TestMatchAllQuotasFailsClosedUntilCurrentGenerationIsReady(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "pods",
|
|
Namespace: "tenant-a",
|
|
Generation: 2,
|
|
},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Limit: resource.MustParse("10"),
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpCount,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Status: capsulev1beta2.CustomQuotaStatus{
|
|
ObservedGeneration: 1,
|
|
Conditions: meta.ConditionList{
|
|
{Type: meta.ReadyCondition, Status: metav1.ConditionTrue},
|
|
},
|
|
},
|
|
}
|
|
|
|
cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(customQuota).Build()
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: cache.NewCompiledTargetsCache[string](),
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
request := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
Namespace: "tenant-a",
|
|
Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Pod"},
|
|
}}
|
|
object := unstructured.Unstructured{}
|
|
object.SetAPIVersion("v1")
|
|
object.SetKind("Pod")
|
|
object.SetNamespace("tenant-a")
|
|
object.SetName("pod-a")
|
|
|
|
_, err := handler.matchAllQuotas(ctx, cl, request, object)
|
|
if err == nil || !strings.Contains(err.Error(), "not ready for generation 2") {
|
|
t.Fatalf("matchAllQuotas() error = %v, want current-generation readiness error", err)
|
|
}
|
|
|
|
current := &capsulev1beta2.CustomQuota{}
|
|
if err := cl.Get(ctx, types.NamespacedName{Namespace: "tenant-a", Name: "pods"}, current); err != nil {
|
|
t.Fatalf("get CustomQuota: %v", err)
|
|
}
|
|
current.Status.ObservedGeneration = current.Generation
|
|
if err := cl.Update(ctx, current); err != nil {
|
|
t.Fatalf("mark CustomQuota ready: %v", err)
|
|
}
|
|
|
|
matched, err := handler.matchAllQuotas(ctx, cl, request, object)
|
|
if err != nil {
|
|
t.Fatalf("matchAllQuotas() error = %v", err)
|
|
}
|
|
if len(matched) != 1 {
|
|
t.Fatalf("matchAllQuotas() matches = %d, want 1", len(matched))
|
|
}
|
|
}
|
|
|
|
func TestCompiledTargetsCacheRefreshesInPlace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
targetsCache := cache.NewCompiledTargetsCache[string]()
|
|
handler := &objectCalculationHandler{
|
|
targetsCache: targetsCache,
|
|
jsonPathCache: cache.NewJSONPathCache(),
|
|
}
|
|
customQuota := &capsulev1beta2.CustomQuota{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "objects", Namespace: "tenant-a"},
|
|
Spec: capsulev1beta2.CustomQuotaSpec{
|
|
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
|
{
|
|
VersionKind: caprunt.VersionKind{APIVersion: "v1", Kind: "Pod"},
|
|
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
|
Operation: quota.OpCount,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
key := "tenant-a/objects"
|
|
targetsCache.Set(key, []cache.CompiledTarget{
|
|
{
|
|
CustomQuotaStatusTarget: capsulev1beta2.CustomQuotaStatusTarget{
|
|
GroupVersionKind: metav1.GroupVersionKind{Version: "v1", Kind: "Service"},
|
|
},
|
|
},
|
|
})
|
|
|
|
compiled, err := handler.getOrCompileCustomQuotaTargets(customQuota)
|
|
if err != nil {
|
|
t.Fatalf("getOrCompileCustomQuotaTargets() error = %v", err)
|
|
}
|
|
if len(compiled) != 1 || compiled[0].Kind != "Pod" {
|
|
t.Fatalf("compiled targets = %#v, want current Pod source", compiled)
|
|
}
|
|
if entries := targetsCache.Stats(); entries != 1 {
|
|
t.Fatalf("compiled target cache entries = %d, want one stable quota key", entries)
|
|
}
|
|
}
|
|
|
|
func ledgerForTest(key types.NamespacedName, allocated string) *capsulev1beta2.QuantityLedger {
|
|
return &capsulev1beta2.QuantityLedger{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
},
|
|
Status: capsulev1beta2.QuantityLedgerStatus{
|
|
Allocated: resource.MustParse(allocated),
|
|
},
|
|
}
|
|
}
|
|
|
|
func statusUpdateRequest(
|
|
uid types.UID,
|
|
oldPod *corev1.Pod,
|
|
newPod *corev1.Pod,
|
|
) admission.Request {
|
|
return admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
|
UID: uid,
|
|
Operation: admissionv1.Update,
|
|
Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Pod"},
|
|
Namespace: oldPod.Namespace,
|
|
Name: oldPod.Name,
|
|
SubResource: "status",
|
|
OldObject: runtime.RawExtension{Object: oldPod},
|
|
Object: runtime.RawExtension{Object: newPod},
|
|
}}
|
|
}
|
|
|
|
func ledgerClientForTest(t *testing.T, ledger *capsulev1beta2.QuantityLedger) client.Client {
|
|
t.Helper()
|
|
|
|
scheme := runtime.NewScheme()
|
|
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
|
|
t.Fatalf("add Capsule scheme: %v", err)
|
|
}
|
|
|
|
return fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithStatusSubresource(&capsulev1beta2.QuantityLedger{}).
|
|
WithObjects(ledger).
|
|
Build()
|
|
}
|
|
|
|
type readinessFlappingReader struct {
|
|
client.Reader
|
|
|
|
customQuotaListCalls int
|
|
}
|
|
|
|
func (r *readinessFlappingReader) List(
|
|
ctx context.Context,
|
|
list client.ObjectList,
|
|
opts ...client.ListOption,
|
|
) error {
|
|
if err := r.Reader.List(ctx, list, opts...); err != nil {
|
|
return err
|
|
}
|
|
|
|
quotas, ok := list.(*capsulev1beta2.CustomQuotaList)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
r.customQuotaListCalls++
|
|
if r.customQuotaListCalls < 2 {
|
|
return nil
|
|
}
|
|
|
|
for i := range quotas.Items {
|
|
condition := quotas.Items[i].Status.Conditions.GetConditionByType(meta.ReadyCondition)
|
|
if condition != nil {
|
|
condition.Status = metav1.ConditionFalse
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|