Files
Oliver BählerandGitHub 305cabd4f6 fix: consistently reconcile quotas from rules (#2083)
* fix: do not mutate on update and bound pvcs

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

* fix: do not mutate on update and bound pvcs

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

* fix: consistently reconcile quotas from rules

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

---------

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

223 lines
7.0 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package globalresourcequotas
import (
"context"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
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/fake"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
func TestObserveUsageTracksTotalAndNamespaces(t *testing.T) {
t.Parallel()
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := capsulev1beta2.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
hard := corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("8"),
corev1.ResourceRequestsMemory: resource.MustParse("16Gi"),
}
quota := &capsulev1beta2.GlobalResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "shared", UID: types.UID("quota-uid")},
Spec: capsulev1beta2.GlobalResourceQuotaSpec{
Quota: corev1.ResourceQuotaSpec{Hard: hard},
},
}
namespaceA := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: "a"},
Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive},
}
namespaceB := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: "b"},
Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive},
}
resourceQuotaA := observedResourceQuota(quota, namespaceA.Name, corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("2"),
corev1.ResourceRequestsMemory: resource.MustParse("3Gi"),
})
resourceQuotaB := observedResourceQuota(quota, namespaceB.Name, corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("1"),
corev1.ResourceRequestsMemory: resource.MustParse("4Gi"),
})
cl := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(quota, resourceQuotaA, resourceQuotaB).
Build()
controller := &Controller{Client: cl, reader: cl}
status, initialized, err := controller.observeUsage(
context.Background(),
quota,
[]corev1.Namespace{namespaceB, namespaceA},
)
if err != nil {
t.Fatalf("observeUsage() error = %v", err)
}
if !initialized {
t.Fatal("observeUsage() initialized = false, want true")
}
if status.NamespaceSize != 2 || len(status.NamespaceUsage) != 2 {
t.Fatalf("namespace status = size %d usage %#v", status.NamespaceSize, status.NamespaceUsage)
}
if status.Namespaces[0] != "a" || status.Namespaces[1] != "b" {
t.Fatalf("ordered namespaces = %#v, want [a b]", status.Namespaces)
}
assertResource(t, status.Total.Used, corev1.ResourceRequestsCPU, "3")
assertResource(t, status.Total.Used, corev1.ResourceRequestsMemory, "7Gi")
assertResource(t, status.Total.Available, corev1.ResourceRequestsCPU, "5")
assertResource(t, status.NamespaceUsage["b"].Used, corev1.ResourceRequestsMemory, "4Gi")
}
func TestReconcileLedgerStatusConsumesObservedUsage(t *testing.T) {
t.Parallel()
now := metav1.Now()
expires := metav1.NewTime(now.Add(time.Minute))
current := &capsulev1beta2.QuantityLedgerResourceQuotaStatus{
Used: corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("0")},
Reservations: []capsulev1beta2.QuantityLedgerResourceQuotaReservation{{
ID: "request",
Delta: corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("2")},
ExpiresAt: &expires,
}},
}
next := reconcileLedgerStatus(
current,
3,
[]string{"a", "b"},
corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("1")},
true,
now,
)
if len(next.Reservations) != 1 {
t.Fatalf("reservations = %d, want 1", len(next.Reservations))
}
assertResource(t, next.Reservations[0].Delta, corev1.ResourceRequestsCPU, "1")
assertResource(t, next.Used, corev1.ResourceRequestsCPU, "1")
assertResource(t, next.Allocated, corev1.ResourceRequestsCPU, "2")
if next.ObservedGeneration != 3 || len(next.Namespaces) != 2 {
t.Fatalf("ledger snapshot = generation %d, namespaces %#v", next.ObservedGeneration, next.Namespaces)
}
}
func TestMatchingNamespaceSelectorsUseOR(t *testing.T) {
t.Parallel()
quota := &capsulev1beta2.GlobalResourceQuota{
Spec: capsulev1beta2.GlobalResourceQuotaSpec{
NamespaceSelectors: []selectors.NamespaceSelector{
{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "a"}}},
{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "b"}}},
},
},
}
namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
Name: "b", Labels: map[string]string{"team": "b"},
}}
cl := fake.NewClientBuilder().WithObjects(namespace).Build()
matched, err := selectors.GetNamespacesMatchingSelectors(
context.Background(),
cl,
quota.Spec.NamespaceSelectors,
)
if err != nil {
t.Fatal(err)
}
if len(matched) != 1 || matched[0].Name != namespace.Name {
t.Fatalf("matched namespaces = %#v, want b", matched)
}
}
func TestProjectedResourceQuotaSpecExposesGlobalRemainingCapacity(t *testing.T) {
t.Parallel()
quota := corev1.ResourceQuotaSpec{
Hard: corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("10"),
corev1.ResourceRequestsMemory: resource.MustParse("4Gi"),
},
Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotTerminating},
}
status := &capsulev1beta2.GlobalResourceQuotaStatus{
Total: capsulev1beta2.GlobalResourceQuotaUsage{
Available: corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("4"),
corev1.ResourceRequestsMemory: resource.MustParse("0"),
},
},
NamespaceUsage: capsulev1beta2.GlobalResourceQuotaNamespaceUsage{
"team-a": {
Used: corev1.ResourceList{
corev1.ResourceRequestsCPU: resource.MustParse("2"),
corev1.ResourceRequestsMemory: resource.MustParse("5Gi"),
},
},
},
}
projected := projectedResourceQuotaSpec(quota, status, "team-a")
assertResource(t, projected.Hard, corev1.ResourceRequestsCPU, "6")
assertResource(t, projected.Hard, corev1.ResourceRequestsMemory, "5Gi")
if len(projected.Scopes) != 1 || projected.Scopes[0] != corev1.ResourceQuotaScopeNotTerminating {
t.Fatalf("projected scopes = %#v, want NotTerminating", projected.Scopes)
}
// The desired shared limit remains immutable while projecting a namespace's
// native hard values.
assertResource(t, quota.Hard, corev1.ResourceRequestsCPU, "10")
assertResource(t, quota.Hard, corev1.ResourceRequestsMemory, "4Gi")
}
func observedResourceQuota(
quota *capsulev1beta2.GlobalResourceQuota,
namespace string,
used corev1.ResourceList,
) *corev1.ResourceQuota {
return &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: quota.GetResourceQuotaName(),
Namespace: namespace,
},
Status: corev1.ResourceQuotaStatus{
Hard: quota.Spec.Quota.Hard.DeepCopy(),
Used: used.DeepCopy(),
},
}
}
func assertResource(
t *testing.T,
resources corev1.ResourceList,
name corev1.ResourceName,
want string,
) {
t.Helper()
got := resources[name]
if got.Cmp(resource.MustParse(want)) != 0 {
t.Fatalf("%s = %s, want %s", name, got.String(), want)
}
}