mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-20 13:06:51 +00:00
* feat: add globalresourcequota api Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
289 lines
7.9 KiB
Go
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
|
|
}
|