@@ -33,9 +33,13 @@ import (
3333 v1 "k8s.io/client-go/applyconfigurations/meta/v1"
3434 policyv1ac "k8s.io/client-go/applyconfigurations/policy/v1"
3535 ctrl "sigs.k8s.io/controller-runtime"
36+ "sigs.k8s.io/controller-runtime/pkg/builder"
3637 k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
3738 "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
39+ "sigs.k8s.io/controller-runtime/pkg/event"
40+ "sigs.k8s.io/controller-runtime/pkg/handler"
3841 logger "sigs.k8s.io/controller-runtime/pkg/log"
42+ "sigs.k8s.io/controller-runtime/pkg/predicate"
3943
4044 kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
4145)
@@ -85,6 +89,19 @@ func (r *GardenerNodeLifecycleController) Reconcile(ctx context.Context, req ctr
8589 return ctrl.Result {}, nil
8690 }
8791
92+ // Apply the offboarding taint once VMs are gone; gate on Evicting=False
93+ // to avoid racing with live-migration.
94+ if hv .Spec .Maintenance == kvmv1 .MaintenanceTermination &&
95+ meta .IsStatusConditionFalse (hv .Status .Conditions , kvmv1 .ConditionTypeEvicting ) {
96+ patched , err := r .ensureOffboardingTaint (ctx , node )
97+ if err != nil {
98+ return ctrl.Result {}, fmt .Errorf ("failed to ensure offboarding taint: %w" , err )
99+ }
100+ if patched {
101+ return ctrl.Result {}, nil
102+ }
103+ }
104+
88105 // We do not care about the particular value, as long as it isn't an error
89106 var minAvailable int32 = 1
90107
@@ -115,6 +132,30 @@ func (r *GardenerNodeLifecycleController) Reconcile(ctx context.Context, req ctr
115132 return ctrl.Result {}, nil
116133}
117134
135+ // ensureOffboardingTaint adds the offboarding NoExecute taint if not already
136+ // present. Returns true when a patch was issued (caller should return early).
137+ func (r * GardenerNodeLifecycleController ) ensureOffboardingTaint (ctx context.Context , node * corev1.Node ) (bool , error ) {
138+ for _ , t := range node .Spec .Taints {
139+ if t .Key == taintKeyOffboarding && t .Effect == corev1 .TaintEffectNoExecute {
140+ return false , nil
141+ }
142+ }
143+
144+ log := logger .FromContext (ctx )
145+ log .Info ("Adding offboarding taint to node" ,
146+ "node" , node .Name ,
147+ "taint" , taintKeyOffboarding ,
148+ "effect" , corev1 .TaintEffectNoExecute )
149+
150+ // StrategicMergeFrom merges taints by key, preserving concurrent additions.
151+ patch := k8sclient .StrategicMergeFrom (node .DeepCopy ())
152+ node .Spec .Taints = append (node .Spec .Taints , corev1.Taint {
153+ Key : taintKeyOffboarding ,
154+ Effect : corev1 .TaintEffectNoExecute ,
155+ })
156+ return true , r .Patch (ctx , node , patch , k8sclient .FieldOwner (MaintenanceControllerName ))
157+ }
158+
118159func (r * GardenerNodeLifecycleController ) ensureBlockingPodDisruptionBudget (ctx context.Context , node * corev1.Node , minAvailable int32 ) error {
119160 name := nameForNode (node )
120161 nodeLabels := labelsForNode (node )
@@ -226,10 +267,48 @@ func (r *GardenerNodeLifecycleController) SetupWithManager(mgr ctrl.Manager, nam
226267 _ = logger .FromContext (ctx )
227268 r .namespace = namespace
228269
270+ hypervisorToNode := handler .EnqueueRequestForOwner (mgr .GetScheme (), mgr .GetRESTMapper (), & corev1.Node {})
271+
272+ // Maintenance=termination bumps generation; Evicting status changes do not.
273+ hypervisorRelevantChange := predicate .Or (
274+ predicate.GenerationChangedPredicate {},
275+ evictingConditionChangedPredicate {},
276+ )
277+
229278 return ctrl .NewControllerManagedBy (mgr ).
230279 Named (MaintenanceControllerName ).
231280 For (& corev1.Node {}).
232- Owns (& appsv1.Deployment {}). // trigger the r.Reconcile whenever an Own-ed deployment is created/updated/deleted
281+ Watches (& kvmv1.Hypervisor {}, hypervisorToNode ,
282+ builder .WithPredicates (hypervisorRelevantChange ),
283+ ).
284+ Owns (& appsv1.Deployment {}).
233285 Owns (& policyv1.PodDisruptionBudget {}).
234286 Complete (r )
235287}
288+
289+ // evictingConditionChangedPredicate complements GenerationChangedPredicate,
290+ // which ignores status-only updates.
291+ type evictingConditionChangedPredicate struct {
292+ predicate.Funcs
293+ }
294+
295+ func (evictingConditionChangedPredicate ) Update (e event.UpdateEvent ) bool {
296+ if e .ObjectOld == nil || e .ObjectNew == nil {
297+ return false
298+ }
299+ oldHv , ok1 := e .ObjectOld .(* kvmv1.Hypervisor )
300+ newHv , ok2 := e .ObjectNew .(* kvmv1.Hypervisor )
301+ if ! ok1 || ! ok2 {
302+ return false
303+ }
304+ oldCond := meta .FindStatusCondition (oldHv .Status .Conditions , kvmv1 .ConditionTypeEvicting )
305+ newCond := meta .FindStatusCondition (newHv .Status .Conditions , kvmv1 .ConditionTypeEvicting )
306+ switch {
307+ case oldCond == nil && newCond == nil :
308+ return false
309+ case oldCond == nil || newCond == nil :
310+ return true
311+ default :
312+ return oldCond .Status != newCond .Status
313+ }
314+ }
0 commit comments