@@ -28,12 +28,15 @@ import (
2828 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2929 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3030 "k8s.io/apimachinery/pkg/runtime/schema"
31+ "k8s.io/apimachinery/pkg/util/sets"
3132 "k8s.io/klog/v2"
33+ "k8s.io/utils/ptr"
3234 ctrl "sigs.k8s.io/controller-runtime"
3335 "sigs.k8s.io/controller-runtime/pkg/client"
3436
3537 clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
3638 "sigs.k8s.io/cluster-api/api/core/v1beta2/index"
39+ "sigs.k8s.io/cluster-api/feature"
3740 "sigs.k8s.io/cluster-api/internal/controllers/machinedeployment/mdutil"
3841 "sigs.k8s.io/cluster-api/internal/util/taints"
3942 "sigs.k8s.io/cluster-api/util"
@@ -335,6 +338,15 @@ func (r *Reconciler) patchNode(ctx context.Context, remoteClient client.Client,
335338 // Drop the NodeUninitializedTaint taint on the node given that we are reconciling labels.
336339 hasTaintChanges := taints .RemoveNodeTaint (newNode , clusterv1 .NodeUninitializedTaint )
337340
341+ // Propagate taints set on the Machine to the Node.
342+ var propagateTaintsChanges bool
343+ if feature .Gates .Enabled (feature .MachineTaintPropagation ) {
344+ var err error
345+ if propagateTaintsChanges , err = propagateMachineTaintsToNode (newNode , m .Spec .Taints ); err != nil {
346+ return errors .Wrapf (err , "failed to propagate machine taints to node %s" , klog .KRef ("" , node .Name ))
347+ }
348+ }
349+
338350 // Set Taint to a node in an old MachineSet and unset Taint from a node in a new MachineSet
339351 isOutdated , notFound , err := shouldNodeHaveOutdatedTaint (ctx , r .Client , m )
340352 if err != nil {
@@ -352,11 +364,106 @@ func (r *Reconciler) patchNode(ctx context.Context, remoteClient client.Client,
352364 }
353365 }
354366
355- if ! hasAnnotationChanges && ! hasLabelChanges && ! hasTaintChanges {
367+ if ! hasAnnotationChanges && ! hasLabelChanges && ! hasTaintChanges && ! propagateTaintsChanges {
356368 return nil
357369 }
358370
359- return remoteClient .Patch (ctx , newNode , client .StrategicMergeFrom (node ))
371+ var mergeOptions []client.MergeFromOption
372+ if feature .Gates .Enabled (feature .MachineTaintPropagation ) {
373+ mergeOptions = append (mergeOptions , client.MergeFromWithOptimisticLock {})
374+ }
375+
376+ return remoteClient .Patch (ctx , newNode , client .StrategicMergeFrom (node , mergeOptions ... ))
377+ }
378+
379+ // propagateMachineTaintsToNode handles propagation of taints defined on a machine to a node.
380+ // It makes use of the annotation clusterv1.TaintsFromMachineAnnotation to track which taints are owned by the controller.
381+ // OnInitialization taints are only added to the node if the tracking annotation has not been set yet.
382+ func propagateMachineTaintsToNode (node * corev1.Node , machineTaints []clusterv1.MachineTaint ) (bool , error ) {
383+ changed := false
384+
385+ // Get the value of the tracking annotation. If it is not set at all we also have to add the OnInitialization taints.
386+ oldTaintsAnnotation , nodeTaintsInitialized := node .Annotations [clusterv1 .TaintsFromMachineAnnotation ]
387+
388+ // ownedTaints contains all Always taints that the controller is owning.
389+ ownedTaints := unmarshalMachineTaintsAnnotation (oldTaintsAnnotation )
390+
391+ // newOwnedTaints will contain all Always taints from the current machine's spec.
392+ newOwnedTaints := sets .New [string ]()
393+ onInitializationTaints := sets .New [string ]()
394+
395+ for _ , taint := range machineTaints {
396+ // Collect Always and OnInitialization taints to identify taints to delete.
397+ // Separating Always taints so the tracking annotation can be updated accordingly.
398+ switch taint .Propagation {
399+ case clusterv1 .TaintPropagationAlways :
400+ newOwnedTaints .Insert (fmt .Sprintf ("%s:%s" , taint .Key , taint .Effect ))
401+ case clusterv1 .TaintPropagationOnInitialization :
402+ onInitializationTaints .Insert (fmt .Sprintf ("%s:%s" , taint .Key , taint .Effect ))
403+ }
404+
405+ // Only add OnInitialization taints if the tracking annotation has not been set yet.
406+ if taint .Propagation == clusterv1 .TaintPropagationOnInitialization && nodeTaintsInitialized {
407+ continue
408+ }
409+
410+ // Ensure the taint is set on the node and has the correct value.
411+ if changedTaints := taints .EnsureNodeTaint (node , convertMachineTaintToCoreV1Taint (taint )); changedTaints {
412+ changed = true
413+ }
414+ }
415+
416+ // Calculate ownedTaints - newOwnedTaints to identify old taints which need to be deleted from the node.
417+ taintsToDelete := ownedTaints .Difference (newOwnedTaints ).Difference (onInitializationTaints )
418+
419+ // Remove all identified taints from the node.
420+ for taintToDelete := range taintsToDelete {
421+ splitted := strings .Split (taintToDelete , ":" )
422+ if len (splitted ) != 2 {
423+ return changed , fmt .Errorf ("invalid taint format: %q" , taintToDelete )
424+ }
425+
426+ if removedTaint := taints .RemoveNodeTaint (node , corev1.Taint {Key : splitted [0 ], Effect : corev1 .TaintEffect (splitted [1 ])}); removedTaint {
427+ changed = true
428+ }
429+ }
430+
431+ // Update the tracking annotation with newOwnedTaints..
432+ if newTaintsAnnotation := marshalMachineTaintsAnnotation (newOwnedTaints ); ! nodeTaintsInitialized || newTaintsAnnotation != oldTaintsAnnotation {
433+ if node .Annotations == nil {
434+ node .Annotations = map [string ]string {}
435+ }
436+ node .Annotations [clusterv1 .TaintsFromMachineAnnotation ] = newTaintsAnnotation
437+ changed = true
438+ }
439+
440+ return changed , nil
441+ }
442+
443+ // marshalMachineTaintsAnnotation marshals the tracking annotation value.
444+ func marshalMachineTaintsAnnotation (ownedTaints sets.Set [string ]) string {
445+ taints := ownedTaints .UnsortedList ()
446+ slices .Sort (taints )
447+
448+ return strings .Join (taints , "," )
449+ }
450+
451+ // unmarshalMachineTaintsAnnotation unmarshals the tracking annotation value.
452+ func unmarshalMachineTaintsAnnotation (annotationValue string ) sets.Set [string ] {
453+ if annotationValue == "" {
454+ return nil
455+ }
456+
457+ return sets .New (strings .Split (annotationValue , "," )... )
458+ }
459+
460+ // convertMachineTaintToCoreV1Taint converts a MachineTaint to a corev1.Taint.
461+ func convertMachineTaintToCoreV1Taint (machineTaint clusterv1.MachineTaint ) corev1.Taint {
462+ return corev1.Taint {
463+ Key : machineTaint .Key ,
464+ Value : ptr .Deref (machineTaint .Value , "" ),
465+ Effect : machineTaint .Effect ,
466+ }
360467}
361468
362469// shouldNodeHaveOutdatedTaint tries to compare the revision of the owning MachineSet to the MachineDeployment.
0 commit comments