Skip to content

Commit 1c15a20

Browse files
authored
Merge pull request #7993 from ykakarap/node-label-sync-taint
⚠️ apply `node.cluster.x-k8s.io/uninitialized` during machine creation
2 parents 4350c44 + a142ca0 commit 1c15a20

File tree

10 files changed

+411
-5
lines changed

10 files changed

+411
-5
lines changed

api/v1beta1/common_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ const (
146146
VariableDefinitionFromInline = "inline"
147147
)
148148

149+
// NodeUninitializedTaint can be added to Nodes at creation by the bootstrap provider, e.g. the
150+
// KubeadmBootstrap provider will add the taint.
151+
// This taint is used to prevent workloads to be scheduled on Nodes before the node is initialized by Cluster API.
152+
// As of today the Node initialization consists of syncing labels from Machines to Nodes. Once the labels
153+
// have been initially synced the taint is removed from the Node.
154+
var NodeUninitializedTaint = corev1.Taint{
155+
Key: "node.cluster.x-k8s.io/uninitialized",
156+
Effect: corev1.TaintEffectNoSchedule,
157+
}
158+
149159
const (
150160
// TemplateSuffix is the object kind suffix used by template types.
151161
TemplateSuffix = "Template"

bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ const (
6363
KubeadmConfigControllerName = "kubeadmconfig-controller"
6464
)
6565

66+
var (
67+
// controlPlaneTaint is the taint that kubeadm applies to the control plane nodes
68+
// for Kubernetes version >= v1.24.0.
69+
// The values are copied from kubeadm codebase.
70+
controlPlaneTaint = corev1.Taint{
71+
Key: "node-role.kubernetes.io/control-plane",
72+
Effect: corev1.TaintEffectNoSchedule,
73+
}
74+
75+
// oldControlPlaneTaint is the taint that kubeadm applies to the control plane nodes
76+
// for Kubernetes version < v1.25.0.
77+
// The values are copied from kubeadm codebase.
78+
oldControlPlaneTaint = corev1.Taint{
79+
Key: "node-role.kubernetes.io/master",
80+
Effect: corev1.TaintEffectNoSchedule,
81+
}
82+
)
83+
6684
const (
6785
// DefaultTokenTTL is the default TTL used for tokens.
6886
DefaultTokenTTL = 15 * time.Minute
@@ -415,7 +433,15 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex
415433
},
416434
}
417435
}
418-
initdata, err := kubeadmtypes.MarshalInitConfigurationForVersion(scope.Config.Spec.InitConfiguration, parsedVersion)
436+
437+
// Add the node uninitialized taint to the list of taints.
438+
// DeepCopy the InitConfiguration to prevent updating the actual KubeadmConfig.
439+
// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
440+
// is initialized by ClusterAPI.
441+
initConfiguration := scope.Config.Spec.InitConfiguration.DeepCopy()
442+
addNodeUninitializedTaint(&initConfiguration.NodeRegistration, true, parsedVersion)
443+
444+
initdata, err := kubeadmtypes.MarshalInitConfigurationForVersion(initConfiguration, parsedVersion)
419445
if err != nil {
420446
scope.Error(err, "Failed to marshal init configuration")
421447
return ctrl.Result{}, err
@@ -551,7 +577,14 @@ func (r *KubeadmConfigReconciler) joinWorker(ctx context.Context, scope *Scope)
551577
return ctrl.Result{}, errors.Wrapf(err, "failed to parse kubernetes version %q", kubernetesVersion)
552578
}
553579

554-
joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(scope.Config.Spec.JoinConfiguration, parsedVersion)
580+
// Add the node uninitialized taint to the list of taints.
581+
// DeepCopy the JoinConfiguration to prevent updating the actual KubeadmConfig.
582+
// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
583+
// is initialized by ClusterAPI.
584+
joinConfiguration := scope.Config.Spec.JoinConfiguration.DeepCopy()
585+
addNodeUninitializedTaint(&joinConfiguration.NodeRegistration, false, parsedVersion)
586+
587+
joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(joinConfiguration, parsedVersion)
555588
if err != nil {
556589
scope.Error(err, "Failed to marshal join configuration")
557590
return ctrl.Result{}, err
@@ -657,7 +690,14 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S
657690
return ctrl.Result{}, errors.Wrapf(err, "failed to parse kubernetes version %q", kubernetesVersion)
658691
}
659692

