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

289 lines
7.9 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package rulestatus
import (
"context"
"fmt"
"reflect"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
k8smeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/events"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/cluster-api/util/patch"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
caperrors "github.com/projectcapsule/capsule/pkg/api/errors"
meta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
"github.com/projectcapsule/capsule/pkg/runtime/predicates"
)
type Manager struct {
client.Client
reader client.Reader
Metrics *metrics.RuleStatusRecorder
Log logr.Logger
Recorder events.EventRecorder
Configuration configuration.Configuration
RESTConfig *rest.Config
RESTMapper k8smeta.RESTMapper
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
r.reader = mgr.GetAPIReader()
r.RESTMapper = mgr.GetRESTMapper()
ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
Named("capsule/rule-status").
For(
&capsulev1beta2.RuleStatus{},
builder.WithPredicates(
predicate.Or(
predicate.GenerationChangedPredicate{},
predicates.UpdatedMetadataPredicate{},
),
),
).
WithOptions(ctrlConfig.Runtime.ToControllerOptions())
return ctrlBuilder.Complete(r)
}
func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ctrl.Result, err error) {
log := r.Log.WithValues("Request.Name", request.Name)
instance := &capsulev1beta2.RuleStatus{}
if err = r.Get(ctx, request.NamespacedName, instance); err != nil {
if apierrors.IsNotFound(err) {
log.V(5).Info("request object not found, could have been deleted after reconcile request")
r.Metrics.DeleteMetrics(request.Name, request.Namespace)
return reconcile.Result{}, nil
}
log.Error(err, "error reading the object")
return result, err
}
patchHelper, err := patch.NewHelper(instance, r.Client)
if err != nil {
return reconcile.Result{}, err
}
defer func() {
if e := r.updateStatus(ctx, instance, err); e != nil {
if apierrors.IsNotFound(err) || apierrors.HasStatusCause(err, corev1.NamespaceTerminatingCause) {
err = nil
return
}
err = fmt.Errorf("cannot update status: %w", e)
return
}
r.Metrics.RecordConditions(instance)
if e := patchHelper.Patch(ctx, instance); e != nil {
if apierrors.IsNotFound(e) || apierrors.HasStatusCause(e, corev1.NamespaceTerminatingCause) {
err = nil
return
}
err = fmt.Errorf("cannot patch: %w", e)
return
}
// Controller-Runtime should never receive error
err = nil
}()
// Best-Effort for Updating the status
if updateErr := r.updateReconcilingStatus(ctx, instance); updateErr != nil {
if caperrors.IgnoreGone(updateErr) {
return reconcile.Result{}, nil
}
log.Error(updateErr, "failed to update status")
}
// Reconcile
if err = r.reconcile(ctx, instance); err != nil {
err = fmt.Errorf("cannot collect available resources: %w", err)
return result, err
}
var reconcileError error
if err != nil {
reconcileError = fmt.Errorf("had errors reconciling")
}
log.V(4).Info("reconciling completed")
return ctrl.Result{}, reconcileError
}
func (r Manager) reconcile(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
previousRules := instance.Status.Rules
hadManagedMetadata := hasManagedMetadata(previousRules)
ruleStatus := make([]*rules.NamespaceRuleBodyNamespace, 0, len(instance.Spec))
for _, rule := range instance.Spec {
if rule == nil || rule.Enforce == nil {
continue
}
statusRule := rule.DeepCopy()
// RuleStatus is an enforcement cache. Quota definitions are reconciled
// independently as GlobalResourceQuotas and may include legacy entries
// which predate stable quota names.
statusRule.Quota = nil
enforce := rule.Enforce.DeepCopy()
for i := range enforce.Metadata {
enforce.Metadata[i].APIGroups = enforce.Metadata[i].StatusAPIGroups()
}
statusRule.Enforce = enforce
ruleStatus = append(ruleStatus, statusRule)
}
instance.Status.Rules = ruleStatus
//nolint:staticcheck
instance.Status.Rule = rules.NamespaceRuleBodyNamespace{}
if hadManagedMetadata || hasManagedMetadata(ruleStatus) {
if err := r.publishRulesStatus(ctx, instance); err != nil {
return fmt.Errorf("publish rules before managed metadata reconciliation: %w", err)
}
if err := r.reconcileManagedMetadata(ctx, instance, previousRules, ruleStatus); err != nil {
return fmt.Errorf("reconcile managed metadata: %w", err)
}
}
return nil
}
func (r *Manager) publishRulesStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
latest := &capsulev1beta2.RuleStatus{}
if err := r.reader.Get(ctx, client.ObjectKeyFromObject(instance), latest); err != nil {
return err
}
latest.Status.Rules = instance.Status.Rules
//nolint:staticcheck
latest.Status.Rule = instance.Status.Rule
return r.Client.Status().Update(ctx, latest)
})
}
func (r *Manager) updateStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus, reconcileError error) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
latest := &capsulev1beta2.RuleStatus{}
if err = r.reader.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return err
}
originalStatus := latest.Status.DeepCopy()
latest.Status = instance.Status
latest.Status.ObservedGeneration = instance.GetGeneration()
// Set Ready Condition
readyCondition := meta.NewReadyCondition(instance)
if reconcileError != nil {
readyCondition.Message = reconcileError.Error()
readyCondition.Status = metav1.ConditionFalse
readyCondition.Reason = meta.FailedReason
}
latest.Status.Conditions.UpdateConditionByType(readyCondition)
if reflect.DeepEqual(*originalStatus, latest.Status) {
return nil
}
if err := r.Client.Status().Update(ctx, latest); err != nil {
return err
}
// Keep the in-memory object aligned with what we just wrote.
instance.Status = latest.Status
return nil
})
}
func (r *Manager) updateReconcilingStatus(ctx context.Context, instance *capsulev1beta2.RuleStatus) error {
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
latest := &capsulev1beta2.RuleStatus{}
if err = r.reader.Get(ctx, types.NamespacedName{Name: instance.GetName(), Namespace: instance.GetNamespace()}, latest); err != nil {
return err
}
cleanedQuota := removeQuotaDefinitions(&latest.Status)
if latest.Status.ObservedGeneration == instance.GetGeneration() {
if !cleanedQuota {
return nil
}
return r.Status().Update(ctx, latest)
}
latest.Status.Conditions.UpdateConditionByType(meta.NewReadyConditionReconcilingReason(instance))
return r.Status().Update(ctx, latest)
})
}
//nolint:staticcheck // The deprecated flattened Rule must be cleaned for objects written by older Capsule versions.
func removeQuotaDefinitions(status *capsulev1beta2.RuleStatusStatus) bool {
if status == nil {
return false
}
changed := len(status.Rule.Quota) > 0
for _, rule := range status.Rules {
if rule != nil && len(rule.Quota) > 0 {
changed = true
rule.Quota = nil
}
}
status.Rule.Quota = nil
return changed
}