Repository: kubernetes-sigs/cluster-api
Pull request #7993 (2023-01-25 01:58:11)

⚠️  apply `node.cluster.x-k8s.io/uninitialized` during machine creation
<!-- please add an icon to the title of this PR (see https://sigs.k8s.io/cluster-api/CONTRIBUTING.md#contributing-a-patch), and delete this line and similar ones -->
<!-- the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, feature additions), 🐛 (:bug:, patch and bugfixes), 📖 (:book:, documentation or proposals), or 🌱 (:seedling:, minor or other) -->

**What this PR does / why we need it**:

 This PR adds the taint logic solve the delay problem in node label sync.
Does the following
- Add the `node.cluster.x-k8s.io/uninitialized:NoSchedule` taint to node at creation
- Drop the taint after the labels are synced from the Machine to the Node for the first time. 

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #

Part of https://github.com/kubernetes-sigs/cluster-api/issues/7730


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign vincepri for approval by writing `/assign @vincepri` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["vincepri"]} -->
k8s-ci-robot: @ykakarap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-test-main | 2a08de592531c5dc0675e4f2eb78efb0ec57905f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-test-main/1618065674955919360) | true | `/test pull-cluster-api-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @ykakarap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-test-main | 2a08de592531c5dc0675e4f2eb78efb0ec57905f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-test-main/1618065674955919360) | true | `/test pull-cluster-api-test-main`
pull-cluster-api-test-mink8s-main | 2a08de592531c5dc0675e4f2eb78efb0ec57905f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-test-mink8s-main/1618065675002056704) | true | `/test pull-cluster-api-test-mink8s-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
ykakarap: /milestone v1.4
k8s-ci-robot: @ykakarap: You must be a member of the [kubernetes-sigs/cluster-api-maintainers](https://github.com/orgs/kubernetes-sigs/teams/cluster-api-maintainers/members) GitHub team to set the milestone. If you believe you should be able to issue the /milestone command, please contact your Cluster API Maintainers and have them propose you as an additional delegate for this responsibility.

<details>

In response to [this](https://github.com/kubernetes-sigs/cluster-api/pull/7993#issuecomment-1404600493):

>/milestone v1.4


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: @ykakarap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-informing-ipv6-main | 9ceca6a04e719eceabf49b16f62dc3459a249e3a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-e2e-informing-ipv6-main/1620681441941655552) | false | `/test pull-cluster-api-e2e-informing-ipv6-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign vincepri for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["vincepri"]} -->
ykakarap: /test pull-cluster-api-e2e-full-main
/test pull-cluster-api-e2e-workload-upgrade-1-26-latest-main
k8s-ci-robot: @ykakarap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-workload-upgrade-1-26-latest-main | eb18a8e7908ca746c7d316b83118508935ace868 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-e2e-workload-upgrade-1-26-latest-main/1631182698842165248) | false | `/test pull-cluster-api-e2e-workload-upgrade-1-26-latest-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
sbueringer: /retest
k8s-ci-robot: @ykakarap: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-workload-upgrade-1-26-latest-main | eb18a8e7908ca746c7d316b83118508935ace868 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-e2e-workload-upgrade-1-26-latest-main/1631324943801126912) | false | `/test pull-cluster-api-e2e-workload-upgrade-1-26-latest-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
sbueringer: @ykakarap Just fyi. Looks like the upgrade test tells us that removing the taint doesn't work at the moment

>     Mar  2 17:08:20.280: INFO: ==== node wait: 3 out of 5 nodes are ready, max notReady allowed 0.  Need 2 more before starting.
>    Mar  2 17:08:50.287: INFO: Unschedulable nodes= 2, maximum value for starting tests= 0
>   Mar  2 17:08:50.287: INFO: 	-> Node k8s-upgrade-and-conformance-jg0iwk-worker-q05w6x [[[ Ready=true, Network(available)=false, Taints=[{node.cluster.x-k8s.io/uninitialized  NoSchedule <nil>}], NonblockingTaints=node-role.kubernetes.io/control-plane,node-role.kubernetes.io/master ]]]
ykakarap: > @ykakarap Just fyi. Looks like the upgrade test tells us that removing the taint doesn't work at the moment
> 
> > Mar  2 17:08:20.280: INFO: ==== node wait: 3 out of 5 nodes are ready, max notReady allowed 0.  Need 2 more before starting.
> > Mar  2 17:08:50.287: INFO: Unschedulable nodes= 2, maximum value for starting tests= 0
> > Mar  2 17:08:50.287: INFO: 	-> Node k8s-upgrade-and-conformance-jg0iwk-worker-q05w6x [[[ Ready=true, Network(available)=false, Taints=[{node.cluster.x-k8s.io/uninitialized  NoSchedule }], NonblockingTaints=node-role.kubernetes.io/control-plane,node-role.kubernetes.io/master ]]]

Looking into it now. 
sbueringer: changes lgtm so far
ykakarap: /test pull-cluster-api-e2e-full-main
/test pull-cluster-api-e2e-workload-upgrade-1-26-latest-main
k8s-ci-robot: @ykakarap: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-workload-upgrade-1-26-latest-main | eb18a8e7908ca746c7d316b83118508935ace868 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-e2e-workload-upgrade-1-26-latest-main/1631324943801126912) | false | `/test pull-cluster-api-e2e-workload-upgrade-1-26-latest-main`
pull-cluster-api-test-main | 23b35b89a3f62dbbbf443b878160df9c42a54fb1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/7993/pull-cluster-api-test-main/1631783738536890368) | true | `/test pull-cluster-api-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=7993). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Aykakarap). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/cluster-api/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
ykakarap: /retest
sbueringer: /lgtm
/assign @fabriziopandini 
k8s-ci-robot: LGTM label has been added.  <details>Git tree hash: 51a2c47088cc92a547f7877074445e07fff9e8b9</details>
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from fabriziopandini. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fcluster-api).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["fabriziopandini"]} -->

Patch:
diff --git a/Makefile b/Makefile
index 2c4cababe..f344205b9 100644
--- a/Makefile
+++ b/Makefile
@@ -133,7 +133,7 @@ ENVSUBST_VER := $(call get_go_version,github.com/drone/envsubst/v2)
 ENVSUBST := $(abspath $(TOOLS_BIN_DIR)/$(ENVSUBST_BIN)-$(ENVSUBST_VER))
 ENVSUBST_PKG := github.com/drone/envsubst/v2/cmd/envsubst
 
-GO_APIDIFF_VER := v0.6.0
+GO_APIDIFF_VER := v0.5.0
 GO_APIDIFF_BIN := go-apidiff
 GO_APIDIFF := $(abspath $(TOOLS_BIN_DIR)/$(GO_APIDIFF_BIN)-$(GO_APIDIFF_VER))
 GO_APIDIFF_PKG := github.com/joelanford/go-apidiff
diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go
index 9c201109e..5fa401dcb 100644
--- a/api/v1beta1/common_types.go
+++ b/api/v1beta1/common_types.go
@@ -146,6 +146,16 @@ const (
 	VariableDefinitionFromInline = "inline"
 )
 
+// NodeUninitializedTaint can be added to Nodes at creation by the bootstrap provider, e.g. the
+// KubeadmBootstrap provider will add the taint.
+// This taint is used to prevent workloads to be scheduled on Nodes before the node is initialized by Cluster API.
+// As of today the Node initialization consists of syncing labels from Machines to Nodes. Once the labels
+// have been initially synced the taint is removed from the Node.
+var NodeUninitializedTaint = corev1.Taint{
+	Key:    "node.cluster.x-k8s.io/uninitialized",
+	Effect: corev1.TaintEffectNoSchedule,
+}
+
 const (
 	// TemplateSuffix is the object kind suffix used by template types.
 	TemplateSuffix = "Template"
diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
index e12305a9c..ee8f63994 100644
--- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
+++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
@@ -63,6 +63,24 @@ const (
 	KubeadmConfigControllerName = "kubeadmconfig-controller"
 )
 
+var (
+	// controlPlaneTaint is the taint that kubeadm applies to the control plane nodes
+	// for Kubernetes version >= v1.24.0.
+	// The values are copied from kubeadm codebase.
+	controlPlaneTaint = corev1.Taint{
+		Key:    "node-role.kubernetes.io/control-plane",
+		Effect: corev1.TaintEffectNoSchedule,
+	}
+
+	// oldControlPlaneTaint is the taint that kubeadm applies to the control plane nodes
+	// for Kubernetes version < v1.25.0.
+	// The values are copied from kubeadm codebase.
+	oldControlPlaneTaint = corev1.Taint{
+		Key:    "node-role.kubernetes.io/master",
+		Effect: corev1.TaintEffectNoSchedule,
+	}
+)
+
 const (
 	// DefaultTokenTTL is the default TTL used for tokens.
 	DefaultTokenTTL = 15 * time.Minute
@@ -415,7 +433,15 @@ func (r *KubeadmConfigReconciler) handleClusterNotInitialized(ctx context.Contex
 			},
 		}
 	}