660-
joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(scope.Config.Spec.JoinConfiguration, parsedVersion)
693+
// Add the node uninitialized taint to the list of taints.
694+
// DeepCopy the JoinConfiguration to prevent updating the actual KubeadmConfig.
695+
// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
696+
// is initialized by ClusterAPI.
697+
joinConfiguration := scope.Config.Spec.JoinConfiguration.DeepCopy()
698+
addNodeUninitializedTaint(&joinConfiguration.NodeRegistration, true, parsedVersion)
699+
700+
joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(joinConfiguration, parsedVersion)
661701
if err != nil {
662702
scope.Error(err, "Failed to marshal join configuration")
663703
return ctrl.Result{}, err
@@ -1066,3 +1106,43 @@ func (r *KubeadmConfigReconciler) ensureBootstrapSecretOwnersRef(ctx context.Con
10661106
}
10671107
return nil
10681108
}
1109+
1110+
// addNodeUninitializedTaint adds the NodeUninitializedTaint to the nodeRegistration.
1111+
// Note: If isControlPlane is true then it also adds the control plane taint if the initial set of taints is nil.
1112+
// This is to ensure consistency with kubeadm's defaulting behavior.
1113+
func addNodeUninitializedTaint(nodeRegistration *bootstrapv1.NodeRegistrationOptions, isControlPlane bool, kubernetesVersion semver.Version) {
1114+
var taints []corev1.Taint
1115+
taints = nodeRegistration.Taints
1116+
if hasTaint(taints, clusterv1.NodeUninitializedTaint) {
1117+
return
1118+
}
1119+
1120+
// For a control plane, kubeadm adds the default control plane taint if the provided taints are nil.
1121+
// Since we are adding the uninitialized taint we also have to add the default taints kubeadm would have added if
1122+
// the taints were nil.
1123+
if isControlPlane && taints == nil {
1124+
// Note: Kubeadm uses a different default control plane taint depending on the kubernetes version.
1125+
// Ref: https://github.com/kubernetes/kubeadm/issues/2200
1126+
if kubernetesVersion.LT(semver.MustParse("1.24.0")) {
1127+
taints = []corev1.Taint{oldControlPlaneTaint}
1128+
} else if kubernetesVersion.GTE(semver.MustParse("1.24.0")) && kubernetesVersion.LT(semver.MustParse("1.25.0")) {
1129+
taints = []corev1.Taint{
1130+
oldControlPlaneTaint,
1131+
controlPlaneTaint,
1132+
}
1133+
} else {
1134+
taints = []corev1.Taint{controlPlaneTaint}
1135+
}
1136+
}
1137+
taints = append(taints, clusterv1.NodeUninitializedTaint)
1138+
nodeRegistration.Taints = taints
1139+
}
1140+
1141+
func hasTaint(taints []corev1.Taint, targetTaint corev1.Taint) bool {
1142+
for _, taint := range taints {
1143+
if taint.MatchTaint(&targetTaint) {
1144+
return true
1145+
}
1146+
}
1147+
return false
1148+
}

bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"testing"
2525
"time"
2626

27+
"github.com/blang/semver"
2728
ignition "github.com/flatcar/ignition/config/v2_3"
2829
. "github.com/onsi/gomega"
2930
corev1 "k8s.io/api/core/v1"
@@ -2193,6 +2194,112 @@ func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) {
21932194
}
21942195
}
21952196

