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