-	initdata, err := kubeadmtypes.MarshalInitConfigurationForVersion(scope.Config.Spec.InitConfiguration, parsedVersion)
+
+	// Add the node uninitialized taint to the list of taints.
+	// DeepCopy the InitConfiguration to prevent updating the actual KubeadmConfig.
+	// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
+	// is initialized by ClusterAPI.
+	initConfiguration := scope.Config.Spec.InitConfiguration.DeepCopy()
+	addNodeUninitializedTaint(&initConfiguration.NodeRegistration, true, parsedVersion)
+
+	initdata, err := kubeadmtypes.MarshalInitConfigurationForVersion(initConfiguration, parsedVersion)
 	if err != nil {
 		scope.Error(err, "Failed to marshal init configuration")
 		return ctrl.Result{}, err
@@ -551,7 +577,14 @@ func (r *KubeadmConfigReconciler) joinWorker(ctx context.Context, scope *Scope)
 		return ctrl.Result{}, errors.Wrapf(err, "failed to parse kubernetes version %q", kubernetesVersion)
 	}
 
-	joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(scope.Config.Spec.JoinConfiguration, parsedVersion)
+	// Add the node uninitialized taint to the list of taints.
+	// DeepCopy the JoinConfiguration to prevent updating the actual KubeadmConfig.
+	// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
+	// is initialized by ClusterAPI.
+	joinConfiguration := scope.Config.Spec.JoinConfiguration.DeepCopy()
+	addNodeUninitializedTaint(&joinConfiguration.NodeRegistration, false, parsedVersion)
+
+	joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(joinConfiguration, parsedVersion)
 	if err != nil {
 		scope.Error(err, "Failed to marshal join configuration")
 		return ctrl.Result{}, err
@@ -657,7 +690,14 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S
 		return ctrl.Result{}, errors.Wrapf(err, "failed to parse kubernetes version %q", kubernetesVersion)
 	}
 
-	joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(scope.Config.Spec.JoinConfiguration, parsedVersion)
+	// Add the node uninitialized taint to the list of taints.
+	// DeepCopy the JoinConfiguration to prevent updating the actual KubeadmConfig.
+	// Do not modify the KubeadmConfig in etcd as this is a temporary taint that will be dropped after the node
+	// is initialized by ClusterAPI.
+	joinConfiguration := scope.Config.Spec.JoinConfiguration.DeepCopy()
+	addNodeUninitializedTaint(&joinConfiguration.NodeRegistration, true, parsedVersion)
+
+	joinData, err := kubeadmtypes.MarshalJoinConfigurationForVersion(joinConfiguration, parsedVersion)
 	if err != nil {
 		scope.Error(err, "Failed to marshal join configuration")
 		return ctrl.Result{}, err
@@ -1066,3 +1106,43 @@ func (r *KubeadmConfigReconciler) ensureBootstrapSecretOwnersRef(ctx context.Con
 	}
 	return nil
 }