2197+
func TestAddNodeUninitializedTaint(t *testing.T) {
2198+
dummyTaint := corev1.Taint{
2199+
Key: "dummy-taint",
2200+
Value: "",
2201+
Effect: corev1.TaintEffectNoSchedule,
2202+
}
2203+
2204+
tests := []struct {
2205+
name string
2206+
nodeRegistration *bootstrapv1.NodeRegistrationOptions
2207+
kubernetesVersion semver.Version
2208+
isControlPlane bool
2209+
wantTaints []corev1.Taint
2210+
}{
2211+
{
2212+
name: "for control plane with version < v1.24.0, if taints is nil it should add the uninitialized and the master taint",
2213+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2214+
Taints: nil,
2215+
},
2216+
kubernetesVersion: semver.MustParse("1.23.0"),
2217+
isControlPlane: true,
2218+
wantTaints: []corev1.Taint{
2219+
oldControlPlaneTaint,
2220+
clusterv1.NodeUninitializedTaint,
2221+
},
2222+
},
2223+
{
2224+
name: "for control plane with version >= v1.24.0 and < v1.25.0, if taints is nil it should add the uninitialized, control-plane and the master taints",
2225+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2226+
Taints: nil,
2227+
},
2228+
kubernetesVersion: semver.MustParse("1.24.5"),
2229+
isControlPlane: true,
2230+
wantTaints: []corev1.Taint{
2231+
oldControlPlaneTaint,
2232+
controlPlaneTaint,
2233+
clusterv1.NodeUninitializedTaint,
2234+
},
2235+
},
2236+
{
2237+
name: "for control plane with version >= v1.25.0, if taints is nil it should add the uninitialized and the control-plane taint",
2238+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2239+
Taints: nil,
2240+
},
2241+
kubernetesVersion: semver.MustParse("1.25.0"),
2242+
isControlPlane: true,
2243+
wantTaints: []corev1.Taint{
2244+
controlPlaneTaint,
2245+
clusterv1.NodeUninitializedTaint,
2246+
},
2247+
},
2248+
{
2249+
name: "for control plane, if taints is not nil it should only add the uninitialized taint",
2250+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2251+
Taints: []corev1.Taint{},
2252+
},
2253+
kubernetesVersion: semver.MustParse("1.26.0"),
2254+
isControlPlane: true,
2255+
wantTaints: []corev1.Taint{
2256+
clusterv1.NodeUninitializedTaint,
2257+
},
2258+
},
2259+
{
2260+
name: "for control plane, if it already has some taints it should add the uninitialized taint",
2261+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2262+
Taints: []corev1.Taint{dummyTaint},
2263+
},
2264+
kubernetesVersion: semver.MustParse("1.26.0"),
2265+
isControlPlane: true,
2266+
wantTaints: []corev1.Taint{
2267+
dummyTaint,
2268+
clusterv1.NodeUninitializedTaint,
2269+
},
2270+
},
2271+
{
2272+
name: "for worker, it should add the uninitialized taint",
2273+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{},
2274+
kubernetesVersion: semver.MustParse("1.26.0"),
2275+
isControlPlane: false,
2276+
wantTaints: []corev1.Taint{
2277+
clusterv1.NodeUninitializedTaint,
2278+
},
2279+
},
2280+
{
2281+
name: "for worker, if it already has some taints it should add the uninitialized taint",
2282+
nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
2283+
Taints: []corev1.Taint{dummyTaint},
2284+
},
2285+
kubernetesVersion: semver.MustParse("1.26.0"),
2286+
isControlPlane: false,
2287+
wantTaints: []corev1.Taint{
2288+
dummyTaint,
2289+
clusterv1.NodeUninitializedTaint,
2290+
},
2291+
},
2292+
}
2293+
2294+
for _, tt := range tests {
2295+
t.Run(tt.name, func(t *testing.T) {
2296+
g := NewWithT(t)
2297+
addNodeUninitializedTaint(tt.nodeRegistration, tt.isControlPlane, tt.kubernetesVersion)
2298+
g.Expect(tt.nodeRegistration.Taints).To(Equal(tt.wantTaints))
2299+
})
2300+
}
2301+
}
2302+
21962303
// test utils.
21972304

21982305
// newWorkerMachineForCluster returns a Machine with the passed Cluster's information and a pre-configured name.

docs/book/src/developer/providers/bootstrap.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ The following diagram shows the typical logic for a bootstrap provider:
123123

124124
A bootstrap provider's bootstrap data must create `/run/cluster-api/bootstrap-success.complete` (or `C:\run\cluster-api\bootstrap-success.complete` for Windows machines) upon successful bootstrapping of a Kubernetes node. This allows infrastructure providers to detect and act on bootstrap failures.
125125

