mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 20:46:42 +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>
315 lines
7.8 KiB
Go
315 lines
7.8 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package customquotas
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/go-logr/logr"
|
|
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
|
"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/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/internal/cache"
|
|
"github.com/projectcapsule/capsule/pkg/api/meta"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/quota"
|
|
)
|
|
|
|
type quotaUsageReconcileInput struct {
|
|
Log logr.Logger
|
|
|
|
Client client.Client
|
|
Mapper k8smeta.RESTMapper
|
|
|
|
JSONPathCache *cache.JSONPathCache
|
|
CELCache *cache.CELCache
|
|
|
|
Sources []capsulev1beta2.CustomQuotaSpecSource
|
|
ScopeSelectors []metav1.LabelSelector
|
|
|
|
// For namespaced CustomQuota:
|
|
// []string{instance.Namespace}
|
|
//
|
|
// For GlobalCustomQuota:
|
|
// resolved namespaces or []string{"*"}
|
|
Namespaces []string
|
|
|
|
// Namespaced CustomQuota should only accept namespaced targets.
|
|
RequireNamespacedTargets bool
|
|
|
|
// Used for compiled target cache.
|
|
CacheKey string
|
|
TargetsCache *cache.CompiledTargetsCache[string]
|
|
}
|
|
|
|
type quotaUsageReconcileResult struct {
|
|
Targets []capsulev1beta2.CustomQuotaStatusTarget
|
|
Usage capsulev1beta2.CustomQuotaStatusUsage
|
|
Claims []capsulev1beta2.CustomQuotaClaimItem
|
|
}
|
|
|
|
type quotaClaimKey struct {
|
|
UID types.UID
|
|
Group string
|
|
Version string
|
|
Kind string
|
|
Namespace string
|
|
Name string
|
|
}
|
|
|
|
func reconcileQuotaUsage(
|
|
ctx context.Context,
|
|
in quotaUsageReconcileInput,
|
|
limit resource.Quantity,
|
|
) (quotaUsageReconcileResult, error) {
|
|
out := quotaUsageReconcileResult{
|
|
Targets: []capsulev1beta2.CustomQuotaStatusTarget{},
|
|
Usage: capsulev1beta2.CustomQuotaStatusUsage{},
|
|
Claims: nil,
|
|
}
|
|
|
|
for _, src := range in.Sources {
|
|
kind := src.GroupVersionKind()
|
|
|
|
mapping, err := in.Mapper.RESTMapping(kind.GroupKind(), kind.Version)
|
|
if err != nil {
|
|
return out, fmt.Errorf("failed to resolve REST mapping for %s: %w", kind.String(), err)
|
|
}
|
|
|
|
if in.RequireNamespacedTargets && mapping.Scope.Name() != k8smeta.RESTScopeNameNamespace {
|
|
return out, fmt.Errorf("GVK %s is not namespaced", kind.String())
|
|
}
|
|
|
|
out.Targets = append(out.Targets, capsulev1beta2.CustomQuotaStatusTarget{
|
|
GroupVersionKind: metav1.GroupVersionKind(kind),
|
|
CustomQuotaSpecSourceConfig: src.CustomQuotaSpecSourceConfig,
|
|
Scope: mapping.Scope.Name(),
|
|
})
|
|
}
|
|
|
|
targets, err := CompileTargets(in.JSONPathCache, in.CELCache, out.Targets)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
|
|
if in.TargetsCache != nil && in.CacheKey != "" {
|
|
in.TargetsCache.Set(in.CacheKey, targets)
|
|
}
|
|
|
|
var errs []error
|
|
|
|
itemsByGVK := make(map[schema.GroupVersionKind][]unstructured.Unstructured, len(out.Targets))
|
|
claimsByKey := make(map[quotaClaimKey]capsulev1beta2.CustomQuotaClaimItem)
|
|
|
|
for _, target := range targets {
|
|
gvk := schema.GroupVersionKind{
|
|
Group: target.Group,
|
|
Version: target.Version,
|
|
Kind: target.Kind,
|
|
}
|
|
|
|
items, ok := itemsByGVK[gvk]
|
|
if !ok {
|
|
items, err = getResourcesByGVK(
|
|
ctx,
|
|
gvk,
|
|
in.Client,
|
|
target.Scope == k8smeta.RESTScopeNameNamespace,
|
|
in.ScopeSelectors,
|
|
in.Namespaces...,
|
|
)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("list resources for %s: %w", gvk.String(), err))
|
|
|
|
continue
|
|
}
|
|
|
|
itemsByGVK[gvk] = items
|
|
}
|
|
|
|
in.Log.V(5).Info("listed resources for target",
|
|
"gvk", gvk.String(),
|
|
"count", len(items),
|
|
"namespaces", in.Namespaces,
|
|
"scopeSelectors", in.ScopeSelectors,
|
|
)
|
|
|
|
for _, item := range items {
|
|
matches, err := MatchesCompiledSelectorsWithFields(ctx, item, target.CompiledSelectors)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf(
|
|
"evaluate selectors for %s/%s (%s): %w",
|
|
item.GetNamespace(),
|
|
item.GetName(),
|
|
item.GetObjectKind().GroupVersionKind().String(),
|
|
err,
|
|
))
|
|
|
|
continue
|
|
}
|
|
|
|
if !matches {
|
|
continue
|
|
}
|
|
|
|
rawUsage, err := usageForTarget(ctx, item, target)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
|
|
continue
|
|
}
|
|
|
|
accountingUsage := rawUsage.DeepCopy()
|
|
|
|
switch target.Operation {
|
|
case quota.OpSub:
|
|
accountingUsage.Neg()
|
|
out.Usage.Used.Add(accountingUsage)
|
|
quota.ClampQuantityToZero(&out.Usage.Used)
|
|
|
|
case quota.OpAdd, quota.OpCount:
|
|
out.Usage.Used.Add(accountingUsage)
|
|
|
|
default:
|
|
errs = append(errs, fmt.Errorf(
|
|
"unsupported operation %q for %s/%s (%s)",
|
|
target.Operation,
|
|
item.GetNamespace(),
|
|
item.GetName(),
|
|
item.GetObjectKind().GroupVersionKind().String(),
|
|
))
|
|
|
|
continue
|
|
}
|
|
|
|
key := quotaClaimKey{
|
|
UID: item.GetUID(),
|
|
Group: target.Group,
|
|
Version: target.Version,
|
|
Kind: target.Kind,
|
|
Namespace: item.GetNamespace(),
|
|
Name: item.GetName(),
|
|
}
|
|
|
|
claim, exists := claimsByKey[key]
|
|
if !exists {
|
|
claim = capsulev1beta2.CustomQuotaClaimItem{
|
|
GroupVersionKind: metav1.GroupVersionKind{
|
|
Group: target.Group,
|
|
Version: target.Version,
|
|
Kind: target.Kind,
|
|
},
|
|
NamespacedObjectWithUIDReference: meta.NamespacedObjectWithUIDReference{
|
|
Name: item.GetName(),
|
|
Namespace: meta.RFC1123SubdomainName(item.GetNamespace()),
|
|
UID: item.GetUID(),
|
|
},
|
|
Usage: resource.MustParse("0"),
|
|
}
|
|
}
|
|
|
|
// Claims must mirror the same net per-object contribution used by admission reservations.
|
|
// This is required so reservationMaterializedLedger(res, claims) can clear reservations.
|
|
//
|
|
// For pure subtraction sources, the claim remains present but clamps to zero.
|
|
// Example:
|
|
// claim usage = max(0 - 2Gi, 0) = 0
|
|
//
|
|
// For mixed sources, e.g. count + sub cpu:
|
|
// claim usage = max(1 - 500m, 0) = 500m
|
|
claim.Usage.Add(accountingUsage)
|
|
quota.ClampQuantityToZero(&claim.Usage)
|
|
|
|
claimsByKey[key] = claim
|
|
}
|
|
}
|
|
|
|
quota.ClampQuantityToZero(&out.Usage.Used)
|
|
|
|
out.Usage.Available = limit.DeepCopy()
|
|
out.Usage.Available.Sub(out.Usage.Used)
|
|
quota.ClampQuantityToZero(&out.Usage.Available)
|
|
|
|
out.Claims = make([]capsulev1beta2.CustomQuotaClaimItem, 0, len(claimsByKey))
|
|
for _, claim := range claimsByKey {
|
|
out.Claims = append(out.Claims, claim)
|
|
}
|
|
|
|
sort.SliceStable(out.Claims, func(i, j int) bool {
|
|
if out.Claims[i].Namespace != out.Claims[j].Namespace {
|
|
return out.Claims[i].Namespace < out.Claims[j].Namespace
|
|
}
|
|
|
|
if out.Claims[i].Kind != out.Claims[j].Kind {
|
|
return out.Claims[i].Kind < out.Claims[j].Kind
|
|
}
|
|
|
|
return out.Claims[i].Name < out.Claims[j].Name
|
|
})
|
|
|
|
if len(errs) > 0 {
|
|
return out, errors.Join(errs...)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func usageForTarget(
|
|
ctx context.Context,
|
|
item unstructured.Unstructured,
|
|
target cache.CompiledTarget,
|
|
) (resource.Quantity, error) {
|
|
switch target.Operation {
|
|
case quota.OpCount:
|
|
return *resource.NewQuantity(1, resource.DecimalSI), nil
|
|
|
|
case quota.OpAdd, quota.OpSub:
|
|
var (
|
|
usage resource.Quantity
|
|
err error
|
|
)
|
|
|
|
switch {
|
|
case target.CompiledCEL != nil:
|
|
usage, err = target.CompiledCEL.EvaluateQuantity(ctx, item)
|
|
case target.CompiledPath != nil:
|
|
usage, err = quota.ParseQuantityFromUnstructured(item, target.CompiledPath)
|
|
default:
|
|
err = fmt.Errorf("compiled usage expression is missing")
|
|
}
|
|
|
|
if err != nil {
|
|
return resource.Quantity{}, fmt.Errorf(
|
|
"get usage from %s/%s (%s) path %q cel %q op %q: %w",
|
|
item.GetNamespace(),
|
|
item.GetName(),
|
|
item.GetObjectKind().GroupVersionKind().String(),
|
|
target.Path,
|
|
target.CEL,
|
|
target.Operation,
|
|
err,
|
|
)
|
|
}
|
|
|
|
return usage, nil
|
|
|
|
default:
|
|
return resource.Quantity{}, fmt.Errorf(
|
|
"unsupported operation %q for %s/%s (%s)",
|
|
target.Operation,
|
|
item.GetNamespace(),
|
|
item.GetName(),
|
|
item.GetObjectKind().GroupVersionKind().String(),
|
|
)
|
|
}
|
|
}
|