+
+// addNodeUninitializedTaint adds the NodeUninitializedTaint to the nodeRegistration.
+// Note: If isControlPlane is true then it also adds the control plane taint if the initial set of taints is nil.
+// This is to ensure consistency with kubeadm's defaulting behavior.
+func addNodeUninitializedTaint(nodeRegistration *bootstrapv1.NodeRegistrationOptions, isControlPlane bool, kubernetesVersion semver.Version) {
+	var taints []corev1.Taint
+	taints = nodeRegistration.Taints
+	if hasTaint(taints, clusterv1.NodeUninitializedTaint) {
+		return
+	}
+
+	// For a control plane, kubeadm adds the default control plane taint if the provided taints are nil.
+	// Since we are adding the uninitialized taint we also have to add the default taints kubeadm would have added if
+	// the taints were nil.
+	if isControlPlane && taints == nil {
+		// Note: Kubeadm uses a different default control plane taint depending on the kubernetes version.
+		// Ref: https://github.com/kubernetes/kubeadm/issues/2200
+		if kubernetesVersion.LT(semver.MustParse("1.24.0")) {
+			taints = []corev1.Taint{oldControlPlaneTaint}
+		} else if kubernetesVersion.GTE(semver.MustParse("1.24.0")) && kubernetesVersion.LT(semver.MustParse("1.25.0")) {
+			taints = []corev1.Taint{
+				oldControlPlaneTaint,
+				controlPlaneTaint,
+			}
+		} else {
+			taints = []corev1.Taint{controlPlaneTaint}
+		}
+	}
+	taints = append(taints, clusterv1.NodeUninitializedTaint)
+	nodeRegistration.Taints = taints
+}
+
+func hasTaint(taints []corev1.Taint, targetTaint corev1.Taint) bool {
+	for _, taint := range taints {
+		if taint.MatchTaint(&targetTaint) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/docs/book/src/clusterctl/developers.md b/docs/book/src/clusterctl/developers.md
index 1170b016b..e5f392579 100644
--- a/docs/book/src/clusterctl/developers.md
+++ b/docs/book/src/clusterctl/developers.md
@@ -33,7 +33,7 @@ If you want to create a local artifact, follow these instructions:
 
 ### Build artifacts locally
 
-In order to build artifacts for the CAPI core provider, the kubeadm bootstrap provider, the kubeadm control plane provider and the Docker infrastructure provider:
+In order to build artifacts for the CAPI core provider, the kubeadm bootstrap provider, the kubeadm control plane provider and the docker infrastructure provider:
 
 ```bash
 make docker-build REGISTRY=gcr.io/k8s-staging-cluster-api PULL_POLICY=IfNotPresent
@@ -96,7 +96,7 @@ during your dev session.
 The above config file changes the location of the [overrides layer] folder thus ensuring
 you dev session isn't hijacked by other local artifacts.
 
-With the only exception of the Docker provider, the local repository folder does not contain cluster templates,
+With the only exception of the docker provider, the local repository folder does not contain cluster templates,
 so the `clusterctl generate cluster` command will fail.
 
 </aside>
@@ -126,11 +126,11 @@ please note that each `provider_repo` should have its own `clusterctl-settings.j
 ## Create a kind management cluster
 
 [kind] can provide a Kubernetes cluster to be used as a management cluster.
-See [Install and/or configure a Kubernetes cluster] for more information.
+See [Install and/or configure a kubernetes cluster] for more information.
 
 *Before* running clusterctl init, you must ensure all the required images are available in the kind cluster.
 
-This is always the case for images published in some image repository like Docker Hub or gcr.io, but it can't be
+This is always the case for images published in some image repository like docker hub or gcr.io, but it can't be
 the case for images built locally; in this case, you can use `kind load` to move the images built locally. e.g.
 
 ```bash
@@ -148,7 +148,7 @@ script.
 
 Optionally, you may want to check if the components are running properly. The
 exact components are dependent on which providers you have initialized. Below
-is an example output with the Docker provider being installed.
+is an example output with the docker provider being installed.
 
 ```bash
 kubectl get deploy -A | grep  "cap\|cert"
@@ -166,7 +166,7 @@ cert-manager                        cert-manager-webhook
 
 ## Additional Notes for the Docker Provider
 
-### Select the appropriate Kubernetes version
+### Select the appropriate kubernetes version
 
 When selecting the `--kubernetes-version`, ensure that the `kindest/node`
 image is available.
@@ -205,6 +205,6 @@ sed -i -e "s/server:.*/server: https:\/\/$(docker port capi-quickstart-lb 6443/t
 [kind]: https://kind.sigs.k8s.io/
 [providers repositories]: configuration.md#provider-repositories
 [overrides layer]: configuration.md#overrides-layer
-[Install and/or configure a Kubernetes cluster]: ../user/quick-start.md#install-andor-configure-a-kubernetes-cluster
+[Install and/or configure a kubernetes cluster]: ../user/quick-start.md#install-andor-configure-a-kubernetes-cluster
 [kind-docker-hub]: https://hub.docker.com/r/kindest/node/tags
 [issue 3795]: https://github.com/kubernetes-sigs/cluster-api/issues/3795
diff --git a/docs/book/src/clusterctl/provider-contract.md b/docs/book/src/clusterctl/provider-contract.md
index a152d8759..6ad30e9cb 100644
--- a/docs/book/src/clusterctl/provider-contract.md
+++ b/docs/book/src/clusterctl/provider-contract.md
@@ -436,7 +436,7 @@ that are compliant with one of the following rules:
   * The object is directly or indirectly linked to a `ClusterResourceSet` object (through the `OwnerReference` chain).
   * The object is directly or indirectly linked to another object with the `clusterctl.cluster.x-k8s.io/move-hierarchy`
     label, e.g. the infrastructure Provider ClusterIdentity objects (linked through the `OwnerReference` chain).
-  * The object has the `clusterctl.cluster.x-k8s.io/move` label or the `clusterctl.cluster.x-k8s.io/move-hierarchy` label,
+  * The object hase the `clusterctl.cluster.x-k8s.io/move` label or the `clusterctl.cluster.x-k8s.io/move-hierarchy` label,
     e.g. the CPI config secret.
 
 Note. `clusterctl.cluster.x-k8s.io/move` and `clusterctl.cluster.x-k8s.io/move-hierarchy` labels could be applied
diff --git a/docs/book/src/developer/architecture/controllers/cluster-topology.md b/docs/book/src/developer/architecture/controllers/cluster-topology.md
index 0aeee2780..b1dd4bdfe 100644
--- a/docs/book/src/developer/architecture/controllers/cluster-topology.md
+++ b/docs/book/src/developer/architecture/controllers/cluster-topology.md
@@ -12,7 +12,7 @@ Its main responsibilities are to:
 
 The high level workflow of ClusterTopology reconciliation is shown below.
 
-![ClusterTopology Reconciler Component Diagram](../../../images/cluster-topology-reconciller.png)
+![ClusterTopology Reconciller Component Diagram](../../../images/cluster-topology-reconciller.png)
 
 ### Additional information
 
diff --git a/docs/book/src/developer/architecture/controllers/control-plane.md b/docs/book/src/developer/architecture/controllers/control-plane.md
index 0d9e34bb3..2eece2fa8 100644
--- a/docs/book/src/developer/architecture/controllers/control-plane.md
+++ b/docs/book/src/developer/architecture/controllers/control-plane.md
@@ -180,7 +180,7 @@ following fields defined:
       introspection by clients, and is used to provide the CRD-based integration
       for the scale subresource and additional integrations for things like
       kubectl describe. The string will be in the same format as the query-param
-      syntax. More info about label selectors: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
+      syntax. More info about label selectors: http://kubernetes.io/docs/user-guide/labels#label-selectors
     </td>
     <td></td>
   </tr>
diff --git a/docs/book/src/developer/architecture/controllers/machine-pool.md b/docs/book/src/developer/architecture/controllers/machine-pool.md
index 964405796..04cd327e1 100644
--- a/docs/book/src/developer/architecture/controllers/machine-pool.md
+++ b/docs/book/src/developer/architecture/controllers/machine-pool.md
@@ -10,7 +10,7 @@ The MachinePool controller's main responsibilities are:
     * The associated InfrastructureMachinePool object.
 * Copy data from `BootstrapConfig.Status.DataSecretName` to `MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName` if
 `MachinePool.Spec.Template.Spec.Bootstrap.DataSecretName` is empty.
-* Setting NodeRefs on MachinePool instances to be able to associate them with Kubernetes nodes.
+* Setting NodeRefs on MachinePool instances to be able to associate them with kubernetes nodes.
 * Deleting Nodes in the target cluster when the associated MachinePool instance is deleted.
 * Keeping the MachinePool's Status object up to date with the InfrastructureMachinePool's Status object.
 * Finding Kubernetes nodes matching the expected providerIDs in the workload cluster.
diff --git a/docs/book/src/developer/architecture/controllers/machine.md b/docs/book/src/developer/architecture/controllers/machine.md
index 679a9cb6f..60a235f16 100644
--- a/docs/book/src/developer/architecture/controllers/machine.md
+++ b/docs/book/src/developer/architecture/controllers/machine.md
@@ -10,7 +10,7 @@ The Machine controller's main responsibilities are:
     * The associated InfrastructureMachine object.
 * Copy data from `BootstrapConfig.Status.DataSecretName` to `Machine.Spec.Bootstrap.DataSecretName` if
 `Machine.Spec.Bootstrap.DataSecretName` is empty.
-* Setting NodeRefs to be able to associate machines and Kubernetes nodes.
+* Setting NodeRefs to be able to associate machines and kubernetes nodes.
 * Deleting Nodes in the target cluster when the associated machine is deleted.
 * Cleanup of related objects.
 * Keeping the Machine's Status object up to date with the InfrastructureMachine's Status object.
diff --git a/docs/book/src/developer/logging.md b/docs/book/src/developer/logging.md
index fc642c565..c5afde93b 100644
--- a/docs/book/src/developer/logging.md
+++ b/docs/book/src/developer/logging.md
@@ -137,7 +137,7 @@ We are working to continuously improving this experience, allowing Cluster API d
 For the best experience exploring the logs using Tilt:
 1. Set `--logging-format=json`. 
 2. Set a high log verbosity, e.g. `v=5`.
-3. Enable Promtail, Loki, and Grafana under `deploy_observability`.
+3. Enable promtail, loki, and grafana under deploy_observability.
 
 A minimal example of a tilt-settings.yaml file that deploys a ready-to-use logging suite looks like:
 ```yaml
diff --git a/docs/book/src/developer/providers/bootstrap.md b/docs/book/src/developer/providers/bootstrap.md
index c25eed0e7..1ff1fef50 100644
--- a/docs/book/src/developer/providers/bootstrap.md
+++ b/docs/book/src/developer/providers/bootstrap.md
@@ -123,6 +123,13 @@ The following diagram shows the typical logic for a bootstrap provider:
 
 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.
 
+## Taint Nodes at creation
+
+A bootstrap provider can optionally taint nodes at creation with `node.cluster.x-k8s.io/uninitialized:NoSchedule`.
+This taint is used to prevent workloads to be scheduled on Nodes before the node is initialized by Cluster API.
+As of today the Node initialization consists of syncing labels from Machines to Nodes. Once the labels have been 
+initially synced the taint is removed form the Node.
+
 ## RBAC
 
 ### Provider controller
diff --git a/docs/book/src/developer/providers/contracts.md b/docs/book/src/developer/providers/contracts.md
index 6aee48288..906e49508 100644
--- a/docs/book/src/developer/providers/contracts.md
+++ b/docs/book/src/developer/providers/contracts.md
@@ -5,7 +5,7 @@ Cluster API defines a contract which requires providers to implement certain fie
 Cluster API defines the following contracts:
 
 - [Infrastructure provider contract](./cluster-infrastructure.md)
-- [Bootstrap provider contract](./bootstrap.md)
+- [Boostrap provider contract](./bootstrap.md)
 - [Control Plane provider contract](../../developer/architecture/controllers/control-plane.md#crd-contracts)
 - [Machine provider contract](./machine-infrastructure.md)
 - [clusterctl provider contract](../../clusterctl/provider-contract.md#clusterctl-provider-contract)
@@ -14,7 +14,7 @@ Cluster API defines the following contracts:
 ## API version labels
 Providers MUST set `cluster.x-k8s.io/<version>` label on all Custom Resource Definitions related to Cluster API starting with v1alpha3.
 The label is a map from an API Version of Cluster API (contract) to your Custom Resource Definition versions.
-The value is an underscore-delimited (_) list of versions.
+The value is a underscore-delimited (_) list of versions.
 Each value MUST point to an available version in your CRD Spec.
 
 The label allows Cluster API controllers to perform automatic conversions for object references, the controllers will pick the last available version in the list if multiple versions are found.
@@ -33,4 +33,4 @@ An example of this is in the [Kubeadm Bootstrap provider](https://github.com/kub
 
 ## Improving and contributing to the contract
 
-The definition of the contract between Cluster API and providers may be changed in future versions of Cluster API. The Cluster API maintainers welcome feedback and contributions to the contract in order to improve how it's defined, its clarity and visibility to provider implementers and its suitability across the different kinds of Cluster API providers. To provide feedback or open a discussion about the provider contract please [open an issue on the Cluster API](https://github.com/kubernetes-sigs/cluster-api/issues/new?assignees=&labels=&template=feature_request.md) repo or add an item to the agenda in the [Cluster API community meeting](https://git.k8s.io/community/sig-cluster-lifecycle/README.md#cluster-api).
+The definition of the contract between Cluster API and providers may be changed in future versions of Cluster API. The Cluster API maintainers welcome feedback and contributions to the contract in order to improve how it's defined, its clarity and visibility to provider implementers and its suitability across the different kinds of Cluster API providers. To provide feedback or open a discussion about the provider contract please [open an issue on the Cluster API](https://github.com/kubernetes-sigs/cluster-api/issues/new?assignees=&labels=&template=feature_request.md) repo or add an item to the agenda in the [Cluster API community meeting](http://git.k8s.io/community/sig-cluster-lifecycle/README.md#cluster-api).
diff --git a/docs/book/src/developer/providers/v0.3-to-v0.4.md b/docs/book/src/developer/providers/v0.3-to-v0.4.md
index 5af8084d6..41481ff8d 100644
--- a/docs/book/src/developer/providers/v0.3-to-v0.4.md
+++ b/docs/book/src/developer/providers/v0.3-to-v0.4.md
@@ -22,7 +22,7 @@
 
 - The `test` folder now ships with its own Go module `sigs.k8s.io/cluster-api/test`.
 - The module is going to be tagged and versioned as part of the release.
-- Folks importing the test e2e framework or the Docker infrastructure provider need to import the new module.
+- Folks importing the test e2e framework or the docker infrastructure provider need to import the new module.
   - When imported, the test module version should always match the Cluster API one.
   - Add the following line in `go.mod` to replace the cluster-api dependency in the test module (change the version to your current Cluster API version):
   ```
@@ -199,7 +199,7 @@ should be executed before this changes.
 
 **Changes in the `/config/default` folder:**
 
-1. Edit the `/config/default/kustomization.yaml` file and replace all the occurrences of
+1. Edit the `/config/default/kustomization.yaml` file and replace all the occurencies of
    ```
          kind: Certificate
          group: cert-manager.io
diff --git a/docs/book/src/developer/providers/v1.0-to-v1.1.md b/docs/book/src/developer/providers/v1.0-to-v1.1.md
index a0291b85f..f7b2759df 100644
--- a/docs/book/src/developer/providers/v1.0-to-v1.1.md
+++ b/docs/book/src/developer/providers/v1.0-to-v1.1.md
@@ -65,5 +65,5 @@ are kept in sync with the versions used by `sigs.k8s.io/controller-runtime`.
   fully exploit this feature, it is required to pass the `LogFolder` parameter when calling `CreateKindBootstrapClusterAndLoadImages`. Please see this [PR](https://github.com/kubernetes-sigs/cluster-api/pull/5910) for an example on how to use it. 
 * The `gci` linter has been enabled to enforce consistent imports. As usual, feel free to take a look at our linter config, but of course it's not mandatory to adopt it.
 * The Tilt dev setup has been extended with:
-    * [an option to deploy Grafana, Loki and Promtail](https://github.com/kubernetes-sigs/cluster-api/pull/5336) 
+    * [an option to deploy Grafana, Loki and promtail](https://github.com/kubernetes-sigs/cluster-api/pull/5336) 
     * [options to ease debugging of controllers (delve, profiler, metrics)](https://github.com/kubernetes-sigs/cluster-api/pull/5485) 
diff --git a/docs/book/src/developer/providers/v1.2-to-v1.3.md b/docs/book/src/developer/providers/v1.2-to-v1.3.md
index 94d34055b..51f77bbce 100644
--- a/docs/book/src/developer/providers/v1.2-to-v1.3.md
+++ b/docs/book/src/developer/providers/v1.2-to-v1.3.md
@@ -62,14 +62,14 @@ The default value is 0, meaning that the volume can be detached without any time
 - The `Move` function in E2E test framework for clusterctl has been modified to:
   * print the `clusterctl move` command including the arguments similar to `Init`.
   * log the output to the a `clusterctl-move.log` file at the subdirectory `logs/<namespace>`.
-- The self-hosted upgrade test now also upgrades the self-hosted cluster's Kubernetes version by default. For that it requires the following variables to be set:
+- The self-hosted upgrade test now also upgrades the self-hosted clusters kubernetes version by default. For that it requires the following variables to be set:
   * `KUBERNETES_VERSION_UPGRADE_FROM`
   * `KUBERNETES_VERSION_UPGRADE_TO`
   * `ETCD_VERSION_UPGRADE_TO`
   * `COREDNS_VERSION_UPGRADE_TO`
-  The variable `SkipUpgrade` could be set to revert to the old behaviour by making use of the `KUBERNETES_VERSION` variable and skipping the Kubernetes upgrade.
+  The variable `SkipUpgrade` could be set to revert to the old behaviour by making use of the `KUBERNETES_VERSION` variable and skipping the kubernetes upgrade.
 - cert-manager upgraded from v1.9.1 to v1.10.1.
-- Machine `providerID` is now being strictly checked for equality when compared against Kubernetes node `providerID` data. This is the expected criteria for correlating a Cluster API machine to its corresponding Kubernetes node, but historically this comparison was not strict, and instead compared only against the `ID` substring part of the full `providerID` string. Because different providers construct `providerID` strings differently, the `ID` substring is not uniformly defined and implemented across providers, and thus the existing `providerID` equality cannot guarantee the correct Machine-Node correlation. It is very unlikely that this new behavior will break existing providers, but FYI: if strict `providerID` equality will degrade expected behaviors, you may need to update your provider implementation prior to adopting Cluster API v1.3.
+- Machine `providerID` is now being strictly checked for equality when compared against Kubernetes node `providerID` data. This is the expected criteria for correlating a Cluster API machine to its corresponding Kubernetes node, but historically this comparison was not strict, and instead compared only against the `ID` substring part of the full `providerID` string. Because different providers construct `providerID` strings differently, the `ID` substring is not uniformly defined and implemented across providers, and thus the existing `providerID` equality can not guarantee the correct Machine-Node correlation. It is very unlikely that this new behavior will break existing providers, but FYI: if strict `providerID` equality will degrade expected behaviors, you may need to update your provider implementation prior to adopting Cluster API v1.3.
 - The default minimum TLS version in use by the webhook servers is 1.2.
 - OwnerReferences are now more strictly enforced for objects managed by Cluster API. Machines, Bootstrap configs, Infrastructure Machines and Secrets created by CAPI components [now have strictly enforced controller owner references](https://github.com/kubernetes-sigs/cluster-api/issues/7575). This is not expected to require changes for providers.
 
diff --git a/docs/book/src/developer/providers/v1.3-to-v1.4.md b/docs/book/src/developer/providers/v1.3-to-v1.4.md
index 574c78f20..5873897d4 100644
--- a/docs/book/src/developer/providers/v1.3-to-v1.4.md
+++ b/docs/book/src/developer/providers/v1.3-to-v1.4.md
@@ -13,7 +13,6 @@ maintainers of providers and consumers of our Go API.
 
 - sigs.k8s.io/controller-runtime: v0.13.x => v0.14.x
 - sigs.k8s.io/controller-tools: v0.10.x => v0.11.x
-- github.com/joelanford/go-apidiff: 0.5.0 => 0.6.0
 
 ## Changes by Kind
 
diff --git a/docs/book/src/developer/repository-layout.md b/docs/book/src/developer/repository-layout.md
index 7eb2ddf35..bc6936fd3 100644
--- a/docs/book/src/developer/repository-layout.md
+++ b/docs/book/src/developer/repository-layout.md
@@ -29,7 +29,7 @@ cluster-api
 └───Makefile
 ```
 
-### GitHub
+### Github
 
 [~/.github](https://github.com/kubernetes-sigs/cluster-api/tree/main/.github)
 
@@ -57,7 +57,7 @@ This folder contains a reference implementation of a Cluster API Control Plane p
 
 ### Cluster API Provider Docker 
 
-[~/test/infrastructure/docker](https://github.com/kubernetes-sigs/cluster-api/tree/main/test/infrastructure/docker)
+[~/test/insfrastructure/docker](https://github.com/kubernetes-sigs/cluster-api/tree/main/test/infrastructure/docker)
 
 This folder contains a reference implementation of an infrastructure provider for the Cluster API project using Docker. This provider is intended for development purposes only. 
 
@@ -148,7 +148,7 @@ The [api](#api) folder contains webhooks consisting of validators and defaults f
 
 [~/internal/webhooks](https://github.com/kubernetes-sigs/cluster-api/tree/main/internal/webhooks)
 
-This directory contains the implementation of some of the Cluster API webhooks. The internal implementation means that the methods supplied by this package cannot be imported by external code bases. 
+This directory contains the implementation of some of the Cluster API webhooks. The internal implementation means that the methods supplied by this package can not be imported by external code bases. 
 
 [~/webhooks](https://github.com/kubernetes-sigs/cluster-api/tree/main/webhooks)
 
diff --git a/docs/book/src/developer/tilt.md b/docs/book/src/developer/tilt.md
index b8971ed75..42f7200a0 100644
--- a/docs/book/src/developer/tilt.md
+++ b/docs/book/src/developer/tilt.md
@@ -20,7 +20,7 @@ workflow that offers easy deployments and rapid iterative builds.
 ## Getting started
 
 ### Create a kind cluster
-A script to create a KIND cluster along with a local Docker registry and the correct mounts to run CAPD is included in the hack/ folder.
+A script to create a KIND cluster along with a local docker registry and the correct mounts to run CAPD is included in the hack/ folder.
 
 To create a pre-configured cluster run:
 
@@ -200,7 +200,7 @@ Important! This feature requires the `helm` command to be available in the user'
 Supported values are:
 
   * `grafana`*: To create dashboards and query `loki` as well as `prometheus`.
-  * `kube-state-metrics`: For exposing metrics for Kubernetes and CAPI resources to `prometheus`.
+  * `kube-state-metrics`: For exposing metrics for kubernetes and CAPI resources to `prometheus`.
   * `loki`: To receive and store logs.
   * `prometheus`*: For collecting metrics from Kubernetes.
   * `promtail`: For providing pod logs to `loki`.
@@ -265,7 +265,7 @@ Supported settings:
     }
     ```
 
-    ###### Goland / IntelliJ
+    ###### Goland / Intellij
     With the above example, you can configure [a Go Remote run/debug
     configuration](https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html#step-3-create-the-remote-run-debug-configuration-on-the-client-computer)
     pointing at port 30000.
@@ -462,7 +462,7 @@ possible (the container images do not need the entire go toolchain, source code,
 
 ## IDE support for Tiltfile
 
-For IntelliJ, Syntax highlighting for the Tiltfile can be configured with a TextMate Bundle. For instructions, please see:
+For Intellij, Syntax highlighting for the Tiltfile can be configured with a TextMate Bundle. For instructions, please see:
 [Tiltfile TextMate Bundle](https://github.com/tilt-dev/tiltfile.tmbundle).
 
 For VSCode the [Bazel plugin](https://marketplace.visualstudio.com/items?itemName=BazelBuild.vscode-bazel) can be used, it provides
@@ -475,10 +475,10 @@ syntax highlighting and auto-formatting. To enable it for Tiltfile a file associ
 
 ## Using Podman
 
-[Podman](https://podman.io) can be used instead of Docker by following these actions:
+[Podman](https://podman.io) can be used instead of docker by following these actions:
 
 1. Enable the podman unix socket (eg. `systemctl --user enable --now podman.socket` on Fedora)
-1. Set `build_engine` to `podman` in `tilt-settings.yaml` (optional, only if both Docker & podman are installed)
+1. Set `build_engine` to `podman` in `tilt-settings.yaml` (optional, only if both docker & podman are installed)
 1. Define the env variable `DOCKER_HOST` to the right socket while running tilt (eg. `DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock tilt up`)
 
 NB: The socket defined by `DOCKER_HOST` is used only for the `hack/tools/tilt-prepare` command, the image build is running the `podman build`/`podman push` commands.
diff --git a/docs/book/src/reference/glossary.md b/docs/book/src/reference/glossary.md
index 0b77cdb02..ddce00543 100644
--- a/docs/book/src/reference/glossary.md
+++ b/docs/book/src/reference/glossary.md
@@ -28,7 +28,7 @@ A temporary cluster that is used to provision a Target Management cluster.
 Refers to a [provider](#provider) that implements a solution for the [bootstrap](#bootstrap) process.
 Bootstrap provider's interaction with Cluster API is based on what is defined in the [Cluster API contract](#contract). 
 
-See [CABPK](#cabpk). 
+See [CABPK](#CABPK). 
 
 # C
 ---
@@ -145,7 +145,7 @@ The set of Kubernetes services that form the basis of a cluster. See also [https
 Refers to a [provider](#provider) that implements a solution for the management of a Kubernetes [control plane](#control-plane).
 Control plane provider's interaction with Cluster API is based on what is defined in the [Cluster API contract](#contract).
 
-See [KCP](#kcp).
+See [KCP](#KCP).
 
 ### Core provider
 
@@ -161,7 +161,7 @@ See [CAPI](#cluster-api).
 ### Default implementation
 
 A feature implementation offered as part of the Cluster API project and maintained by the CAPI core team; For example
-[KCP](#kcp) is a default implementation for a [control plane provider](#control-plane-provider).
+[KCP](#KCP) is a default implementation for a [control plane provider](#control-plane-provider).
 
 # E
 ---
@@ -179,7 +179,7 @@ A [runtime extension](#runtime-extension) that implements a [topology mutation h
 
 ### Horizontal Scaling
 
-The ability to add more machines based on policy and well-defined metrics. For example, add a machine to a cluster when CPU load average > (X) for a period of time (Y).
+The ability to add more machines based on policy and well defined metrics. For example, add a machine to a cluster when CPU load average > (X) for a period of time (Y).
 
 ### Host
 
@@ -217,7 +217,7 @@ see [Server](#server)
 
 ### Immutability
 
-A resource that does not mutate.  In Kubernetes we often state the instance of a running pod is immutable or does not change once it is run.  In order to make a change, a new pod is run.  In the context of [Cluster API](#cluster-api) we often refer to a running instance of a [Machine](#machine) as being immutable, from a [Cluster API](#cluster-api) perspective.
+A resource that does not mutate.  In kubernetes we often state the instance of a running pod is immutable or does not change once it is run.  In order to make a change, a new pod is run.  In the context of [Cluster API](#cluster-api) we often refer to a running instance of a [Machine](#machine) as being immutable, from a [Cluster API](#cluster-api) perspective.
 
 ### IPAM provider
 
@@ -265,7 +265,7 @@ Perform create, scale, upgrade, or destroy operations on the cluster.
 
 ### Managed Topology
 
-See [Topology](#topology)
+See [Topology](#Topology)
 
 ### Management cluster
 
diff --git a/docs/book/src/security/pod-security-standards.md b/docs/book/src/security/pod-security-standards.md
index c8ab7801a..0f39a98fb 100644
--- a/docs/book/src/security/pod-security-standards.md
+++ b/docs/book/src/security/pod-security-standards.md
@@ -2,7 +2,7 @@
 
 Pod Security Admission allows applying [Pod Security Standards] during creation of pods at the cluster level.
 
-The flavor `development-topology` for the Docker provider used in [Quick Start](../user/quick-start.md) already includes a basic Pod Security Standard configuration.
+The flavor `development-topology` for the docker provider used in [Quick Start](../user/quick-start.md) already includes a basic Pod Security Standard configuration.
 It is using ClusterClass variables and patches to inject the configuration.
 
 ## Adding a basic Pod Security Standards configuration to a ClusterClass
diff --git a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
index 3d83f4484..fdef08630 100644
--- a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
+++ b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md
@@ -629,9 +629,9 @@ constant default value which can be specified in the schema is not enough.
           # If .vnetName is set, it is used. Otherwise, we will use `{{.builtin.cluster.name}}-vnet`.  
           template: "{{ if .vnetName }}{{.vnetName}}{{else}}{{.builtin.cluster.name}}-vnet{{end}}"
 ```
-When writing templates, a subset of functions from [the Sprig library](https://masterminds.github.io/sprig/) can be used to
-write expressions, e.g., `{{ .name | upper }}`. Only functions that are guaranteed to evaluate to the same result
-for a given input are allowed (e.g. `upper` or `max` can be used, while `now` or `randAlpha` cannot be used).
+When writing templates, a subset of functions from [the sprig library](http://masterminds.github.io/sprig/) can be used to
+write expressions like e.g. `{{ .name | upper }}`. Only functions that are guaranteed to evaluate to the same result
+for a given input are allowed (e.g. `upper` or `max` can be used, while `now` or `randAlpha` can not be used).
 
 ### Optional patches
 
diff --git a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
index c4df864a7..7f75f6f83 100644
--- a/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
+++ b/docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md
@@ -119,7 +119,7 @@ Variable definitions can be inline in the ClusterClass or from any number of ext
 of a variable definition is recorded in the `from` field in ClusterClass `.status.variables`.
 Variables that are defined by an external DiscoverVariables hook will have the name of the patch they are associated with as the value of `from`.
 Variables that are defined in the ClusterClass `.spec.variables` will have `inline` as the value of `from`.
-Note: `inline` is a reserved name for patches. It cannot be used as the name of an external patch to avoid conflicts.
+Note: `inline` is a reserved name for patches. It can not be used as the name of an external patch to avoid conflicts.
 
 If all variables that share a name have equivalent schemas the variable definitions are not in conflict. These variables can
 be set without providing `definitionFrom` value - [see below](#setting-values-for-variables-in-the-cluster). The CAPI components will
@@ -358,7 +358,7 @@ variables:
         type: array
         items:
           type: string
-        description: "preLoadImages sets the images for the Docker machines to preload."
+        description: "preLoadImages sets the images for the docker machines to preload."
   - name: podSecurityStandard
     required: false
     schema:
diff --git a/docs/book/src/tasks/multiple-providers.md b/docs/book/src/tasks/multiple-providers.md
index 4225266ba..0be7a4553 100644
--- a/docs/book/src/tasks/multiple-providers.md
+++ b/docs/book/src/tasks/multiple-providers.md
@@ -1,6 +1,6 @@
 # Running multiple providers
 
-Cluster API supports running multiple infrastructure/bootstrap/control plane providers on the same management cluster. It's highly recommended to rely on
+Cluster API supports running multiple infrastructure/bootstrap/control plane providers on the same management cluster. It's highly recommeded to rely on
 [clusterctl init](../clusterctl/commands/init.md) command in this case. [clusterctl](../clusterctl/overview.md) will help ensure that all providers support the same
 [API Version of Cluster API](../clusterctl/provider-contract.md#metadata-yaml) (contract).
 
diff --git a/docs/book/src/user/quick-start.md b/docs/book/src/user/quick-start.md
index 7112b8532..ce4ba3775 100644
--- a/docs/book/src/user/quick-start.md
+++ b/docs/book/src/user/quick-start.md
@@ -101,16 +101,16 @@ a target [management cluster] on the selected [infrastructure provider].
    the workload cluster's nodes, are actually running within pods in the management cluster. In order to communicate with
    the workload cluster's API server, we'll need to expose it. We are using Kind which is a limited environment. The
    easiest way to expose the workload cluster's API server (a pod within a node running in a VM that is itself running
-   within a pod in the management cluster, that is running inside a Docker container), is to use a LoadBalancer service.
+   within a pod in the management cluster, that is running inside a docker container), is to use a LoadBalancer service.
 
    To allow using a LoadBalancer service, we can't use the kind's default CNI (kindnet), but we'll need to install
    another CNI, like Calico. In order to do that, we'll need first to initiate the kind cluster with two modifications:
    1. Disable the default CNI
-   2. Add the Docker credentials to the cluster, to avoid the Docker Hub pull rate limit of the calico images; read more
+   2. Add the docker credentials to the cluster, to avoid the docker hub pull rate limit of the calico images; read more
       about it in the [docker documentation](https://docs.docker.com/docker-hub/download-rate-limit/), and in the
       [kind documentation](https://kind.sigs.k8s.io/docs/user/private-registries/#mount-a-config-file-to-each-node).
 
-   Create a configuration file for kind. Please notice the Docker config file path, and adjust it to your local setting:
+   Create a configuration file for kind. Please notice the docker config file path, and adjust it to your local setting:
    ```bash
    cat <<EOF > kind-config.yaml
    kind: Cluster
@@ -835,17 +835,17 @@ export AZURE_RESOURCE_GROUP="<ResourceGroupName>"
 {{#/tab }}
 {{#tab CloudStack}}
 
-A Cluster API compatible image must be available in your CloudStack installation. For instructions on how to build a compatible image
-see [image-builder (CloudStack)](https://image-builder.sigs.k8s.io/capi/providers/cloudstack.html)
+A ClusterAPI compatible image must be available in your Cloudstack installation. For instructions on how to build a compatible image
+see [image-builder (Cloudstack)](https://image-builder.sigs.k8s.io/capi/providers/cloudstack.html)
 
 Prebuilt images can be found [here](http://packages.shapeblue.com/cluster-api-provider-cloudstack/images/)
 
-To see all required CloudStack environment variables execute:
+To see all required Cloudstack environment variables execute:
 ```bash
 clusterctl generate cluster --infrastructure cloudstack --list-variables capi-quickstart
 ```
 
-Apart from the script, the following CloudStack environment variables are required.
+Apart from the script, the following Cloudstack environment variables are required.
 ```bash
 # Set this to the name of the zone in which to deploy the cluster
 export CLOUDSTACK_ZONE_NAME=<zone name>
@@ -951,7 +951,7 @@ export KUBE_VIP_VERSION="v0.5.0"
 # Name of the GCP datacenter location. Change this value to your desired location
 export GCP_REGION="<GCP_REGION>"
 export GCP_PROJECT="<GCP_PROJECT>"
-# Make sure to use same Kubernetes version here as building the GCE image
+# Make sure to use same kubernetes version here as building the GCE image
 export KUBERNETES_VERSION=1.23.3
 # This is the image you built. See https://github.com/kubernetes-sigs/image-builder
 export IMAGE_ID=projects/$GCP_PROJECT/global/images/<built image>
@@ -1452,12 +1452,12 @@ If the nodes don't become ready after a long period, read the pods in the `kube-
 kubectl --kubeconfig=./capi-quickstart.kubeconfig get pod -n kube-system
 ```
 
-If the Calico pods are in image pull error state (`ErrImagePull`), it's probably because of the Docker Hub pull rate limit.
-We can try to fix that by adding a secret with our Docker Hub credentials, and use it;
+If the Calico pods are in image pull error state (`ErrImagePull`), it's probably because of the docker hub pull rate limit.
+We can try to fix that by adding a secret with our docker hub credentials, and use it;
 see [here](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials)
 for details.
 
-First, create the secret. Please notice the Docker config file path, and adjust it to your local setting.
+First, create the secret. Please notice the docker config file path, and adjust it to your local setting.
 ```bash
 kubectl --kubeconfig=./capi-quickstart.kubeconfig create secret generic docker-creds \
     --from-file=.dockerconfigjson=<YOUR DOCKER CONFIG FILE PATH> \
diff --git a/docs/book/src/user/troubleshooting.md b/docs/book/src/user/troubleshooting.md
index 225a058a6..599336ff4 100644
--- a/docs/book/src/user/troubleshooting.md
+++ b/docs/book/src/user/troubleshooting.md
@@ -46,7 +46,7 @@ In the example above we can see that the Machine `capi-quickstart-6587k-xtvnz` h
 
 To investigate why a machine fails to start you can inspect the conditions of the objects using `clusterctl describe --show-conditions all cluster capi-quickstart`. You can get more detailed information about the status of the machines using `kubectl describe machines`.
 
-To inspect the underlying infrastructure - in this case Docker containers acting as Machines - you can access the logs using `docker logs <MACHINE-NAME>`. For example:
+To inspect the underlying infrastructure - in this case docker containers acting as Machines - you can access the logs using `docker logs <MACHINE-NAME>`. For example:
 
 ```shell
 docker logs capi-quickstart-6587k-xtvnz
@@ -109,7 +109,7 @@ provisioning might be stuck:
  
 1. if there are stopped containers on your machine from previous runs. Clean unused containers with [docker rm -f ](https://docs.docker.com/engine/reference/commandline/rm/). 
 
-2. if the Docker space on your disk is being exhausted 
+2. if the docker space on your disk is being exhausted 
     * Run [docker system df](https://docs.docker.com/engine/reference/commandline/system_df/) to inspect the disk space consumed by Docker resources.
     * Run [docker system prune --volumes](https://docs.docker.com/engine/reference/commandline/system_prune/) to prune dangling images, containers, volumes and networks.
 
@@ -132,7 +132,7 @@ This error was also observed in Docker Desktop 4.3 and 4.4 on MacOS. It can be r
 
 [The upstream issue for this error is closed as of the release of Docker 4.5.0](https://github.com/docker/for-mac/issues/6071)
 
-Note: The below workaround is not recommended unless upgrade or downgrade cannot be performed.
+Note: The below workaround is not recommended unless upgrade or downgrade can not be performed.
 
 If using a version of Docker Desktop for Mac 4.3 or 4.4, the following workaround can be used:
 
diff --git a/exp/internal/controllers/machinepool_controller_noderef.go b/exp/internal/controllers/machinepool_controller_noderef.go
index 275027b0b..1d680ed05 100644
--- a/exp/internal/controllers/machinepool_controller_noderef.go
+++ b/exp/internal/controllers/machinepool_controller_noderef.go
@@ -30,6 +30,7 @@ import (
 	"sigs.k8s.io/cluster-api/controllers/noderefutil"
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
+	"sigs.k8s.io/cluster-api/internal/util/taints"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
@@ -110,9 +111,10 @@ func (r *MachinePoolReconciler) reconcileNodeRefs(ctx context.Context, cluster *
 			clusterv1.OwnerKindAnnotation:        mp.Kind,
 			clusterv1.OwnerNameAnnotation:        mp.Name,
 		}
-		if annotations.AddAnnotations(node, desired) {
+		// Add annotations and drop NodeUninitializedTaint.
+		if annotations.AddAnnotations(node, desired) || taints.RemoveNodeTaint(node, clusterv1.NodeUninitializedTaint) {
 			if err := patchHelper.Patch(ctx, node); err != nil {
-				log.V(2).Info("Failed patch node to set annotations", "err", err, "node name", node.Name)
+				log.V(2).Info("Failed patch node to set annotations and drop taints", "err", err, "node name", node.Name)
 				return ctrl.Result{}, err
 			}
 		}
diff --git a/go.mod b/go.mod
index 94dcb95a0..154df1398 100644
--- a/go.mod
+++ b/go.mod
@@ -20,8 +20,8 @@ require (
 	github.com/google/go-github/v48 v48.2.0
 	github.com/google/gofuzz v1.2.0
 	github.com/mattn/go-runewidth v0.0.14 // indirect
-	github.com/onsi/ginkgo/v2 v2.9.0
-	github.com/onsi/gomega v1.27.2
+	github.com/onsi/ginkgo/v2 v2.8.4
+	github.com/onsi/gomega v1.27.1
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/cobra v1.6.1
 	github.com/spf13/pflag v1.0.5
@@ -29,8 +29,8 @@ require (
 	github.com/valyala/fastjson v1.6.4
 	go.etcd.io/etcd/api/v3 v3.5.6
 	go.etcd.io/etcd/client/v3 v3.5.6
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/oauth2 v0.6.0
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/oauth2 v0.5.0
 	google.golang.org/grpc v1.52.0
 	k8s.io/api v0.26.1
 	k8s.io/apiextensions-apiserver v0.26.1
@@ -123,9 +123,9 @@ require (
 	go.uber.org/zap v1.24.0 // indirect
 	go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
 	golang.org/x/crypto v0.3.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0
 	golang.org/x/time v0.3.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 5ffb1f8b3..380477913 100644
--- a/go.sum
+++ b/go.sum
@@ -415,10 +415,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
-github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
-github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY=
-github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI=
+github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0=
+github.com/onsi/ginkgo/v2 v2.8.4/go.mod h1:427dEDQZkDKsBvCjc2A/ZPefhKxsTTrsQegMlayL730=
+github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -682,8 +682,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -695,8 +695,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -767,13 +767,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -784,8 +784,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 6fdc937fe..c79ad1bf7 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -108,7 +108,7 @@ require (
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/onsi/gomega v1.27.2 // indirect
+	github.com/onsi/gomega v1.27.1 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
@@ -131,12 +131,12 @@ require (
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	golang.org/x/crypto v0.5.0 // indirect
 	golang.org/x/mod v0.8.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/oauth2 v0.6.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/oauth2 v0.5.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index da33661c1..e824e1b3c 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -425,9 +425,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
-github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY=
-github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI=
+github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0=
+github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
@@ -671,8 +671,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -684,8 +684,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -758,13 +758,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -775,8 +775,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/internal/controllers/machine/machine_controller_noderef.go b/internal/controllers/machine/machine_controller_noderef.go
index 20b83e39d..89ac0a2df 100644
--- a/internal/controllers/machine/machine_controller_noderef.go
+++ b/internal/controllers/machine/machine_controller_noderef.go
@@ -34,6 +34,7 @@ import (
 	"sigs.k8s.io/cluster-api/api/v1beta1/index"
 	"sigs.k8s.io/cluster-api/controllers/noderefutil"
 	"sigs.k8s.io/cluster-api/internal/util/ssa"
+	"sigs.k8s.io/cluster-api/internal/util/taints"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
@@ -130,6 +131,15 @@ func (r *Reconciler) reconcileNode(ctx context.Context, cluster *clusterv1.Clust
 	if err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "failed to apply labels to Node")
 	}
+	// Update `node` with the new version of the object.
+	if err := r.Client.Scheme().Convert(updatedNode, node, nil); err != nil {
+		return ctrl.Result{}, errors.Wrapf(err, "failed to convert node to structured object %s", klog.KObj(node))
+	}
+
+	// Reconcile node taints
+	if err := r.reconcileNodeTaints(ctx, remoteClient, node); err != nil {
+		return ctrl.Result{}, errors.Wrapf(err, "failed to reconcile taints on Node %s", klog.KObj(node))
+	}
 
 	// Do the remaining node health checks, then set the node health to true if all checks pass.
 	status, message := summarizeNodeConditions(node)
@@ -258,3 +268,17 @@ func (r *Reconciler) getNode(ctx context.Context, c client.Reader, providerID *n
 
 	return &nodeList.Items[0], nil
 }
+
+func (r *Reconciler) reconcileNodeTaints(ctx context.Context, remoteClient client.Client, node *corev1.Node) error {
+	patchHelper, err := patch.NewHelper(node, remoteClient)
+	if err != nil {
+		return errors.Wrapf(err, "failed to create patch helper for Node %s", klog.KObj(node))
+	}
+	// Drop the NodeUninitializedTaint taint on the node.
+	if taints.RemoveNodeTaint(node, clusterv1.NodeUninitializedTaint) {
+		if err := patchHelper.Patch(ctx, node); err != nil {
+			return errors.Wrapf(err, "failed to patch Node %s to modify taints", klog.KObj(node))
+		}
+	}
+	return nil
+}
diff --git a/internal/util/taints/taints.go b/internal/util/taints/taints.go
new file mode 100644
index 000000000..2d07ce422
--- /dev/null
+++ b/internal/util/taints/taints.go
@@ -0,0 +1,38 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package taints implements taint helper functions.
+package taints
+
+import (
+	corev1 "k8s.io/api/core/v1"
+)
+
+// RemoveNodeTaint drops the taint from the list of node taints.
+// It returns true if the taints are modified, false otherwise.
+func RemoveNodeTaint(node *corev1.Node, drop corev1.Taint) bool {
+	droppedTaint := false
+	taints := []corev1.Taint{}
+	for _, taint := range node.Spec.Taints {
+		if taint.MatchTaint(&drop) {
+			droppedTaint = true
+			continue
+		}
+		taints = append(taints, taint)
+	}
+	node.Spec.Taints = taints
+	return droppedTaint
+}


Test patch:
diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go
index 4038c3a16..54e9304ce 100644
--- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go
+++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller_test.go
@@ -24,6 +24,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/blang/semver"
 	ignition "github.com/flatcar/ignition/config/v2_3"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
@@ -2193,6 +2194,112 @@ func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) {
 	}
 }
 
+func TestAddNodeUninitializedTaint(t *testing.T) {
+	dummyTaint := corev1.Taint{
+		Key:    "dummy-taint",
+		Value:  "",
+		Effect: corev1.TaintEffectNoSchedule,
+	}
+
+	tests := []struct {
+		name              string
+		nodeRegistration  *bootstrapv1.NodeRegistrationOptions
+		kubernetesVersion semver.Version
+		isControlPlane    bool
+		wantTaints        []corev1.Taint
+	}{
+		{
+			name: "for control plane with version < v1.24.0, if taints is nil it should add the uninitialized and the master taint",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: nil,
+			},
+			kubernetesVersion: semver.MustParse("1.23.0"),
+			isControlPlane:    true,
+			wantTaints: []corev1.Taint{
+				oldControlPlaneTaint,
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			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",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: nil,
+			},
+			kubernetesVersion: semver.MustParse("1.24.5"),
+			isControlPlane:    true,
+			wantTaints: []corev1.Taint{
+				oldControlPlaneTaint,
+				controlPlaneTaint,
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			name: "for control plane with version >= v1.25.0, if taints is nil it should add the uninitialized and the control-plane taint",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: nil,
+			},
+			kubernetesVersion: semver.MustParse("1.25.0"),
+			isControlPlane:    true,
+			wantTaints: []corev1.Taint{
+				controlPlaneTaint,
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			name: "for control plane, if taints is not nil it should only add the uninitialized taint",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: []corev1.Taint{},
+			},
+			kubernetesVersion: semver.MustParse("1.26.0"),
+			isControlPlane:    true,
+			wantTaints: []corev1.Taint{
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			name: "for control plane, if it already has some taints it should add the uninitialized taint",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: []corev1.Taint{dummyTaint},
+			},
+			kubernetesVersion: semver.MustParse("1.26.0"),
+			isControlPlane:    true,
+			wantTaints: []corev1.Taint{
+				dummyTaint,
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			name:              "for worker, it should add the uninitialized taint",
+			nodeRegistration:  &bootstrapv1.NodeRegistrationOptions{},
+			kubernetesVersion: semver.MustParse("1.26.0"),
+			isControlPlane:    false,
+			wantTaints: []corev1.Taint{
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+		{
+			name: "for worker, if it already has some taints it should add the uninitialized taint",
+			nodeRegistration: &bootstrapv1.NodeRegistrationOptions{
+				Taints: []corev1.Taint{dummyTaint},
+			},
+			kubernetesVersion: semver.MustParse("1.26.0"),
+			isControlPlane:    false,
+			wantTaints: []corev1.Taint{
+				dummyTaint,
+				clusterv1.NodeUninitializedTaint,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			addNodeUninitializedTaint(tt.nodeRegistration, tt.isControlPlane, tt.kubernetesVersion)
+			g.Expect(tt.nodeRegistration.Taints).To(Equal(tt.wantTaints))
+		})
+	}
+}
+
 // test utils.
 
 // newWorkerMachineForCluster returns a Machine with the passed Cluster's information and a pre-configured name.
diff --git a/docs/book/src/developer/e2e.md b/docs/book/src/developer/e2e.md
index 590ec81dc..94762a200 100644
--- a/docs/book/src/developer/e2e.md
+++ b/docs/book/src/developer/e2e.md
@@ -18,9 +18,9 @@ test specs for the most common Cluster API use cases.
 
 Each E2E test requires a set of artifacts to be available:
 
-- Binaries & Docker images for Kubernetes, CNI, CRI & CSI
-- Manifests & Docker images for the Cluster API core components
-- Manifests & Docker images for the Cluster API infrastructure provider; in most cases
+- Binaries & docker images for Kubernetes, CNI, CRI & CSI
+- Manifests & docker images for the Cluster API core components
+- Manifests & docker images for the Cluster API infrastructure provider; in most cases
   machine images are also required (AMI, OVA etc.)
 - Credentials for the target infrastructure provider
 - Other support tools (e.g. kustomize, gsutil etc.)
diff --git a/docs/book/src/developer/testing.md b/docs/book/src/developer/testing.md
index 07de97689..1d60fec1c 100644
--- a/docs/book/src/developer/testing.md
+++ b/docs/book/src/developer/testing.md
@@ -193,11 +193,11 @@ kind images). This can be done by executing the `./scripts/ci-e2e.sh` script.
 GINKGO_FOCUS="\[PR-Blocking\]" ./scripts/ci-e2e.sh
 ```
 
-Now, the tests can be run in an IDE. The following describes how this can be done in IntelliJ IDEA and VS Code. It should work
+Now, the tests can be run in an IDE. The following describes how this can be done in Intellij IDEA and VS Code. It should work
 roughly the same way in all other IDEs. We assume the `cluster-api` repository has been checked
 out into `/home/user/code/src/sigs.k8s.io/cluster-api`.
 
-#### IntelliJ
+#### Intellij
 
 Create a new run configuration and fill in:
 * Test framework: `gotest`
@@ -555,8 +555,8 @@ In Cluster API Unit and integration test MUST use [go test].
 [Cluster API quick start]: ../user/quick-start.md
 [Cluster API test framework]: https://pkg.go.dev/sigs.k8s.io/cluster-api/test/framework?tab=doc
 [e2e development]: ./e2e.md
-[Ginkgo]: https://onsi.github.io/ginkgo/
-[Gomega]: https://onsi.github.io/gomega/
+[Ginkgo]: http://onsi.github.io/ginkgo/
+[Gomega]: http://onsi.github.io/gomega/
 [go test]: https://golang.org/pkg/testing/
 [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime
 [envtest]: https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/envtest
diff --git a/internal/controllers/machine/machine_controller_noderef_test.go b/internal/controllers/machine/machine_controller_noderef_test.go
index ede053801..f71d10972 100644
--- a/internal/controllers/machine/machine_controller_noderef_test.go
+++ b/internal/controllers/machine/machine_controller_noderef_test.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
@@ -459,3 +460,70 @@ func TestGetManagedLabels(t *testing.T) {
 	got := getManagedLabels(allLabels)
 	g.Expect(got).To(BeEquivalentTo(managedLabels))
 }
+
+func TestReconcileNodeTaints(t *testing.T) {
+	tests := []struct {
+		name string
+		node *corev1.Node
+	}{
+		{
+			name: "if the node has the uninitialized taint it should be dropped from the node",
+			node: &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-node-1",
+				},
+				Spec: corev1.NodeSpec{
+					Taints: []corev1.Taint{
+						clusterv1.NodeUninitializedTaint,
+					},
+				},
+			},
+		},
+		{
+			name: "if the node is a control plane and has the uninitialized taint it should be dropped from the node",
+			node: &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-node-1",
+				},
+				Spec: corev1.NodeSpec{
+					Taints: []corev1.Taint{
+						{
+							Key:    "node-role.kubernetes.io/control-plane",
+							Effect: corev1.TaintEffectNoSchedule,
+						},
+						clusterv1.NodeUninitializedTaint,
+					},
+				},
+			},
+		},
+		{
+			name: "if the node does not have the uninitialized taint it should remain absent from the node",
+			node: &corev1.Node{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "test-node-1",
+				},
+				Spec: corev1.NodeSpec{
+					Taints: []corev1.Taint{},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			fakeClient := fake.NewClientBuilder().WithObjects(tt.node).WithScheme(fakeScheme).Build()
+			nodeBefore := tt.node.DeepCopy()
+			r := &Reconciler{}
+			g.Expect(r.reconcileNodeTaints(ctx, fakeClient, tt.node)).Should(Succeed())
+			// Verify the NodeUninitializedTaint is dropped.
+			g.Expect(tt.node.Spec.Taints).ShouldNot(ContainElement(clusterv1.NodeUninitializedTaint))
+			// Verify all other taints are same.
+			for _, taint := range nodeBefore.Spec.Taints {
+				if !taint.MatchTaint(&clusterv1.NodeUninitializedTaint) {
+					g.Expect(tt.node.Spec.Taints).Should(ContainElement(taint))
+				}
+			}
+		})
+	}
+}
diff --git a/internal/controllers/machine/suite_test.go b/internal/controllers/machine/suite_test.go
index 3143e7699..d8439e520 100644
--- a/internal/controllers/machine/suite_test.go
+++ b/internal/controllers/machine/suite_test.go
@@ -27,6 +27,7 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/types"
 	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -55,6 +56,7 @@ func init() {
 	_ = clientgoscheme.AddToScheme(fakeScheme)
 	_ = clusterv1.AddToScheme(fakeScheme)
 	_ = apiextensionsv1.AddToScheme(fakeScheme)
+	_ = corev1.AddToScheme(fakeScheme)
 }
 
 func TestMain(m *testing.M) {
diff --git a/internal/util/taints/taints_test.go b/internal/util/taints/taints_test.go
new file mode 100644
index 000000000..f830c9573
--- /dev/null
+++ b/internal/util/taints/taints_test.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package taints
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func TestRemoveNodeTaint(t *testing.T) {
+	taint1 := corev1.Taint{Key: "taint1", Effect: corev1.TaintEffectNoSchedule}
+	taint2 := corev1.Taint{Key: "taint2", Effect: corev1.TaintEffectNoSchedule}
+
+	tests := []struct {
+		name         string
+		node         *corev1.Node
+		dropTaint    corev1.Taint
+		wantTaints   []corev1.Taint
+		wantModified bool
+	}{
+		{
+			name: "dropping taint from node should return true",
+			node: &corev1.Node{Spec: corev1.NodeSpec{
+				Taints: []corev1.Taint{
+					taint1,
+					taint2,
+				}}},
+			dropTaint:    taint1,
+			wantTaints:   []corev1.Taint{taint2},
+			wantModified: true,
+		},
+		{
+			name: "drop non-existing taint should return false",
+			node: &corev1.Node{Spec: corev1.NodeSpec{
+				Taints: []corev1.Taint{
+					taint2,
+				}}},
+			dropTaint:    taint1,
+			wantTaints:   []corev1.Taint{taint2},
+			wantModified: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			got := RemoveNodeTaint(tt.node, tt.dropTaint)
+			g.Expect(got).To(Equal(tt.wantModified))
+			g.Expect(tt.node.Spec.Taints).To(Equal(tt.wantTaints))
+		})
+	}
+}
diff --git a/test/go.mod b/test/go.mod
index 4b3bd0cf5..94150499e 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -10,8 +10,8 @@ require (
 	github.com/docker/go-connections v0.4.0
 	github.com/flatcar/ignition v0.36.2
 	github.com/go-logr/logr v1.2.3
-	github.com/onsi/ginkgo/v2 v2.9.0
-	github.com/onsi/gomega v1.27.2
+	github.com/onsi/ginkgo/v2 v2.8.4
+	github.com/onsi/gomega v1.27.1
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/pflag v1.0.5
 	github.com/vincent-petithory/dataurl v1.0.0
@@ -110,11 +110,11 @@ require (
 	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.24.0 // indirect
 	golang.org/x/crypto v0.3.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/oauth2 v0.6.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
+	golang.org/x/oauth2 v0.5.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/tools v0.6.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
diff --git a/test/go.sum b/test/go.sum
index 33a52d466..24b56791e 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -385,10 +385,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
-github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
-github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY=
-github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI=
+github.com/onsi/ginkgo/v2 v2.8.4 h1:gf5mIQ8cLFieruNLAdgijHF1PYfLphKm2dxxcUtcqK0=
+github.com/onsi/ginkgo/v2 v2.8.4/go.mod h1:427dEDQZkDKsBvCjc2A/ZPefhKxsTTrsQegMlayL730=
+github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
@@ -634,8 +634,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -647,8 +647,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
-golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
+golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -717,13 +717,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -734,8 +734,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=