// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package customquota import ( "context" "fmt" "reflect" "slices" "sort" "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" 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/conversion" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/internal/cache" controller "github.com/projectcapsule/capsule/internal/controllers/customquotas" "github.com/projectcapsule/capsule/pkg/api/meta" ad "github.com/projectcapsule/capsule/pkg/runtime/admission" "github.com/projectcapsule/capsule/pkg/runtime/configuration" "github.com/projectcapsule/capsule/pkg/runtime/events" "github.com/projectcapsule/capsule/pkg/runtime/handlers" "github.com/projectcapsule/capsule/pkg/runtime/quota" "github.com/projectcapsule/capsule/pkg/runtime/selectors" ) // Might need some tuning in the. var customAdmissionBackoff = wait.Backoff{ Steps: 6, Duration: 20 * time.Millisecond, Factor: 1.5, Jitter: 0.2, } var ledgerMutationBackoff = wait.Backoff{ Steps: 8, Duration: 10 * time.Millisecond, Factor: 1.6, Jitter: 0.2, } type objectCalculationHandler struct { targetsCache *cache.CompiledTargetsCache[string] jsonPathCache *cache.JSONPathCache celCache *cache.CELCache } func ObjectCalculationHandler( targetsCache *cache.CompiledTargetsCache[string], jsonPathCache *cache.JSONPathCache, celCache *cache.CELCache, ) handlers.Handler { return &objectCalculationHandler{ targetsCache: targetsCache, jsonPathCache: jsonPathCache, celCache: celCache, } } func (h *objectCalculationHandler) OnCreate( c client.Client, reader client.Reader, decoder admission.Decoder, recorder events.EventRecorder, ) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { dryRun := req.DryRun != nil && *req.DryRun log := log.FromContext(ctx).WithValues( "op", "create", "kind", req.Kind.String(), "namespace", req.Namespace, "requestUID", string(req.UID), "name", req.Name, ) u, err := getUnstructured(req.Object) if err != nil { return ad.ErroredResponse(err) } var finalResp *admission.Response err = retry.OnError(customAdmissionBackoff, apierrors.IsConflict, func() error { matched, err := h.matchAllQuotas(ctx, reader, req, u) if err != nil { finalResp = ad.ErroredResponse(err) return nil } if len(matched) == 0 { return nil } evaluated, err := h.evaluateMatchedQuotas(ctx, u, matched) if err != nil { finalResp = ad.Denyf( "creating resource %s/%s (%s) cannot be admitted because custom quota usage could not be calculated: %v", req.Namespace, req.Name, req.Kind.String(), err, ) return nil } type appliedReservation struct { LedgerKey types.NamespacedName ReservationID string } applied := make([]appliedReservation, 0, len(evaluated)) for _, item := range evaluated { ledgerKey := quantityLedgerKeyForMatchedQuota(item) reservation := buildReservation(req, u, item.Usage, item.Usage, item.Key) allowed, effectiveUsed, reserved, err := reserveCreateOnLedger( ctx, c, reader, item, &reservation, dryRun, ) if err != nil { for _, a := range applied { _ = deleteLedgerReservation(ctx, c, reader, a.LedgerKey, a.ReservationID) } return err } if !allowed { for _, a := range applied { _ = deleteLedgerReservation(ctx, c, reader, a.LedgerKey, a.ReservationID) } available := item.Limit.DeepCopy() available.Sub(effectiveUsed) if available.Sign() < 0 { available = resource.MustParse("0") } log.V(5).Info("denying create due to quota", "quotaKey", item.Key, "quotaName", item.Name, "isGlobal", item.IsGlobal, "requestedUsage", item.Usage.String(), "currentUsed", effectiveUsed.String(), "available", available.String(), "limit", item.Limit.String(), "inflightReserved", reserved.String(), ) finalResp = ad.Denyf( "creating resource exceeds limit for %s %q (requested=%s, currentUsed=%s, available=%s, limit=%s, inflightReserved=%s)", quotaTypeName(item.IsGlobal), item.Name, item.Usage.String(), effectiveUsed.String(), available.String(), item.Limit.String(), reserved.String(), ) return nil } if !dryRun { applied = append(applied, appliedReservation{ LedgerKey: ledgerKey, ReservationID: reservation.ID, }) } } finalResp = nil return nil }) if err != nil { if apierrors.IsConflict(err) { return ad.Denyf( "custom quota admission could not reserve usage due to concurrent quota updates after %d attempts; please retry the request: %v", customAdmissionBackoff.Steps, err, ) } return ad.ErroredResponse(err) } return finalResp } } //nolint:gocognit,gocyclo,cyclop,maintidx func (h *objectCalculationHandler) OnUpdate( c client.Client, reader client.Reader, _ admission.Decoder, recorder events.EventRecorder, ) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { dryRun := req.DryRun != nil && *req.DryRun statusUpdate := req.SubResource == "status" logger := log.FromContext(ctx).WithValues( "op", "update", "kind", req.Kind.String(), "namespace", req.Namespace, "name", req.Name, "subresource", req.SubResource, ) terminating, namespaceErr := namespaceTerminating(ctx, c, req.Namespace) if namespaceErr != nil { logger.Error(namespaceErr, "cannot determine whether namespace is terminating") } else if terminating { logger.V(5).Info("allowing update without quota processing because namespace is terminating") return nil } oldObj, err := getUnstructured(req.OldObject) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because the previous object could not be decoded") return nil } return ad.ErroredResponse(err) } newObj, err := getUnstructured(req.Object) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because the new object could not be decoded") return nil } return ad.ErroredResponse(err) } var finalResp *admission.Response err = retry.OnError(customAdmissionBackoff, apierrors.IsConflict, func() error { policies, err := loadQuotaPolicySnapshot(ctx, reader, req.Namespace) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because quota policies could not be loaded") finalResp = nil return nil } finalResp = ad.ErroredResponse(err) return nil } oldMatched, err := h.matchAllQuotasFromSnapshot(ctx, reader, req, oldObj, policies) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because previous quota matches could not be evaluated") finalResp = nil return nil } finalResp = ad.ErroredResponse(err) return nil } newMatched, err := h.matchAllQuotasFromSnapshot(ctx, reader, req, newObj, policies) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because new quota matches could not be evaluated") finalResp = nil return nil } finalResp = ad.ErroredResponse(err) return nil } oldEvaluated, err := h.evaluateMatchedQuotas(ctx, oldObj, oldMatched) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because previous quota usage could not be calculated") finalResp = nil return nil } finalResp = ad.Denyf( "updating resource %s/%s (%s) cannot be admitted because previous custom quota usage could not be calculated: %v", req.Namespace, req.Name, req.Kind.String(), err, ) return nil } newEvaluated, err := h.evaluateMatchedQuotas(ctx, newObj, newMatched) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because new quota usage could not be calculated") finalResp = nil return nil } finalResp = ad.Denyf( "updating resource %s/%s (%s) cannot be admitted because new custom quota usage could not be calculated: %v", req.Namespace, req.Name, req.Kind.String(), err, ) return nil } oldByKey := evaluatedByKey(oldEvaluated) newByKey := evaluatedByKey(newEvaluated) relevantChange := meta.LabelsChangedUnstructured(oldObj, newObj) || len(oldByKey) != len(newByKey) if !relevantChange { for key, oldItem := range oldByKey { newItem, ok := newByKey[key] if !ok || oldItem.Usage.Cmp(newItem.Usage) != 0 { relevantChange = true break } } } if !relevantChange { finalResp = nil return nil } type appliedUpdate struct { LedgerKey types.NamespacedName ReservationID string OldUsage resource.Quantity NewUsage resource.Quantity PendingDelete *capsulev1beta2.QuantityLedgerPendingDelete } applied := make([]appliedUpdate, 0, len(oldByKey)+len(newByKey)) for _, key := range allKeys(oldByKey, newByKey) { oldItem, hadOld := oldByKey[key] newItem, hadNew := newByKey[key] var base evaluatedQuota switch { case hadNew: base = newItem case hadOld: base = oldItem default: continue } oldUsage := resource.MustParse("0") if hadOld { oldUsage = oldItem.Usage.DeepCopy() } newUsage := resource.MustParse("0") if hadNew { newUsage = newItem.Usage.DeepCopy() } ledgerKey := quantityLedgerKeyForMatchedQuota(base) var pendingDelete *capsulev1beta2.QuantityLedgerPendingDelete if hadOld && !hadNew { pendingDelete = &capsulev1beta2.QuantityLedgerPendingDelete{ ID: fmt.Sprintf("%s/%s", req.UID, base.Key), ObjectRef: capsulev1beta2.QuantityLedgerObjectRef{ APIGroup: req.Kind.Group, APIVersion: req.Kind.Version, Kind: req.Kind.Kind, Namespace: oldObj.GetNamespace(), Name: oldObj.GetName(), UID: oldObj.GetUID(), }, } } var reservation *capsulev1beta2.QuantityLedgerReservation if hadNew { delta := newUsage.DeepCopy() delta.Sub(oldUsage) quota.ClampQuantityToZero(&delta) // Status is observed state owned by Kubernetes controllers. // It must notify quota reconciliation, but it must never // reserve capacity or be rejected for exceeding a quota. if statusUpdate { delta = resource.MustParse("0") } r := buildReservation(req, newObj, newUsage, delta, base.Key) reservation = &r } allowed, effectiveUsed, reserved, err := replaceUsageOnLedger( ctx, c, reader, base, oldUsage, newUsage, reservation, pendingDelete, !statusUpdate, dryRun, ) if err != nil { for _, v := range slices.Backward(applied) { _ = rollbackUsageReplacementOnLedger( ctx, c, reader, v.LedgerKey, v.ReservationID, v.OldUsage, v.NewUsage, v.PendingDelete, ) } return err } if !allowed { for _, v := range slices.Backward(applied) { _ = rollbackUsageReplacementOnLedger( ctx, c, reader, v.LedgerKey, v.ReservationID, v.OldUsage, v.NewUsage, v.PendingDelete, ) } if statusUpdate { logger.Info("allowing status update despite quota limit") finalResp = nil return nil } available := base.Limit.DeepCopy() available.Sub(effectiveUsed) if available.Sign() < 0 { available = resource.MustParse("0") } finalResp = ad.Denyf( "updating resource exceeds limit for %s %q (requested=%s, currentUsed=%s, available=%s, limit=%s, inflightReserved=%s)", quotaTypeName(base.IsGlobal), base.Name, newUsage.String(), effectiveUsed.String(), available.String(), base.Limit.String(), reserved.String(), ) return nil } reservationID := "" if reservation != nil { reservationID = reservation.ID } if !dryRun { applied = append(applied, appliedUpdate{ LedgerKey: ledgerKey, ReservationID: reservationID, OldUsage: oldUsage.DeepCopy(), NewUsage: newUsage.DeepCopy(), PendingDelete: pendingDelete, }) } } finalResp = nil return nil }) if err != nil { if statusUpdate { logger.Error(err, "allowing status update because quota reconciliation could not be queued") return nil } if apierrors.IsConflict(err) { return ad.Denyf( "custom quota admission could not reserve usage due to concurrent quota updates after %d attempts; please retry the request: %v", customAdmissionBackoff.Steps, err, ) } return ad.ErroredResponse(err) } return finalResp } } func (h *objectCalculationHandler) OnDelete( c client.Client, reader client.Reader, _ admission.Decoder, recorder events.EventRecorder, ) handlers.Func { return func(ctx context.Context, req admission.Request) *admission.Response { if req.DryRun != nil && *req.DryRun { return nil } logger := log.FromContext(ctx).WithValues( "op", "delete", "kind", req.Kind.String(), "namespace", req.Namespace, "name", req.Name, ) // Namespace termination can fan out into many object deletions at once. // Use the local informer for this best-effort fast path so those deletes // do not each add an API read before quota processing can be skipped. // A stale non-terminating result only falls back to the normal, // conservative ledger path. terminating, err := namespaceTerminating(ctx, c, req.Namespace) if err != nil { logger.Error(err, "cannot determine whether namespace is terminating") } else if terminating { logger.V(5).Info("allowing delete without quota processing because namespace is terminating") return nil } oldObj, err := getUnstructured(req.OldObject) if err != nil { return ad.ErroredResponse(err) } uid := oldObj.GetUID() if uid == "" { return nil } objRef := capsulev1beta2.QuantityLedgerObjectRef{ APIGroup: req.Kind.Group, APIVersion: req.Kind.Version, Kind: req.Kind.Kind, Namespace: oldObj.GetNamespace(), Name: oldObj.GetName(), UID: uid, } namespacedcq := &capsulev1beta2.CustomQuotaList{} if err := reader.List(ctx, namespacedcq, client.InNamespace(req.Namespace)); err != nil { return ad.ErroredResponse(err) } for _, nscq := range namespacedcq.Items { if !nscq.Status.HasClaimUID(uid) { continue } ledgerKey := types.NamespacedName{ Name: nscq.GetName(), Namespace: nscq.GetNamespace(), } if err := addLedgerPendingDelete(ctx, c, reader, ledgerKey, objRef); err != nil { return ad.ErroredResponse(err) } } globalcq := &capsulev1beta2.GlobalCustomQuotaList{} if err := reader.List(ctx, globalcq); err != nil { return ad.ErroredResponse(err) } for _, gcq := range globalcq.Items { if !gcq.Status.HasClaimUID(uid) { continue } ledgerKey := types.NamespacedName{ Name: gcq.GetName(), Namespace: configuration.ControllerNamespace(), } if err := addLedgerPendingDelete(ctx, c, reader, ledgerKey, objRef); err != nil { return ad.ErroredResponse(err) } } return nil } } func deleteLedgerReservation( ctx context.Context, c client.Client, reader client.Reader, ledgerKey types.NamespacedName, reservationID string, ) error { return retry.RetryOnConflict(retry.DefaultBackoff, func() error { ledger := &capsulev1beta2.QuantityLedger{} if err := reader.Get(ctx, ledgerKey, ledger); err != nil { if apierrors.IsNotFound(err) { return nil } return err } active := make([]capsulev1beta2.QuantityLedgerReservation, 0, len(ledger.Status.Reservations)) released := resource.MustParse("0") for _, res := range ledger.Status.Reservations { if res.ID == reservationID { released.Add(reservationDelta(res)) continue } active = append(active, res) } if released.Sign() == 0 { return nil } allocated := ledger.Status.Allocated.DeepCopy() allocated.Sub(released) quota.ClampQuantityToZero(&allocated) reserved := resource.MustParse("0") for _, res := range active { reserved.Add(reservationDelta(res)) } ledger.Status.Reservations = active ledger.Status.Reserved = reserved ledger.Status.Allocated = allocated return c.Status().Update(ctx, ledger) }) } type quotaPolicySnapshot struct { namespaced []capsulev1beta2.CustomQuota global []capsulev1beta2.GlobalCustomQuota } func loadQuotaPolicySnapshot( ctx context.Context, reader client.Reader, namespace string, ) (quotaPolicySnapshot, error) { snapshot := quotaPolicySnapshot{} // Correctness requires an authoritative policy set. A single snapshot is // also reused for both sides of UPDATE admission so transient readiness // changes cannot turn an unchanged object into a false quota transition. if namespace != "" { list := &capsulev1beta2.CustomQuotaList{} if err := reader.List(ctx, list, client.InNamespace(namespace)); err != nil { return quotaPolicySnapshot{}, err } snapshot.namespaced = list.Items } global := &capsulev1beta2.GlobalCustomQuotaList{} if err := reader.List(ctx, global); err != nil { return quotaPolicySnapshot{}, err } snapshot.global = global.Items return snapshot, nil } func (h *objectCalculationHandler) matchAllQuotas( ctx context.Context, reader client.Reader, req admission.Request, u unstructured.Unstructured, ) ([]quota.MatchedQuota, error) { snapshot, err := loadQuotaPolicySnapshot(ctx, reader, req.Namespace) if err != nil { return nil, err } return h.matchAllQuotasFromSnapshot(ctx, reader, req, u, snapshot) } func (h *objectCalculationHandler) matchAllQuotasFromSnapshot( ctx context.Context, reader client.Reader, req admission.Request, u unstructured.Unstructured, snapshot quotaPolicySnapshot, ) ([]quota.MatchedQuota, error) { namespaced, err := h.matchCustomQuotasFromItems(ctx, req, u, snapshot.namespaced) if err != nil { return nil, err } global, err := h.matchGlobalCustomQuotasFromItems(ctx, reader, req, u, snapshot.global) if err != nil { return nil, err } out := make([]quota.MatchedQuota, 0, len(namespaced)+len(global)) out = append(out, namespaced...) out = append(out, global...) sort.SliceStable(out, func(i, j int) bool { if out[i].Limit.Cmp(out[j].Limit) != 0 { return out[i].Limit.Cmp(out[j].Limit) < 0 } if out[i].IsGlobal != out[j].IsGlobal { return out[i].IsGlobal } if out[i].Namespace != out[j].Namespace { return out[i].Namespace < out[j].Namespace } if out[i].SourceRank != out[j].SourceRank { return out[i].SourceRank < out[j].SourceRank } return out[i].Name < out[j].Name }) return out, nil } func (h *objectCalculationHandler) matchCustomQuotasFromItems( ctx context.Context, req admission.Request, u unstructured.Unstructured, items []capsulev1beta2.CustomQuota, ) ([]quota.MatchedQuota, error) { if req.Namespace == "" || len(items) == 0 { return nil, nil } objLabels := labels.Set(u.GetLabels()) out := make([]quota.MatchedQuota, 0) for _, cq := range items { if !sourcesTargetKind(cq.Spec.Sources, req.Kind) { continue } if !customQuotaReadyForAdmission(cq.Generation, cq.Status) { // Status is observed state and cannot be blocked by quota policy. // A NotReady quota has no reliable selector/usage model to notify, // so skip it and allow Kubernetes to persist the status update. if req.SubResource == "status" { continue } return nil, fmt.Errorf( "CustomQuota %s/%s is not ready for generation %d", cq.Namespace, cq.Name, cq.Generation, ) } if !selectors.MatchesSelectors(objLabels, cq.Spec.ScopeSelectors) { continue } compiledTargets, err := h.getOrCompileCustomQuotaTargets(&cq) if err != nil { return nil, fmt.Errorf("compile targets for CustomQuota %s/%s: %w", cq.Namespace, cq.Name, err) } for i, target := range compiledTargets { if target.Group != req.Kind.Group || target.Version != req.Kind.Version || target.Kind != req.Kind.Kind { continue } matches, err := controller.MatchesCompiledSelectorsWithFields(ctx, u, target.CompiledSelectors) if err != nil { return nil, fmt.Errorf( "evaluate selectors for %s/%s on CustomQuota %s/%s: %w", u.GetNamespace(), u.GetName(), cq.Namespace, cq.Name, err, ) } if !matches { continue } out = append(out, quota.MatchedQuota{ Key: controller.MakeCustomQuotaCacheKey(cq.Namespace, cq.Name), Name: cq.Name, Namespace: cq.Namespace, Path: target.Path, CompiledPath: target.CompiledPath, CEL: target.CEL, CompiledCEL: target.CompiledCEL, Operation: target.Operation, Limit: cq.Spec.Limit.DeepCopy(), Used: cq.Status.Usage.Used.DeepCopy(), IsGlobal: false, SourceRank: i, }) } } return out, nil } func (h *objectCalculationHandler) matchGlobalCustomQuotasFromItems( ctx context.Context, reader client.Reader, req admission.Request, u unstructured.Unstructured, items []capsulev1beta2.GlobalCustomQuota, ) ([]quota.MatchedQuota, error) { if len(items) == 0 { return nil, nil } objLabels := labels.Set(u.GetLabels()) out := make([]quota.MatchedQuota, 0) for _, gcq := range items { if !sourcesTargetKind(gcq.Spec.Sources, req.Kind) { continue } if !customQuotaReadyForAdmission(gcq.Generation, gcq.Status.CustomQuotaStatus) { if req.SubResource == "status" { continue } applies, err := desiredGlobalQuotaAppliesToNamespace(ctx, reader, &gcq, req.Namespace) if err != nil { return nil, fmt.Errorf( "evaluate namespaces for GlobalCustomQuota %s generation %d: %w", gcq.Name, gcq.Generation, err, ) } if !applies { continue } return nil, fmt.Errorf( "GlobalCustomQuota %s is not ready for generation %d", gcq.Name, gcq.Generation, ) } if !gcq.Status.NamespacePresent("*") && !gcq.Status.NamespacePresent(req.Namespace) { continue } if !selectors.MatchesSelectors(objLabels, gcq.Spec.ScopeSelectors) { continue } compiledTargets, err := h.getOrCompileGlobalCustomQuotaTargets(&gcq) if err != nil { return nil, fmt.Errorf("compile targets for GlobalCustomQuota %s: %w", gcq.Name, err) } for i, target := range compiledTargets { if target.Group != req.Kind.Group || target.Version != req.Kind.Version || target.Kind != req.Kind.Kind { continue } matches, err := controller.MatchesCompiledSelectorsWithFields(ctx, u, target.CompiledSelectors) if err != nil { return nil, fmt.Errorf( "evaluate selectors for %s/%s on GlobalCustomQuota %s: %w", u.GetNamespace(), u.GetName(), gcq.Name, err, ) } if !matches { continue } out = append(out, quota.MatchedQuota{ Key: controller.MakeGlobalCustomQuotaCacheKey(gcq.Name), Name: gcq.Name, Namespace: "", Path: target.Path, CompiledPath: target.CompiledPath, CEL: target.CEL, CompiledCEL: target.CompiledCEL, Operation: target.Operation, Limit: gcq.Spec.Limit.DeepCopy(), Used: gcq.Status.Usage.Used.DeepCopy(), IsGlobal: true, SourceRank: i, }) } } return out, nil } func getUnstructured(rawExt runtime.RawExtension) (unstructured.Unstructured, error) { var ( obj runtime.Object scope conversion.Scope ) err := runtime.Convert_runtime_RawExtension_To_runtime_Object(&rawExt, &obj, scope) if err != nil { return unstructured.Unstructured{}, err } innerObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return unstructured.Unstructured{}, err } u := unstructured.Unstructured{Object: innerObj} return u, nil } func namespaceTerminating( ctx context.Context, reader client.Reader, namespace string, ) (bool, error) { if namespace == "" { return false, nil } ns := &corev1.Namespace{} if err := reader.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil { if apierrors.IsNotFound(err) { return true, nil } return false, err } return ns.DeletionTimestamp != nil, nil } func quotaTypeName(global bool) string { if global { return "GlobalCustomQuota" } return "CustomQuota" } type evaluatedQuota struct { quota.MatchedQuota Usage resource.Quantity } func (h *objectCalculationHandler) evaluateMatchedQuotas( ctx context.Context, u unstructured.Unstructured, matched []quota.MatchedQuota, ) ([]evaluatedQuota, error) { log := log.FromContext(ctx) usageByExpression := make(map[string]resource.Quantity, len(matched)) for _, mq := range matched { // count does not use a calculation expression if mq.Operation == quota.OpCount { continue } expressionKey := matchedQuotaExpressionKey(mq) if _, ok := usageByExpression[expressionKey]; ok { continue } var ( usage resource.Quantity err error ) switch { case mq.CompiledCEL != nil: usage, err = mq.CompiledCEL.EvaluateQuantity(ctx, u) case mq.CompiledPath != nil: usage, err = quota.ParseQuantityFromUnstructured(u, mq.CompiledPath) default: err = fmt.Errorf("compiled usage expression is missing") } if err != nil { return nil, fmt.Errorf( "%s %q source path %q cel %q op %q did not resolve to a valid quantity: %w", quotaTypeName(mq.IsGlobal), mq.Name, mq.Path, mq.CEL, mq.Operation, err, ) } log.V(5).Info( "evaluated usage", "path", mq.Path, "cel", mq.CEL, "quantity", usage.String(), ) usageByExpression[expressionKey] = usage } byKey := make(map[string]evaluatedQuota, len(matched)) order := make([]string, 0, len(matched)) for _, mq := range matched { ev, ok := byKey[mq.Key] if !ok { ev = evaluatedQuota{ MatchedQuota: mq, Usage: resource.MustParse("0"), } order = append(order, mq.Key) } var usage resource.Quantity switch mq.Operation { case quota.OpCount: usage = *resource.NewQuantity(1, resource.DecimalSI) case quota.OpSub: usage = usageByExpression[matchedQuotaExpressionKey(mq)].DeepCopy() usage.Neg() ev.Usage.Add(usage) quota.ClampQuantityToZero(&ev.Usage) byKey[mq.Key] = ev continue case quota.OpAdd: usage = usageByExpression[matchedQuotaExpressionKey(mq)].DeepCopy() default: return nil, fmt.Errorf("unsupported quota operation %q for key %q", mq.Operation, mq.Key) } ev.Usage.Add(usage) byKey[mq.Key] = ev } out := make([]evaluatedQuota, 0, len(order)) for _, key := range order { out = append(out, byKey[key]) } return out, nil } func matchedQuotaExpressionKey(matched quota.MatchedQuota) string { if matched.CEL != "" { return "cel:" + matched.CEL } return "path:" + matched.Path } func addLedgerPendingDelete( ctx context.Context, c client.Client, reader client.Reader, ledgerKey types.NamespacedName, objRef capsulev1beta2.QuantityLedgerObjectRef, ) error { return retry.RetryOnConflict(ledgerMutationBackoff, func() error { ledger := &capsulev1beta2.QuantityLedger{} if err := reader.Get(ctx, ledgerKey, ledger); err != nil { return err } now := metav1.Now() for _, pd := range ledger.Status.PendingDeletes { if pd.ObjectRef.UID != "" && pd.ObjectRef.UID == objRef.UID { return nil } } if len(ledger.Status.PendingDeletes) >= maxQuantityLedgerPendingDeletes { return fmt.Errorf( "quantity ledger %s has reached the maximum of %d pending deletes", ledgerKey.String(), maxQuantityLedgerPendingDeletes, ) } ledger.Status.PendingDeletes = append(ledger.Status.PendingDeletes, capsulev1beta2.QuantityLedgerPendingDelete{ ObjectRef: objRef, CreatedAt: now, }) return c.Status().Update(ctx, ledger) }) } func (h *objectCalculationHandler) getOrCompileCustomQuotaTargets( cq *capsulev1beta2.CustomQuota, ) ([]cache.CompiledTarget, error) { key := controller.MakeCustomQuotaCacheKey(cq.Namespace, cq.Name) targets := customQuotaTargets(cq.Spec.Sources, cq.Status.Targets) return h.getOrCompileCurrentTargets(key, targets) } func (h *objectCalculationHandler) getOrCompileGlobalCustomQuotaTargets( gcq *capsulev1beta2.GlobalCustomQuota, ) ([]cache.CompiledTarget, error) { key := controller.MakeGlobalCustomQuotaCacheKey(gcq.Name) targets := customQuotaTargets(gcq.Spec.Sources, gcq.Status.Targets) return h.getOrCompileCurrentTargets(key, targets) } func customQuotaTargets( sources []capsulev1beta2.CustomQuotaSpecSource, statusTargets []capsulev1beta2.CustomQuotaStatusTarget, ) []capsulev1beta2.CustomQuotaStatusTarget { targets := make([]capsulev1beta2.CustomQuotaStatusTarget, 0, len(sources)) for i, source := range sources { scope := k8smeta.RESTScopeName("") if i < len(statusTargets) { scope = statusTargets[i].Scope } targets = append(targets, capsulev1beta2.CustomQuotaStatusTarget{ GroupVersionKind: metav1.GroupVersionKind(source.GroupVersionKind()), CustomQuotaSpecSourceConfig: source.CustomQuotaSpecSourceConfig, Scope: scope, }) } return targets } func (h *objectCalculationHandler) getOrCompileCurrentTargets( key string, targets []capsulev1beta2.CustomQuotaStatusTarget, ) ([]cache.CompiledTarget, error) { if compiled, ok := h.targetsCache.Get(key); ok && compiledTargetsCurrent(compiled, targets) { return compiled, nil } compiled, err := controller.CompileTargets(h.jsonPathCache, h.celCache, targets) if err != nil { return nil, err } h.targetsCache.Set(key, compiled) return compiled, nil } func compiledTargetsCurrent( compiled []cache.CompiledTarget, targets []capsulev1beta2.CustomQuotaStatusTarget, ) bool { if len(compiled) != len(targets) { return false } for i := range targets { if !reflect.DeepEqual(compiled[i].CustomQuotaStatusTarget, targets[i]) { return false } } return true } func desiredGlobalQuotaAppliesToNamespace( ctx context.Context, reader client.Reader, quota *capsulev1beta2.GlobalCustomQuota, namespace string, ) (bool, error) { if len(quota.Spec.NamespaceSelectors) == 0 { return true, nil } if namespace == "" { return false, nil } ns := &corev1.Namespace{} if err := reader.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil { return false, err } nsLabels := labels.Set(ns.Labels) for _, rawSelector := range quota.Spec.NamespaceSelectors { if rawSelector.LabelSelector == nil { continue } selector, err := metav1.LabelSelectorAsSelector(rawSelector.LabelSelector) if err != nil { return false, err } if selector.Matches(nsLabels) { return true, nil } } return false, nil } func sourcesTargetKind( sources []capsulev1beta2.CustomQuotaSpecSource, kind metav1.GroupVersionKind, ) bool { for _, source := range sources { target := source.GroupVersionKind() if target.Group == kind.Group && target.Version == kind.Version && target.Kind == kind.Kind { return true } } return false } func customQuotaReadyForAdmission( generation int64, status capsulev1beta2.CustomQuotaStatus, ) bool { return status.ObservedGeneration == generation && meta.IsStatusConditionTrue(status.Conditions, meta.ReadyCondition) } func evaluatedByKey(in []evaluatedQuota) map[string]evaluatedQuota { out := make(map[string]evaluatedQuota, len(in)) for _, item := range in { existing, ok := out[item.Key] if !ok { copyItem := item copyItem.Usage = item.Usage.DeepCopy() out[item.Key] = copyItem continue } existing.Usage.Add(item.Usage) quota.ClampQuantityToZero(&existing.Usage) out[item.Key] = existing } return out }