Files
Oliver BählerandGitHub bdcdcefe63 feat: add globalresourcequota api (#2068)
* feat: add globalresourcequota api

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

196 lines
4.8 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package globalresourcequotas
import (
"context"
"reflect"
"slices"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
runtimequota "github.com/projectcapsule/capsule/pkg/runtime/quota"
)
func (r *Controller) ensureLedger(
ctx context.Context,
quota *capsulev1beta2.GlobalResourceQuota,
) (*capsulev1beta2.QuantityLedger, error) {
ledger := &capsulev1beta2.QuantityLedger{
ObjectMeta: metav1.ObjectMeta{
Name: quota.GetLedgerName(),
Namespace: configuration.ControllerNamespace(),
},
}
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, ledger, func() error {
ledgerLabels := ledger.GetLabels()
if ledgerLabels == nil {
ledgerLabels = map[string]string{}
}
ledgerLabels[meta.NewManagedByCapsuleLabel] = meta.ValueController
ledgerLabels[meta.GlobalResourceQuotaLabel] = quota.Name
ledger.SetLabels(ledgerLabels)
ledger.Spec.TargetRef = capsulev1beta2.QuantityLedgerTargetRef{
APIGroup: capsulev1beta2.GroupVersion.Group,
Kind: "GlobalResourceQuota",
Name: quota.Name,
UID: quota.UID,
}
return controllerutil.SetControllerReference(quota, ledger, r.Scheme())
})
if err != nil {
return nil, err
}
return ledger, nil
}
func (r *Controller) reconcileLedger(
ctx context.Context,
ledger *capsulev1beta2.QuantityLedger,
generation int64,
namespaces []string,
used corev1.ResourceList,
initialized bool,
) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
current := &capsulev1beta2.QuantityLedger{}
if err := r.reader.Get(ctx, types.NamespacedName{
Namespace: ledger.Namespace,
Name: ledger.Name,
}, current); err != nil {
return err
}
before := current.Status.ResourceQuota.DeepCopy()
next := reconcileLedgerStatus(
current.Status.ResourceQuota,
generation,
namespaces,
used,
initialized,
metav1.Now(),
)
if reflect.DeepEqual(before, next) {
return nil
}
current.Status.ResourceQuota = next
return r.Status().Update(ctx, current)
})
}
func reconcileLedgerStatus(
current *capsulev1beta2.QuantityLedgerResourceQuotaStatus,
generation int64,
namespaces []string,
used corev1.ResourceList,
initialized bool,
now metav1.Time,
) *capsulev1beta2.QuantityLedgerResourceQuotaStatus {
if current == nil {
current = &capsulev1beta2.QuantityLedgerResourceQuotaStatus{}
} else {
current = current.DeepCopy()
}
increase := positiveDifference(used, current.Used)
active := make([]capsulev1beta2.QuantityLedgerResourceQuotaReservation, 0, len(current.Reservations))
for _, reservation := range current.Reservations {
if reservation.ExpiresAt != nil && reservation.ExpiresAt.Before(&now) {
continue
}
reservation.Delta = consumeResourceList(reservation.Delta, increase)
if resourceListPositive(reservation.Delta) {
active = append(active, reservation)
}
}
reserved := capsulev1beta2.ZeroResourceList(used)
for _, reservation := range active {
addResourceList(reserved, reservation.Delta)
}
allocated := used.DeepCopy()
addResourceList(allocated, reserved)
current.ObservedGeneration = generation
current.Initialized = initialized
current.Namespaces = slices.Clone(namespaces)
current.Used = used.DeepCopy()
current.Reserved = reserved
current.Allocated = allocated
current.Reservations = active
return current
}
func positiveDifference(next, previous corev1.ResourceList) corev1.ResourceList {
out := make(corev1.ResourceList, len(next))
for name, quantity := range next {
delta := quantity.DeepCopy()
delta.Sub(previous[name])
runtimequota.ClampQuantityToZero(&delta)
out[name] = delta
}
return out
}
func consumeResourceList(delta, available corev1.ResourceList) corev1.ResourceList {
out := delta.DeepCopy()
for name, quantity := range out {
increase := available[name]
if quantity.Sign() <= 0 || increase.Sign() <= 0 {
continue
}
consumed := quantity.DeepCopy()
if consumed.Cmp(increase) > 0 {
consumed = increase.DeepCopy()
}
quantity.Sub(consumed)
out[name] = quantity
increase.Sub(consumed)
available[name] = increase
}
return out
}
func addResourceList(target, addition corev1.ResourceList) {
for name, quantity := range addition {
current := target[name]
current.Add(quantity)
target[name] = current
}
}
func resourceListPositive(resources corev1.ResourceList) bool {
for _, quantity := range resources {
if quantity.Sign() > 0 {
return true
}
}
return false
}