mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
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>
This commit is contained in:
@@ -128,15 +128,15 @@ func (r *Controller) reconcile(
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := r.syncResourceQuotas(ctx, instance, namespaces); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
status, initialized, err := r.observeUsage(ctx, instance, namespaces)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := r.syncResourceQuotas(ctx, instance, namespaces, status); err != nil {
|
||||
return status, false, err
|
||||
}
|
||||
|
||||
ledger, err := r.ensureLedger(ctx, instance)
|
||||
if err != nil {
|
||||
return status, false, err
|
||||
@@ -160,6 +160,7 @@ func (r *Controller) syncResourceQuotas(
|
||||
ctx context.Context,
|
||||
instance *capsulev1beta2.GlobalResourceQuota,
|
||||
namespaces []corev1.Namespace,
|
||||
status *capsulev1beta2.GlobalResourceQuotaStatus,
|
||||
) error {
|
||||
selected := make(map[string]struct{}, len(namespaces))
|
||||
|
||||
@@ -184,7 +185,7 @@ func (r *Controller) syncResourceQuotas(
|
||||
targetLabels[meta.NewManagedByCapsuleLabel] = meta.ValueController
|
||||
targetLabels[meta.GlobalResourceQuotaLabel] = instance.Name
|
||||
target.SetLabels(targetLabels)
|
||||
target.Spec = *instance.Spec.Quota.DeepCopy()
|
||||
target.Spec = projectedResourceQuotaSpec(instance.Spec.Quota, status, namespace.Name)
|
||||
|
||||
return controllerutil.SetControllerReference(instance, target, r.Scheme())
|
||||
})
|
||||
@@ -221,6 +222,40 @@ func (r *Controller) syncResourceQuotas(
|
||||
return nil
|
||||
}
|
||||
|
||||
// projectedResourceQuotaSpec gives every selected namespace access to the
|
||||
// quota which is still available globally, while retaining that namespace's
|
||||
// already-observed usage in its native ResourceQuota hard limit. Consequently
|
||||
// Spec.Hard-Status.Used exposes the same remaining capacity in every
|
||||
// namespace. When the global quota is exhausted or over limit, Hard is pinned
|
||||
// to the namespace's current usage so native ResourceQuota admission blocks
|
||||
// further consumption.
|
||||
func projectedResourceQuotaSpec(
|
||||
quota corev1.ResourceQuotaSpec,
|
||||
status *capsulev1beta2.GlobalResourceQuotaStatus,
|
||||
namespace string,
|
||||
) corev1.ResourceQuotaSpec {
|
||||
desired := *quota.DeepCopy()
|
||||
desired.Hard = make(corev1.ResourceList, len(quota.Hard))
|
||||
|
||||
var namespaceUsed corev1.ResourceList
|
||||
if status != nil {
|
||||
namespaceUsed = status.NamespaceUsage[namespace].Used
|
||||
}
|
||||
|
||||
for name, hard := range quota.Hard {
|
||||
available := hard.DeepCopy()
|
||||
if status != nil {
|
||||
available = status.Total.Available[name].DeepCopy()
|
||||
}
|
||||
|
||||
projected := namespaceUsed[name].DeepCopy()
|
||||
projected.Add(available)
|
||||
desired.Hard[name] = projected
|
||||
}
|
||||
|
||||
return desired
|
||||
}
|
||||
|
||||
func (r *Controller) observeUsage(
|
||||
ctx context.Context,
|
||||
instance *capsulev1beta2.GlobalResourceQuota,
|
||||
|
||||
@@ -149,6 +149,47 @@ func TestMatchingNamespaceSelectorsUseOR(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user