pkg/controllers/clusterresourceplacementeviction/controller.go (304 lines of code) (raw):

/* Copyright 2025 The KubeFleet Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package clusterresourceplacementeviction import ( "context" "fmt" "time" "github.com/prometheus/client_golang/prometheus" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/klog/v2" "k8s.io/utils/ptr" runtime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrl "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" bindingutils "go.goms.io/fleet/pkg/utils/binding" "go.goms.io/fleet/pkg/utils/condition" "go.goms.io/fleet/pkg/utils/controller" "go.goms.io/fleet/pkg/utils/controller/metrics" "go.goms.io/fleet/pkg/utils/defaulter" evictionutils "go.goms.io/fleet/pkg/utils/eviction" ) // Reconciler reconciles a ClusterResourcePlacementEviction object. type Reconciler struct { client.Client // UncachedReader is only used to read disruption budget objects directly from the API server to ensure we can enforce the disruption budget for eviction. UncachedReader client.Reader } // Reconcile triggers a single eviction reconcile round. func (r *Reconciler) Reconcile(ctx context.Context, req runtime.Request) (runtime.Result, error) { startTime := time.Now() evictionName := req.NamespacedName.Name klog.V(2).InfoS("ClusterResourcePlacementEviction reconciliation starts", "clusterResourcePlacementEviction", evictionName) var internalError bool defer func() { if internalError { metrics.FleetEvictionStatus.WithLabelValues(evictionName, "false", "unknown").SetToCurrentTime() } latency := time.Since(startTime).Milliseconds() klog.V(2).InfoS("ClusterResourcePlacementEviction reconciliation ends", "clusterResourcePlacementEviction", evictionName, "latency", latency) }() var eviction placementv1beta1.ClusterResourcePlacementEviction if err := r.Client.Get(ctx, req.NamespacedName, &eviction); err != nil { internalError = true klog.ErrorS(err, "Failed to get cluster resource placement eviction", "clusterResourcePlacementEviction", evictionName) return runtime.Result{}, client.IgnoreNotFound(err) } if evictionutils.IsEvictionInTerminalState(&eviction) { return runtime.Result{}, nil } validationResult, err := r.validateEviction(ctx, &eviction) if err != nil { internalError = true return runtime.Result{}, err } if !validationResult.isValid { if err = r.updateEvictionStatus(ctx, &eviction); err != nil { internalError = true return runtime.Result{}, err } emitEvictionCompleteMetric(&eviction) return runtime.Result{}, nil } markEvictionValid(&eviction) if err = r.executeEviction(ctx, validationResult, &eviction); err != nil { internalError = true return runtime.Result{}, err } if err = r.updateEvictionStatus(ctx, &eviction); err != nil { internalError = true return runtime.Result{}, err } emitEvictionCompleteMetric(&eviction) return runtime.Result{}, nil } // validateEviction performs validation for eviction object's spec and returns a wrapped validation result. func (r *Reconciler) validateEviction(ctx context.Context, eviction *placementv1beta1.ClusterResourcePlacementEviction) (*evictionValidationResult, error) { validationResult := &evictionValidationResult{isValid: false} var crp placementv1beta1.ClusterResourcePlacement if err := r.Client.Get(ctx, types.NamespacedName{Name: eviction.Spec.PlacementName}, &crp); err != nil { if k8serrors.IsNotFound(err) { klog.V(2).InfoS(condition.EvictionInvalidMissingCRPMessage, "clusterResourcePlacementEviction", eviction.Name, "clusterResourcePlacement", eviction.Spec.PlacementName) markEvictionInvalid(eviction, condition.EvictionInvalidMissingCRPMessage) return validationResult, nil } return nil, controller.NewAPIServerError(true, err) } // set default values for CRP. defaulter.SetDefaultsClusterResourcePlacement(&crp) if crp.DeletionTimestamp != nil { klog.V(2).InfoS(condition.EvictionInvalidDeletingCRPMessage, "clusterResourcePlacementEviction", eviction.Name, "clusterResourcePlacement", eviction.Spec.PlacementName) markEvictionInvalid(eviction, condition.EvictionInvalidDeletingCRPMessage) return validationResult, nil } if crp.Spec.Policy.PlacementType == placementv1beta1.PickFixedPlacementType { klog.V(2).InfoS(condition.EvictionInvalidPickFixedCRPMessage, "clusterResourcePlacementEviction", eviction.Name, "clusterResourcePlacement", eviction.Spec.PlacementName) markEvictionInvalid(eviction, condition.EvictionInvalidPickFixedCRPMessage) return validationResult, nil } validationResult.crp = &crp var crbList placementv1beta1.ClusterResourceBindingList if err := r.Client.List(ctx, &crbList, client.MatchingLabels{placementv1beta1.CRPTrackingLabel: crp.Name}); err != nil { return nil, controller.NewAPIServerError(true, err) } validationResult.bindings = crbList.Items var evictionTargetBinding *placementv1beta1.ClusterResourceBinding for i := range crbList.Items { if crbList.Items[i].Spec.TargetCluster == eviction.Spec.ClusterName { if evictionTargetBinding == nil { evictionTargetBinding = &crbList.Items[i] } else { klog.V(2).InfoS(condition.EvictionInvalidMultipleCRBMessage, "clusterResourcePlacementEviction", eviction.Name, "clusterResourcePlacement", eviction.Spec.PlacementName) markEvictionInvalid(eviction, condition.EvictionInvalidMultipleCRBMessage) return validationResult, nil } } } if evictionTargetBinding == nil { klog.V(2).InfoS("Failed to find cluster resource binding for cluster targeted by eviction", "clusterResourcePlacementEviction", eviction.Name, "targetCluster", eviction.Spec.ClusterName) markEvictionInvalid(eviction, condition.EvictionInvalidMissingCRBMessage) return validationResult, nil } validationResult.crb = evictionTargetBinding validationResult.isValid = true return validationResult, nil } // updateEvictionStatus updates eviction status. func (r *Reconciler) updateEvictionStatus(ctx context.Context, eviction *placementv1beta1.ClusterResourcePlacementEviction) error { evictionRef := klog.KObj(eviction) if err := r.Client.Status().Update(ctx, eviction); err != nil { klog.ErrorS(err, "Failed to update eviction status", "clusterResourcePlacementEviction", evictionRef) return controller.NewUpdateIgnoreConflictError(err) } klog.V(2).InfoS("Updated the status of a eviction", "clusterResourcePlacementEviction", evictionRef) return nil } // deleteClusterResourceBinding deletes the specified cluster resource binding. func (r *Reconciler) deleteClusterResourceBinding(ctx context.Context, binding *placementv1beta1.ClusterResourceBinding) error { bindingRef := klog.KObj(binding) deleteOptions := &client.DeleteOptions{ Preconditions: &metav1.Preconditions{ ResourceVersion: ptr.To(binding.ResourceVersion), }, } if err := r.Client.Delete(ctx, binding, deleteOptions); err != nil { klog.ErrorS(err, "Failed to delete cluster resource binding", "clusterResourceBinding", bindingRef) return controller.NewDeleteIgnoreNotFoundError(err) } klog.V(2).InfoS("Issued delete on cluster resource binding, eviction succeeded", "clusterResourceBinding", bindingRef) return nil } // executeEviction tries to remove resources from target cluster placed by placement targeted by eviction. func (r *Reconciler) executeEviction(ctx context.Context, validationResult *evictionValidationResult, eviction *placementv1beta1.ClusterResourcePlacementEviction) error { // Unwrap validation result for processing. crp, evictionTargetBinding, bindingList := validationResult.crp, validationResult.crb, validationResult.bindings // Check to see if binding is being deleted. if evictionTargetBinding.GetDeletionTimestamp() != nil { klog.V(2).InfoS("ClusterResourceBinding targeted by eviction is being deleted", "clusterResourcePlacementEviction", eviction.Name, "clusterResourceBinding", evictionTargetBinding.Name, "targetCluster", eviction.Spec.ClusterName) markEvictionExecuted(eviction, condition.EvictionAllowedPlacementRemovedMessage) return nil } if !evictionutils.IsPlacementPresent(evictionTargetBinding) { klog.V(2).InfoS("No resources have been placed for ClusterResourceBinding in target cluster", "clusterResourcePlacementEviction", eviction.Name, "clusterResourceBinding", evictionTargetBinding.Name, "targetCluster", eviction.Spec.ClusterName) markEvictionNotExecuted(eviction, condition.EvictionBlockedMissingPlacementMessage) return nil } // Check to see if binding has failed or just reportDiff. If so no need to check disruption budget we can evict. if bindingutils.HasBindingFailed(evictionTargetBinding) || bindingutils.IsBindingDiffReported(evictionTargetBinding) { klog.V(2).InfoS("ClusterResourceBinding targeted by eviction is in failed state", "clusterResourcePlacementEviction", eviction.Name, "clusterResourceBinding", evictionTargetBinding.Name, "targetCluster", eviction.Spec.ClusterName) if err := r.deleteClusterResourceBinding(ctx, evictionTargetBinding); err != nil { return err } markEvictionExecuted(eviction, condition.EvictionAllowedPlacementFailedMessage) return nil } var db placementv1beta1.ClusterResourcePlacementDisruptionBudget if err := r.UncachedReader.Get(ctx, types.NamespacedName{Name: crp.Name}, &db); err != nil { if k8serrors.IsNotFound(err) { if err = r.deleteClusterResourceBinding(ctx, evictionTargetBinding); err != nil { return err } markEvictionExecuted(eviction, condition.EvictionAllowedNoPDBMessage) return nil } return controller.NewAPIServerError(true, err) } // handle special case for PickAll CRP. if crp.Spec.Policy.PlacementType == placementv1beta1.PickAllPlacementType { if db.Spec.MaxUnavailable != nil || (db.Spec.MinAvailable != nil && db.Spec.MinAvailable.Type == intstr.String) { markEvictionNotExecuted(eviction, condition.EvictionBlockedMisconfiguredPDBSpecifiedMessage) return nil } } totalBindings := len(bindingList) allowed, availableBindings := isEvictionAllowed(bindingList, *crp, db) if allowed { if err := r.deleteClusterResourceBinding(ctx, evictionTargetBinding); err != nil { return err } markEvictionExecuted(eviction, fmt.Sprintf(condition.EvictionAllowedPDBSpecifiedMessageFmt, availableBindings, totalBindings)) } else { markEvictionNotExecuted(eviction, fmt.Sprintf(condition.EvictionBlockedPDBSpecifiedMessageFmt, availableBindings, totalBindings)) } return nil } // isEvictionAllowed calculates if eviction allowed based on available bindings and spec specified in placement disruption budget. func isEvictionAllowed(bindings []placementv1beta1.ClusterResourceBinding, crp placementv1beta1.ClusterResourcePlacement, db placementv1beta1.ClusterResourcePlacementDisruptionBudget) (bool, int) { availableBindings := 0 for i := range bindings { availableCondition := bindings[i].GetCondition(string(placementv1beta1.ResourceBindingAvailable)) if condition.IsConditionStatusTrue(availableCondition, bindings[i].GetGeneration()) { availableBindings++ } } var desiredBindings int placementType := crp.Spec.Policy.PlacementType // we don't know the desired bindings for PickAll and we won't evict a binding for PickFixed CRP. if placementType == placementv1beta1.PickNPlacementType { desiredBindings = int(*crp.Spec.Policy.NumberOfClusters) } var disruptionsAllowed int switch { // For PickAll CRPs, MaxUnavailable won't be specified in DB. case db.Spec.MaxUnavailable != nil: maxUnavailable, _ := intstr.GetScaledValueFromIntOrPercent(db.Spec.MaxUnavailable, desiredBindings, true) unavailableBindings := len(bindings) - availableBindings disruptionsAllowed = maxUnavailable - unavailableBindings case db.Spec.MinAvailable != nil: var minAvailable int if placementType == placementv1beta1.PickAllPlacementType { // MinAvailable will be an Integer value for PickAll CRP. minAvailable = db.Spec.MinAvailable.IntValue() } else { minAvailable, _ = intstr.GetScaledValueFromIntOrPercent(db.Spec.MinAvailable, desiredBindings, true) } disruptionsAllowed = availableBindings - minAvailable } if disruptionsAllowed < 0 { disruptionsAllowed = 0 } return disruptionsAllowed > 0, availableBindings } // markEvictionValid sets the valid condition as true in eviction status. func markEvictionValid(eviction *placementv1beta1.ClusterResourcePlacementEviction) { cond := metav1.Condition{ Type: string(placementv1beta1.PlacementEvictionConditionTypeValid), Status: metav1.ConditionTrue, ObservedGeneration: eviction.Generation, Reason: condition.ClusterResourcePlacementEvictionValidReason, Message: condition.EvictionValidMessage, } eviction.SetConditions(cond) klog.V(2).InfoS("Marked eviction as valid", "clusterResourcePlacementEviction", klog.KObj(eviction)) } // markEvictionInvalid sets the valid condition as false in eviction status. func markEvictionInvalid(eviction *placementv1beta1.ClusterResourcePlacementEviction, message string) { cond := metav1.Condition{ Type: string(placementv1beta1.PlacementEvictionConditionTypeValid), Status: metav1.ConditionFalse, ObservedGeneration: eviction.Generation, Reason: condition.ClusterResourcePlacementEvictionInvalidReason, Message: message, } eviction.SetConditions(cond) klog.V(2).InfoS("Marked eviction as invalid", "clusterResourcePlacementEviction", klog.KObj(eviction)) } // markEvictionExecuted sets the executed condition as true in eviction status. func markEvictionExecuted(eviction *placementv1beta1.ClusterResourcePlacementEviction, message string) { cond := metav1.Condition{ Type: string(placementv1beta1.PlacementEvictionConditionTypeExecuted), Status: metav1.ConditionTrue, ObservedGeneration: eviction.Generation, Reason: condition.ClusterResourcePlacementEvictionExecutedReason, Message: message, } eviction.SetConditions(cond) klog.V(2).InfoS("Marked eviction as executed", "clusterResourcePlacementEviction", klog.KObj(eviction)) } // markEvictionNotExecuted sets the executed condition as false in eviction status. func markEvictionNotExecuted(eviction *placementv1beta1.ClusterResourcePlacementEviction, message string) { cond := metav1.Condition{ Type: string(placementv1beta1.PlacementEvictionConditionTypeExecuted), Status: metav1.ConditionFalse, ObservedGeneration: eviction.Generation, Reason: condition.ClusterResourcePlacementEvictionNotExecutedReason, Message: message, } eviction.SetConditions(cond) klog.V(2).InfoS("Marked eviction as not executed", "clusterResourcePlacementEviction", klog.KObj(eviction)) } func emitEvictionCompleteMetric(eviction *placementv1beta1.ClusterResourcePlacementEviction) { metrics.FleetEvictionStatus.DeletePartialMatch(prometheus.Labels{"name": eviction.GetName(), "isCompleted": "false"}) // check to see if eviction is valid. if condition.IsConditionStatusTrue(eviction.GetCondition(string(placementv1beta1.PlacementEvictionConditionTypeValid)), eviction.GetGeneration()) { metrics.FleetEvictionStatus.WithLabelValues(eviction.Name, "true", "true").SetToCurrentTime() } else { metrics.FleetEvictionStatus.WithLabelValues(eviction.Name, "true", "false").SetToCurrentTime() } } // SetupWithManager sets up the controller with the Manager. func (r *Reconciler) SetupWithManager(mgr runtime.Manager) error { return runtime.NewControllerManagedBy(mgr).Named("clusterresourceplacementeviction-controller"). WithOptions(ctrl.Options{MaxConcurrentReconciles: 1}). // max concurrent reconciles is currently set to 1 for concurrency control. For(&placementv1beta1.ClusterResourcePlacementEviction{}). WithEventFilter(predicate.Funcs{ DeleteFunc: func(e event.DeleteEvent) bool { // delete complete status metric for eviction and skip reconciliation. count := metrics.FleetEvictionStatus.DeletePartialMatch(prometheus.Labels{"name": e.Object.GetName()}) klog.V(2).InfoS("ClusterResourcePlacementEviction is being deleted", "clusterResourcePlacementEviction", e.Object.GetName(), "metricCount", count) return false }, }). WithEventFilter(predicate.GenerationChangedPredicate{}). Complete(r) } type evictionValidationResult struct { crp *placementv1beta1.ClusterResourcePlacement crb *placementv1beta1.ClusterResourceBinding bindings []placementv1beta1.ClusterResourceBinding isValid bool }