126+
## Taint Nodes at creation
127+
128+
A bootstrap provider can optionally taint nodes at creation with `node.cluster.x-k8s.io/uninitialized:NoSchedule`.
129+
This taint is used to prevent workloads to be scheduled on Nodes before the node is initialized by Cluster API.
130+
As of today the Node initialization consists of syncing labels from Machines to Nodes. Once the labels have been
131+
initially synced the taint is removed form the Node.
132+
126133
## RBAC
127134

128135
### Provider controller

exp/internal/controllers/machinepool_controller_noderef.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"sigs.k8s.io/cluster-api/controllers/noderefutil"
3131
"sigs.k8s.io/cluster-api/controllers/remote"
3232
expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
33+
"sigs.k8s.io/cluster-api/internal/util/taints"
3334
"sigs.k8s.io/cluster-api/util"
3435
"sigs.k8s.io/cluster-api/util/annotations"
3536
"sigs.k8s.io/cluster-api/util/conditions"
@@ -110,9 +111,10 @@ func (r *MachinePoolReconciler) reconcileNodeRefs(ctx context.Context, cluster *
110111
clusterv1.OwnerKindAnnotation: mp.Kind,
111112
clusterv1.OwnerNameAnnotation: mp.Name,
112113
}
113-
if annotations.AddAnnotations(node, desired) {
114+
// Add annotations and drop NodeUninitializedTaint.
115+
if annotations.AddAnnotations(node, desired) || taints.RemoveNodeTaint(node, clusterv1.NodeUninitializedTaint) {
114116
if err := patchHelper.Patch(ctx, node); err != nil {
115-
log.V(2).Info("Failed patch node to set annotations", "err", err, "node name", node.Name)
117+
log.V(2).Info("Failed patch node to set annotations and drop taints", "err", err, "node name", node.Name)
116118
return ctrl.Result{}, err
117119
}
118120
}

internal/controllers/machine/machine_controller_noderef.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"sigs.k8s.io/cluster-api/api/v1beta1/index"
3535
"sigs.k8s.io/cluster-api/controllers/noderefutil"
3636
"sigs.k8s.io/cluster-api/internal/util/ssa"
37+
"sigs.k8s.io/cluster-api/internal/util/taints"
3738
"sigs.k8s.io/cluster-api/util"
3839
"sigs.k8s.io/cluster-api/util/annotations"
3940
"sigs.k8s.io/cluster-api/util/conditions"
@@ -130,6 +131,15 @@ func (r *Reconciler) reconcileNode(ctx context.Context, cluster *clusterv1.Clust
130131
if err != nil {
131132
return ctrl.Result{}, errors.Wrap(err, "failed to apply labels to Node")
132133
}
134+
// Update `node` with the new version of the object.
135+
if err := r.Client.Scheme().Convert(updatedNode, node, nil); err != nil {
136+
return ctrl.Result{}, errors.Wrapf(err, "failed to convert node to structured object %s", klog.KObj(node))
137+
}
138+
139+
// Reconcile node taints
140+
if err := r.reconcileNodeTaints(ctx, remoteClient, node); err != nil {
141+
return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile taints on Node %s", klog.KObj(node))
142+
}
133143

134144
// Do the remaining node health checks, then set the node health to true if all checks pass.
135145
status, message := summarizeNodeConditions(node)
@@ -258,3 +268,17 @@ func (r *Reconciler) getNode(ctx context.Context, c client.Reader, providerID *n
258268

259269
return &nodeList.Items[0], nil
260270
}
271+
272+
func (r *Reconciler) reconcileNodeTaints(ctx context.Context, remoteClient client.Client, node *corev1.Node) error {
273+
patchHelper, err := patch.NewHelper(node, remoteClient)
274+
if err != nil {
275+
return errors.Wrapf(err, "failed to create patch helper for Node %s", klog.KObj(node))
276+
}
277+
// Drop the NodeUninitializedTaint taint on the node.
278+
if taints.RemoveNodeTaint(node, clusterv1.NodeUninitializedTaint) {
279+
if err := patchHelper.Patch(ctx, node); err != nil {
280+
return errors.Wrapf(err, "failed to patch Node %s to modify taints", klog.KObj(node))
281+
}
282+
}
283+
return nil
284+
}

0 commit comments

Comments
 (0)