Skip to content

Commit eb18a8e

Browse files
author
Yuvaraj Kakaraparthi
committed
Apply uninitialized taint to nodes at creation
and delete after labels are synced
1 parent 9fe7ff5 commit eb18a8e

File tree

7 files changed

+304
-3
lines changed

7 files changed

+304
-3
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 form 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: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ const (
6363
KubeadmConfigControllerName = "kubeadmconfig-controller"
6464
)
6565

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

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

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

bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go

Lines changed: 110 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,115 @@ 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 taints",
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 taints",
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+
clusterv1.NodeUninitializedTaint,
2268+
dummyTaint,
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(HaveLen(len(tt.wantTaints)))
2299+
for _, taint := range tt.wantTaints {
2300+
g.Expect(hasTaint(tt.nodeRegistration.Taints, taint)).To(BeTrue())
2301+
}
2302+
})
2303+
}
2304+
}
2305+
21962306
// test utils.
21972307

21982308
// 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

internal/controllers/machine/machine_controller_noderef.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ func (r *Reconciler) reconcileNode(ctx context.Context, cluster *clusterv1.Clust
133133
return ctrl.Result{}, errors.Wrap(err, "failed to apply patch label to the node")
134134
}
135135

136+
// Reconcile node taints
137+
if err := r.reconcileNodeTaints(ctx, remoteClient, node); err != nil {
138+
return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile taints on Node %s", klog.KObj(node))
139+
}
140+
136141
// Do the remaining node health checks, then set the node health to true if all checks pass.
137142
status, message := summarizeNodeConditions(node)
138143
if status == corev1.ConditionFalse {
@@ -260,3 +265,23 @@ func (r *Reconciler) getNode(ctx context.Context, c client.Reader, providerID *n
260265

261266
return &nodeList.Items[0], nil
262267
}
268+
269+
func (r *Reconciler) reconcileNodeTaints(ctx context.Context, remoteClient client.Client, node *corev1.Node) error {
270+
taints := []corev1.Taint{}
271+
patchHelper, err := patch.NewHelper(node, remoteClient)
272+
if err != nil {
273+
return errors.Wrapf(err, "failed to create patch helper for Node %s", klog.KObj(node))
274+
}
275+
// Drop the NodeUninitializedTaint taint on the node.
276+
for _, taint := range node.Spec.Taints {
277+
if taint.MatchTaint(&clusterv1.NodeUninitializedTaint) {
278+
continue
279+
}
280+
taints = append(taints, taint)
281+
}
282+
node.Spec.Taints = taints
283+
if err := patchHelper.Patch(ctx, node); err != nil {
284+
return errors.Wrapf(err, "failed patch Node %s to set taints", klog.KObj(node))
285+
}
286+
return nil
287+
}

internal/controllers/machine/machine_controller_noderef_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/utils/pointer"
2828
ctrl "sigs.k8s.io/controller-runtime"
2929
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3031
"sigs.k8s.io/controller-runtime/pkg/handler"
3132
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3233

@@ -459,3 +460,70 @@ func TestGetManagedLabels(t *testing.T) {
459460
got := getManagedLabels(allLabels)
460461
g.Expect(got).To(BeEquivalentTo(managedLabels))
461462
}
463+
464+
func TestReconcileNodeTaints(t *testing.T) {
465+
tests := []struct {
466+
name string
467+
node *corev1.Node
468+
}{
469+
{
470+
name: "if the node has the uninitialized taint it should be dropped from the node",
471+
node: &corev1.Node{
472+
ObjectMeta: metav1.ObjectMeta{
473+
Name: "test-node-1",
474+
},
475+
Spec: corev1.NodeSpec{
476+
Taints: []corev1.Taint{
477+
clusterv1.NodeUninitializedTaint,
478+
},
479+
},
480+
},
481+
},
482+
{
483+
name: "if the node is a control plane and has the uninitialized taint it should be dropped from the node",
484+
node: &corev1.Node{
485+
ObjectMeta: metav1.ObjectMeta{
486+
Name: "test-node-1",
487+
},
488+
Spec: corev1.NodeSpec{
489+
Taints: []corev1.Taint{
490+
{
491+
Key: "node-role.kubernetes.io/control-plane",
492+
Effect: corev1.TaintEffectNoSchedule,
493+
},
494+
clusterv1.NodeUninitializedTaint,
495+
},
496+
},
497+
},
498+
},
499+
{
500+
name: "if the node does not have the uninitialized taint it should remain absent from the node",
501+
node: &corev1.Node{
502+
ObjectMeta: metav1.ObjectMeta{
503+
Name: "test-node-1",
504+
},
505+
Spec: corev1.NodeSpec{
506+
Taints: []corev1.Taint{},
507+
},
508+
},
509+
},
510+
}
511+
512+
for _, tt := range tests {
513+
t.Run(tt.name, func(t *testing.T) {
514+
g := NewWithT(t)
515+
fakeClient := fake.NewClientBuilder().WithObjects(tt.node).WithScheme(fakeScheme).Build()
516+
nodeBefore := tt.node.DeepCopy()
517+
r := &Reconciler{}
518+
g.Expect(r.reconcileNodeTaints(ctx, fakeClient, tt.node)).Should(Succeed())
519+
// Verify the NodeUninitializedTaint is dropped.
520+
g.Expect(tt.node.Spec.Taints).ShouldNot(ContainElement(clusterv1.NodeUninitializedTaint))
521+
// Verify all other taints are same.
522+
for _, taint := range nodeBefore.Spec.Taints {
523+
if !taint.MatchTaint(&clusterv1.NodeUninitializedTaint) {
524+
g.Expect(tt.node.Spec.Taints).Should(ContainElement(taint))
525+
}
526+
}
527+
})
528+
}
529+
}

0 commit comments

Comments
 (0)