Repository: kubernetes-sigs/cluster-api
Pull request #9161 (2023-08-10 06:30:14)

📖 book: Update versions doc adding Kubernetes 1.28 + prowjob documentation
<!-- Thanks for sending a pull request! Here are some tips for you:
    1. If this is your first time, please read our contributor guidelines: https://github.com/kubernetes-sigs/cluster-api/blob/main/CONTRIBUTING.md#contributing-a-patch and developer guide https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/developer/guide.md

    2. 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**:

Update book:
  
  * Update supported versions in `versions.md`
  * Update job documentation in `jobs.md`
  * Prior art: [🌱 Update versions doc adding Kubernetes 1.25 #7194](https://github.com/kubernetes-sigs/cluster-api/pull/7194) [📖 docs update jobs.md #7196](https://github.com/kubernetes-sigs/cluster-api/pull/7196)

TODO

- [ ] Wait for merge of https://github.com/kubernetes/test-infra/pull/30347

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Related to #8708

<!-- 
Please label this pull request according to what area(s) you are addressing. For reference on PR/issue labels, see: https://github.com/kubernetes-sigs/cluster-api/labels?q=area+

Area example:
/area runtime-sdk
-->

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 [oscr](https://github.com/oscr) 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:

- **[docs/OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/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":["oscr"]} -->
sbueringer: Btw I think /hold is enough you don't have to keep it in draft state. Makes it slightly clearer that the PR can already be reviewed + we can run tests

Patch:
diff --git a/.github/workflows/pr-dependabot.yaml b/.github/workflows/pr-dependabot.yaml
index 44c14bf6a..d3b2f1d63 100644
--- a/.github/workflows/pr-dependabot.yaml
+++ b/.github/workflows/pr-dependabot.yaml
@@ -24,7 +24,7 @@ jobs:
       id: vars
       run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
     - name: Set up Go
-      uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # tag=v4.1.0
+      uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # tag=v4.0.1
       with:
         go-version: ${{ steps.vars.outputs.go_version }}
     - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # tag=v3.3.1
diff --git a/.github/workflows/pr-golangci-lint.yaml b/.github/workflows/pr-golangci-lint.yaml
index 12b00a39d..b5dbbaa98 100644
--- a/.github/workflows/pr-golangci-lint.yaml
+++ b/.github/workflows/pr-golangci-lint.yaml
@@ -24,12 +24,11 @@ jobs:
         id: vars
         run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
       - name: Set up Go
-        uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # tag=v4.1.0
+        uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # tag=v4.0.1
         with:
           go-version: ${{ steps.vars.outputs.go_version }}
       - name: golangci-lint
         uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 # tag=v3.6.0
         with:
-          version: v1.54.1
-          args: --out-format=colored-line-number
+          version: v1.53.3
           working-directory: ${{matrix.working-directory}}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index c7eb22995..d76225d8b 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -23,7 +23,7 @@ jobs:
       - name: Calculate go version
         run: echo "go_version=$(make go-version)" >> $GITHUB_ENV
       - name: Set up Go
-        uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # tag=v4.1.0
+        uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # tag=v4.0.1
         with:
           go-version: ${{ env.go_version }}
       - name: generate release artifacts
diff --git a/.github/workflows/weekly-security-scan.yaml b/.github/workflows/weekly-image-scan.yaml
similarity index 78%
rename from .github/workflows/weekly-security-scan.yaml
rename to .github/workflows/weekly-image-scan.yaml
index ada88d79e..cac026712 100644
--- a/.github/workflows/weekly-security-scan.yaml
+++ b/.github/workflows/weekly-image-scan.yaml
@@ -1,4 +1,4 @@
-name: Weekly security scan
+name: Weekly image scan
 
 on:
   schedule:
@@ -25,8 +25,8 @@ jobs:
       id: vars
       run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
     - name: Set up Go
-      uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # tag=v4.1.0
+      uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # tag=v4.0.1
       with:
         go-version: ${{ steps.vars.outputs.go_version }}
-    - name: Run verify security target
-      run: make verify-security
+    - name: Run verify container script
+      run: make verify-container-images
diff --git a/Makefile b/Makefile
index 1146a456e..72fdaa32b 100644
--- a/Makefile
+++ b/Makefile
@@ -164,11 +164,6 @@ GOLANGCI_LINT_VER := $(shell cat .github/workflows/pr-golangci-lint.yaml | grep
 GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN)-$(GOLANGCI_LINT_VER))
 GOLANGCI_LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
 
-GOVULNCHECK_BIN := govulncheck
-GOVULNCHECK_VER := v1.0.0
-GOVULNCHECK := $(abspath $(TOOLS_BIN_DIR)/$(GOVULNCHECK_BIN)-$(GOVULNCHECK_VER))
-GOVULNCHECK_PKG := golang.org/x/vuln/cmd/govulncheck
-
 CONVERSION_VERIFIER_BIN := conversion-verifier
 CONVERSION_VERIFIER := $(abspath $(TOOLS_BIN_DIR)/$(CONVERSION_VERIFIER_BIN))
 
@@ -228,7 +223,7 @@ CAPI_KIND_CLUSTER_NAME ?= capi-test
 
 TAG ?= dev
 ARCH ?= $(shell go env GOARCH)
-ALL_ARCH ?= amd64 arm arm64 ppc64le s390x
+ALL_ARCH = amd64 arm arm64 ppc64le s390x
 
 # Allow overriding the imagePullPolicy
 PULL_POLICY ?= Always
@@ -513,7 +508,7 @@ generate-doctoc:
 	TRACE=$(TRACE) ./hack/generate-doctoc.sh
 
 .PHONY: generate-e2e-templates
-generate-e2e-templates: $(KUSTOMIZE) $(addprefix generate-e2e-templates-, v1.0 v1.4 v1.5 main) ## Generate cluster templates for all versions
+generate-e2e-templates: $(KUSTOMIZE) $(addprefix generate-e2e-templates-, v1.0 v1.3 v1.4 main) ## Generate cluster templates for all versions
 
 DOCKER_TEMPLATES := test/e2e/data/infrastructure-docker
 INMEMORY_TEMPLATES := test/e2e/data/infrastructure-inmemory
@@ -522,16 +517,16 @@ INMEMORY_TEMPLATES := test/e2e/data/infrastructure-inmemory
 generate-e2e-templates-v1.0: $(KUSTOMIZE)
 	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.0/cluster-template --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.0/cluster-template.yaml
 
+.PHONY: generate-e2e-templates-v1.3
+generate-e2e-templates-v1.3: $(KUSTOMIZE)
+	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.3/cluster-template --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.3/cluster-template.yaml
+	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.3/cluster-template-topology --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.3/cluster-template-topology.yaml
+
 .PHONY: generate-e2e-templates-v1.4
 generate-e2e-templates-v1.4: $(KUSTOMIZE)
 	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.4/cluster-template --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.4/cluster-template.yaml
 	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.4/cluster-template-topology --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.4/cluster-template-topology.yaml
 
-.PHONY: generate-e2e-templates-v1.5
-generate-e2e-templates-v1.5: $(KUSTOMIZE)
-	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.5/cluster-template --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.5/cluster-template.yaml
-	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/v1.5/cluster-template-topology --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/v1.5/cluster-template-topology.yaml
-
 .PHONY: generate-e2e-templates-main
 generate-e2e-templates-main: $(KUSTOMIZE)
 	$(KUSTOMIZE) build $(DOCKER_TEMPLATES)/main/cluster-template --load-restrictor LoadRestrictionsNone > $(DOCKER_TEMPLATES)/main/cluster-template.yaml
@@ -605,7 +600,7 @@ APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main)
 apidiff: $(GO_APIDIFF) ## Check for API differences
 	$(GO_APIDIFF) $(APIDIFF_OLD_COMMIT) --print-compatible
 
-ALL_VERIFY_CHECKS = boilerplate shellcheck tiltfile modules gen conversions doctoc capi-book-summary
+ALL_VERIFY_CHECKS = doctoc boilerplate shellcheck tiltfile modules gen conversions doctoc capi-book-summary
 
 .PHONY: verify
 verify: $(addprefix verify-,$(ALL_VERIFY_CHECKS)) lint-dockerfiles ## Run all verify-* targets
@@ -659,24 +654,6 @@ verify-tiltfile: ## Verify Tiltfile format
 verify-container-images: ## Verify container images
 	TRACE=$(TRACE) ./hack/verify-container-images.sh
 
-.PHONY: verify-govulncheck
-verify-govulncheck: $(GOVULNCHECK) ## Verify code for vulnerabilities
-	$(GOVULNCHECK) ./... && R1=$$? || R1=$$?; \
-	$(GOVULNCHECK) -C "$(TOOLS_DIR)" ./... && R2=$$? || R2=$$?; \
-	$(GOVULNCHECK) -C "$(TEST_DIR)" ./... && R3=$$? || R3=$$?; \
-	if [ "$$R1" -ne "0" ] || [ "$$R2" -ne "0" ] || [ "$$R3" -ne "0" ]; then \
-		exit 1; \
-	fi
-
-.PHONY: verify-security
-verify-security: ## Verify code and images for vulnerabilities
-	$(MAKE) verify-container-images && R1=$$? || R1=$$?; \
-	$(MAKE) verify-govulncheck && R2=$$? || R2=$$?; \
-	if [ "$$R1" -ne "0" ] || [ "$$R2" -ne "0" ]; then \
-	  echo "Check for vulnerabilities failed! There are vulnerabilities to be fixed"; \
-		exit 1; \
-	fi
-
 ## --------------------------------------
 ## Binaries
 ## --------------------------------------
@@ -724,8 +701,7 @@ docker-build-all: $(addprefix docker-build-,$(ALL_ARCH)) ## Build docker images
 docker-build-%:
 	$(MAKE) ARCH=$* docker-build
 
-# Choice of images to build/push
-ALL_DOCKER_BUILD ?= core kubeadm-bootstrap kubeadm-control-plane docker-infrastructure in-memory-infrastructure test-extension clusterctl
+ALL_DOCKER_BUILD = core kubeadm-bootstrap kubeadm-control-plane docker-infrastructure in-memory-infrastructure test-extension clusterctl
 
 .PHONY: docker-build
 docker-build: docker-pull-prerequisites ## Run docker-build-* targets for all the images
@@ -1072,17 +1048,26 @@ promote-images: $(KPROMO)
 
 .PHONY: docker-push-all
 docker-push-all: $(addprefix docker-push-,$(ALL_ARCH))  ## Push the docker images to be included in the release for all architectures + related multiarch manifests
-	$(MAKE) ALL_ARCH="$(ALL_ARCH)" $(addprefix docker-push-manifest-,$(ALL_DOCKER_BUILD))
+	$(MAKE) docker-push-manifest-core
+	$(MAKE) docker-push-manifest-kubeadm-bootstrap
+	$(MAKE) docker-push-manifest-kubeadm-control-plane
+	$(MAKE) docker-push-manifest-docker-infrastructure
+	$(MAKE) docker-push-manifest-in-memory-infrastructure
+	$(MAKE) docker-push-manifest-test-extension
+	$(MAKE) docker-push-clusterctl
 
 docker-push-%:
 	$(MAKE) ARCH=$* docker-push
 
 .PHONY: docker-push
-docker-push: $(addprefix docker-push-,$(ALL_DOCKER_BUILD)) ## Push the docker images to be included in the release
-
-.PHONY: docker-push-core
-docker-push-core: ## Push the core docker image
+docker-push: ## Push the docker images to be included in the release
 	docker push $(CONTROLLER_IMG)-$(ARCH):$(TAG)
+	docker push $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG)-$(ARCH):$(TAG)
+	docker push $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG)-$(ARCH):$(TAG)
+	docker push $(CLUSTERCTL_IMG)-$(ARCH):$(TAG)
+	docker push $(CAPD_CONTROLLER_IMG)-$(ARCH):$(TAG)
+	docker push $(CAPIM_CONTROLLER_IMG)-$(ARCH):$(TAG)
+	docker push $(TEST_EXTENSION_IMG)-$(ARCH):$(TAG)
 
 .PHONY: docker-push-manifest-core
 docker-push-manifest-core: ## Push the multiarch manifest for the core docker images
@@ -1092,10 +1077,6 @@ docker-push-manifest-core: ## Push the multiarch manifest for the core docker im
 	$(MAKE) set-manifest-image MANIFEST_IMG=$(CONTROLLER_IMG) MANIFEST_TAG=$(TAG) TARGET_RESOURCE="./config/default/manager_image_patch.yaml"
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="./config/default/manager_pull_policy.yaml"
 
-.PHONY: docker-push-kubeadm-bootstrap
-docker-push-kubeadm-bootstrap: ## Push the kubeadm bootstrap docker image
-	docker push $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG)-$(ARCH):$(TAG)
-
 .PHONY: docker-push-manifest-kubeadm-bootstrap
 docker-push-manifest-kubeadm-bootstrap: ## Push the multiarch manifest for the kubeadm bootstrap docker images
 	docker manifest create --amend $(KUBEADM_BOOTSTRAP_CONTROLLER_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(KUBEADM_BOOTSTRAP_CONTROLLER_IMG)\-&:$(TAG)~g")
@@ -1104,10 +1085,6 @@ docker-push-manifest-kubeadm-bootstrap: ## Push the multiarch manifest for the k
 	$(MAKE) set-manifest-image MANIFEST_IMG=$(KUBEADM_BOOTSTRAP_CONTROLLER_IMG) MANIFEST_TAG=$(TAG) TARGET_RESOURCE="./bootstrap/kubeadm/config/default/manager_image_patch.yaml"
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="./bootstrap/kubeadm/config/default/manager_pull_policy.yaml"
 
-.PHONY: docker-push-kubeadm-control-plane
-docker-push-kubeadm-control-plane: ## Push the kubeadm control plane docker image
-	docker push $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG)-$(ARCH):$(TAG)
-
 .PHONY: docker-push-manifest-kubeadm-control-plane
 docker-push-manifest-kubeadm-control-plane: ## Push the multiarch manifest for the kubeadm control plane docker images
 	docker manifest create --amend $(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG)\-&:$(TAG)~g")
@@ -1116,10 +1093,6 @@ docker-push-manifest-kubeadm-control-plane: ## Push the multiarch manifest for t
 	$(MAKE) set-manifest-image MANIFEST_IMG=$(KUBEADM_CONTROL_PLANE_CONTROLLER_IMG) MANIFEST_TAG=$(TAG) TARGET_RESOURCE="./controlplane/kubeadm/config/default/manager_image_patch.yaml"
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="./controlplane/kubeadm/config/default/manager_pull_policy.yaml"
 
-.PHONY: docker-push-docker-infrastructure
-docker-push-docker-infrastructure: ## Push the docker infrastructure provider image
-	docker push $(CAPD_CONTROLLER_IMG)-$(ARCH):$(TAG)
-
 .PHONY: docker-push-manifest-docker-infrastructure
 docker-push-manifest-docker-infrastructure: ## Push the multiarch manifest for the docker infrastructure provider images
 	docker manifest create --amend $(CAPD_CONTROLLER_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(CAPD_CONTROLLER_IMG)\-&:$(TAG)~g")
@@ -1128,10 +1101,6 @@ docker-push-manifest-docker-infrastructure: ## Push the multiarch manifest for t
 	$(MAKE) set-manifest-image MANIFEST_IMG=$(CAPD_CONTROLLER_IMG) MANIFEST_TAG=$(TAG) TARGET_RESOURCE="$(CAPD_DIR)/config/default/manager_image_patch.yaml"
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="$(CAPD_DIR)/config/default/manager_pull_policy.yaml"
 
-.PHONY: docker-push-in-memory-infrastructure
-docker-push-in-memory-infrastructure: ## Push the in-memory infrastructure provider image
-	docker push $(CAPIM_CONTROLLER_IMG)-$(ARCH):$(TAG)
-
 .PHONY: docker-push-manifest-in-memory-infrastructure
 docker-push-manifest-in-memory-infrastructure: ## Push the multiarch manifest for the in-memory infrastructure provider images
 	docker manifest create --amend $(CAPIM_CONTROLLER_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(CAPIM_CONTROLLER_IMG)\-&:$(TAG)~g")
@@ -1140,10 +1109,6 @@ docker-push-manifest-in-memory-infrastructure: ## Push the multiarch manifest fo
 	$(MAKE) set-manifest-image MANIFEST_IMG=$(CAPIM_CONTROLLER_IMG) MANIFEST_TAG=$(TAG) TARGET_RESOURCE="$(CAPIM_DIR)/config/default/manager_image_patch.yaml"
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="$(CAPIM_DIR)/config/default/manager_pull_policy.yaml"
 
-.PHONY: docker-push-test-extension
-docker-push-test-extension: ## Push the test extension provider image
-	docker push $(TEST_EXTENSION_IMG)-$(ARCH):$(TAG)
-
 .PHONY: docker-push-manifest-test-extension
 docker-push-manifest-test-extension: ## Push the multiarch manifest for the test extension provider images
 	docker manifest create --amend $(TEST_EXTENSION_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(TEST_EXTENSION_IMG)\-&:$(TAG)~g")
@@ -1153,11 +1118,7 @@ docker-push-manifest-test-extension: ## Push the multiarch manifest for the test
 	$(MAKE) set-manifest-pull-policy TARGET_RESOURCE="./test/extension/config/default/manager_pull_policy.yaml"
 
 .PHONY: docker-push-clusterctl
-docker-push-clusterctl: ## Push the clusterctl image
-	docker push $(CLUSTERCTL_IMG)-$(ARCH):$(TAG)
-
-.PHONY: docker-push-manifest-clusterctl
-docker-push-manifest-clusterctl: ## Push the multiarch manifest for the clusterctl images
+docker-push-clusterctl: ## Push the clusterctl images
 	docker manifest create --amend $(CLUSTERCTL_IMG):$(TAG) $(shell echo $(ALL_ARCH) | sed -e "s~[^ ]*~$(CLUSTERCTL_IMG)\-&:$(TAG)~g")
 	@for arch in $(ALL_ARCH); do docker manifest annotate --arch $${arch} ${CLUSTERCTL_IMG}:${TAG} ${CLUSTERCTL_IMG}-$${arch}:${TAG}; done
 	docker manifest push --purge $(CLUSTERCTL_IMG):$(TAG)
@@ -1289,9 +1250,6 @@ $(GINKGO_BIN): $(GINKGO) ## Build a local copy of ginkgo.
 .PHONY: $(GOLANGCI_LINT_BIN)
 $(GOLANGCI_LINT_BIN): $(GOLANGCI_LINT) ## Build a local copy of golangci-lint.
 
-.PHONY: $(GOVULNCHECK_BIN)
-$(GOVULNCHECK_BIN): $(GOVULNCHECK) ## Build a local copy of govulncheck.
-
 $(CONTROLLER_GEN): # Build controller-gen from tools folder.
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(CONTROLLER_GEN_PKG) $(CONTROLLER_GEN_BIN) $(CONTROLLER_GEN_VER)
 
@@ -1329,7 +1287,7 @@ $(SETUP_ENVTEST): # Build setup-envtest from tools folder.
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(SETUP_ENVTEST_PKG) $(SETUP_ENVTEST_BIN) $(SETUP_ENVTEST_VER)
 
 $(TILT_PREPARE): $(TOOLS_DIR)/go.mod # Build tilt-prepare from tools folder.
-	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/tilt-prepare sigs.k8s.io/cluster-api/hack/tools/internal/tilt-prepare
+	cd $(TOOLS_DIR); go build -tags=tools -o $(BIN_DIR)/tilt-prepare sigs.k8s.io/cluster-api/hack/tools/tilt-prepare
 
 $(KPROMO):
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(KPROMO_PKG) $(KPROMO_BIN) ${KPROMO_VER}
@@ -1343,9 +1301,6 @@ $(GINKGO): # Build ginkgo from tools folder.
 $(GOLANGCI_LINT): # Build golangci-lint from tools folder.
 	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOLANGCI_LINT_PKG) $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER)
 
-$(GOVULNCHECK): # Build govulncheck.
-	GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(GOVULNCHECK_PKG) $(GOVULNCHECK_BIN) $(GOVULNCHECK_VER)
-
 ## --------------------------------------
 ## Helpers
 ## --------------------------------------
diff --git a/Tiltfile b/Tiltfile
index e93549889..cd00fb1c0 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -431,7 +431,7 @@ def deploy_observability():
 
         cmd_button(
             "loki:import logs",
-            argv = ["sh", "-c", "cd ./hack/tools/internal/log-push && go run ./main.go --log-path=$LOG_PATH"],
+            argv = ["sh", "-c", "cd ./hack/tools/log-push && go run ./main.go --log-path=$LOG_PATH"],
             resource = "loki",
             icon_name = "import_export",
             text = "Import logs",
diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
index dd0d572f2..18e1fce57 100644
--- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
+++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
@@ -22,7 +22,7 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
diff --git a/bootstrap/kubeadm/types/utils.go b/bootstrap/kubeadm/types/utils.go
index 7372d9378..0ee8c23bb 100644
--- a/bootstrap/kubeadm/types/utils.go
+++ b/bootstrap/kubeadm/types/utils.go
@@ -18,7 +18,7 @@ limitations under the License.
 package utils
 
 import (
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/cmd/clusterctl/api/v1alpha3/metadata_type.go b/cmd/clusterctl/api/v1alpha3/metadata_type.go
index 0009c160b..0eacecbf4 100644
--- a/cmd/clusterctl/api/v1alpha3/metadata_type.go
+++ b/cmd/clusterctl/api/v1alpha3/metadata_type.go
@@ -17,7 +17,7 @@ limitations under the License.
 package v1alpha3
 
 import (
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/version"
 )
diff --git a/cmd/clusterctl/client/cluster/cert_manager.go b/cmd/clusterctl/client/cluster/cert_manager.go
index 2c73c5d4b..954c401b9 100644
--- a/cmd/clusterctl/client/cluster/cert_manager.go
+++ b/cmd/clusterctl/client/cluster/cert_manager.go
@@ -21,7 +21,7 @@ import (
 	_ "embed"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/cmd/clusterctl/client/config/cert_manager_client.go b/cmd/clusterctl/client/config/cert_manager_client.go
index 5ea44244a..ea3858c27 100644
--- a/cmd/clusterctl/client/config/cert_manager_client.go
+++ b/cmd/clusterctl/client/config/cert_manager_client.go
@@ -29,7 +29,7 @@ const (
 	CertManagerConfigKey = "cert-manager"
 
 	// CertManagerDefaultVersion defines the default cert-manager version to be used by clusterctl.
-	CertManagerDefaultVersion = "v1.12.3"
+	CertManagerDefaultVersion = "v1.12.2"
 
 	// CertManagerDefaultURL defines the default cert-manager repository url to be used by clusterctl.
 	// NOTE: At runtime CertManagerDefaultVersion may be replaced with the
diff --git a/cmd/clusterctl/client/repository/repository_github.go b/cmd/clusterctl/client/repository/repository_github.go
index ab51f41fb..5cddd0128 100644
--- a/cmd/clusterctl/client/repository/repository_github.go
+++ b/cmd/clusterctl/client/repository/repository_github.go
@@ -28,7 +28,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-github/v53/github"
 	"github.com/pkg/errors"
 	"golang.org/x/oauth2"
diff --git a/cmd/clusterctl/cmd/version_checker.go b/cmd/clusterctl/cmd/version_checker.go
index f0334225c..c195a5eb0 100644
--- a/cmd/clusterctl/cmd/version_checker.go
+++ b/cmd/clusterctl/cmd/version_checker.go
@@ -26,7 +26,7 @@ import (
 	"time"
 
 	"github.com/adrg/xdg"
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-github/v53/github"
 	"github.com/pkg/errors"
 	"golang.org/x/oauth2"
diff --git a/cmd/clusterctl/hack/create-local-repository.py b/cmd/clusterctl/hack/create-local-repository.py
index 41bc8403a..c99d5753f 100755
--- a/cmd/clusterctl/hack/create-local-repository.py
+++ b/cmd/clusterctl/hack/create-local-repository.py
@@ -52,36 +52,36 @@ settings = {}
 providers = {
     'cluster-api': {
         'componentsFile': 'core-components.yaml',
-        'nextVersion': 'v1.6.99',
+        'nextVersion': 'v1.5.99',
         'type': 'CoreProvider',
     },
     'bootstrap-kubeadm': {
         'componentsFile': 'bootstrap-components.yaml',
-        'nextVersion': 'v1.6.99',
+        'nextVersion': 'v1.5.99',
         'type': 'BootstrapProvider',
         'configFolder': 'bootstrap/kubeadm/config/default',
     },
     'control-plane-kubeadm': {
         'componentsFile': 'control-plane-components.yaml',
-        'nextVersion': 'v1.6.99',
+        'nextVersion': 'v1.5.99',
         'type': 'ControlPlaneProvider',
         'configFolder': 'controlplane/kubeadm/config/default',
     },
     'infrastructure-docker': {
         'componentsFile': 'infrastructure-components.yaml',
-        'nextVersion': 'v1.6.99',
+        'nextVersion': 'v1.5.99',
         'type': 'InfrastructureProvider',
         'configFolder': 'test/infrastructure/docker/config/default',
     },
     'infrastructure-in-memory': {
           'componentsFile': 'infrastructure-components.yaml',
-          'nextVersion': 'v1.6.99',
+          'nextVersion': 'v1.5.99',
           'type': 'InfrastructureProvider',
           'configFolder': 'test/infrastructure/inmemory/config/default',
       },
       'runtime-extension-test': {
         'componentsFile': 'runtime-extension-components.yaml',
-        'nextVersion': 'v1.6.99',
+        'nextVersion': 'v1.5.99',
         'type': 'RuntimeExtensionProvider',
         'configFolder': 'test/extension/config/default',
     },
diff --git a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go
index 84b55b454..9db8198d2 100644
--- a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go
+++ b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook.go
@@ -21,7 +21,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/coredns/corefile-migration/migration"
 	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/pkg/errors"
@@ -140,7 +140,6 @@ func (in *KubeadmControlPlane) ValidateUpdate(old runtime.Object) (admission.War
 	// For example, {"spec", "*"} will allow any path under "spec" to change.
 	allowedPaths := [][]string{
 		{"metadata", "*"},
-		{spec, kubeadmConfigSpec, "useExperimentalRetryJoin"},
 		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageRepository"},
 		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageTag"},
 		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "extraArgs"},
diff --git a/controlplane/kubeadm/internal/controllers/controller.go b/controlplane/kubeadm/internal/controllers/controller.go
index f71bdec07..73c2ae7b9 100644
--- a/controlplane/kubeadm/internal/controllers/controller.go
+++ b/controlplane/kubeadm/internal/controllers/controller.go
@@ -22,7 +22,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -85,6 +85,12 @@ type KubeadmControlPlaneReconciler struct {
 
 	managementCluster         internal.ManagementCluster
 	managementClusterUncached internal.ManagementCluster
+
+	// disableInPlacePropagation should only be used for tests. This is used to skip
+	// some parts of the controller that need SSA as the current test setup does not
+	// support SSA. This flag should be dropped after all affected tests are migrated
+	// to envtest.
+	disableInPlacePropagation bool
 	ssaCache                  ssa.Cache
 }
 
@@ -369,8 +375,10 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, controlPl
 		return result, err
 	}
 
-	if err := r.syncMachines(ctx, controlPlane); err != nil {
-		return ctrl.Result{}, errors.Wrap(err, "failed to sync Machines")
+	if !r.disableInPlacePropagation {
+		if err := r.syncMachines(ctx, controlPlane); err != nil {
+			return ctrl.Result{}, errors.Wrap(err, "failed to sync Machines")
+		}
 	}
 
 	// Aggregate the operational state of all the machines; while aggregating we are adding the
diff --git a/controlplane/kubeadm/internal/controllers/remediation.go b/controlplane/kubeadm/internal/controllers/remediation.go
index 698612437..d1c8fd44e 100644
--- a/controlplane/kubeadm/internal/controllers/remediation.go
+++ b/controlplane/kubeadm/internal/controllers/remediation.go
@@ -22,7 +22,7 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/controlplane/kubeadm/internal/controllers/scale.go b/controlplane/kubeadm/internal/controllers/scale.go
index 66608e60a..4ed1682c2 100644
--- a/controlplane/kubeadm/internal/controllers/scale.go
+++ b/controlplane/kubeadm/internal/controllers/scale.go
@@ -20,7 +20,7 @@ import (
 	"context"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/controlplane/kubeadm/internal/controllers/upgrade.go b/controlplane/kubeadm/internal/controllers/upgrade.go
index f51bce9d8..b1528283d 100644
--- a/controlplane/kubeadm/internal/controllers/upgrade.go
+++ b/controlplane/kubeadm/internal/controllers/upgrade.go
@@ -19,7 +19,7 @@ package controllers
 import (
 	"context"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	ctrl "sigs.k8s.io/controller-runtime"
 
diff --git a/controlplane/kubeadm/internal/workload_cluster.go b/controlplane/kubeadm/internal/workload_cluster.go
index 9d7de352b..4112106f5 100644
--- a/controlplane/kubeadm/internal/workload_cluster.go
+++ b/controlplane/kubeadm/internal/workload_cluster.go
@@ -29,7 +29,7 @@ import (
 	"reflect"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
diff --git a/controlplane/kubeadm/internal/workload_cluster_coredns.go b/controlplane/kubeadm/internal/workload_cluster_coredns.go
index 5699c9c06..9ebf7eda9 100644
--- a/controlplane/kubeadm/internal/workload_cluster_coredns.go
+++ b/controlplane/kubeadm/internal/workload_cluster_coredns.go
@@ -22,7 +22,7 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/coredns/corefile-migration/migration"
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
diff --git a/controlplane/kubeadm/internal/workload_cluster_etcd.go b/controlplane/kubeadm/internal/workload_cluster_etcd.go
index bb4c4d417..3b3662a29 100644
--- a/controlplane/kubeadm/internal/workload_cluster_etcd.go
+++ b/controlplane/kubeadm/internal/workload_cluster_etcd.go
@@ -19,7 +19,7 @@ package internal
 import (
 	"context"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	kerrors "k8s.io/apimachinery/pkg/util/errors"
 
diff --git a/controlplane/kubeadm/internal/workload_cluster_rbac.go b/controlplane/kubeadm/internal/workload_cluster_rbac.go
index eb22e3480..d714c6329 100644
--- a/controlplane/kubeadm/internal/workload_cluster_rbac.go
+++ b/controlplane/kubeadm/internal/workload_cluster_rbac.go
@@ -20,7 +20,7 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/docs/book/src/clusterctl/commands/init.md b/docs/book/src/clusterctl/commands/init.md
index 34479acef..73092f46c 100644
--- a/docs/book/src/clusterctl/commands/init.md
+++ b/docs/book/src/clusterctl/commands/init.md
@@ -190,7 +190,7 @@ If this happens, there are no guarantees about the proper functioning of `cluste
 Cluster API providers require a cert-manager version supporting the `cert-manager.io/v1` API to be installed in the cluster.
 
 While doing init, clusterctl checks if there is a version of cert-manager already installed. If not, clusterctl will
-install a default version (currently cert-manager v1.12.3). See [clusterctl configuration](../configuration.md) for
+install a default version (currently cert-manager v1.12.2). See [clusterctl configuration](../configuration.md) for
 available options to customize this operation.
 
 <aside class="note warning">
diff --git a/docs/book/src/developer/guide.md b/docs/book/src/developer/guide.md
index 350f6148a..3c055812b 100644
--- a/docs/book/src/developer/guide.md
+++ b/docs/book/src/developer/guide.md
@@ -81,7 +81,7 @@ The generated binary can be found at ./hack/tools/bin/envsubst
 You'll need to deploy [cert-manager] components on your [management cluster][mcluster], using `kubectl`
 
 ```bash
-kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.3/cert-manager.yaml
+kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.2/cert-manager.yaml
 ```
 
 Ensure the cert-manager webhook service is ready before creating the Cluster API components.
diff --git a/docs/book/src/developer/providers/version-migration.md b/docs/book/src/developer/providers/version-migration.md
index 4f970fe00..dcfb364dd 100644
--- a/docs/book/src/developer/providers/version-migration.md
+++ b/docs/book/src/developer/providers/version-migration.md
@@ -9,4 +9,5 @@ maintainers of other providers and consumers of the Go API in upgrading from one
 - [v1.1 to v1.2](migrations/v1.1-to-v1.2.md)
 - [v1.2 to v1.3](migrations/v1.2-to-v1.3.md)
 - [v1.3 to v1.4](migrations/v1.3-to-v1.4.md)
-- [v1.4 to v1.5](migrations/v1.4-to-v1.5.md)
\ No newline at end of file
+- [v1.4 to v1.5](migrations/v1.4-to-v1.5.md)
+- [v1.5 to v1.6](migrations/v1.5-to-v1.6.md)
diff --git a/docs/book/src/developer/tilt.md b/docs/book/src/developer/tilt.md
index 8aa1c2f96..61b51b612 100644
--- a/docs/book/src/developer/tilt.md
+++ b/docs/book/src/developer/tilt.md
@@ -489,4 +489,4 @@ syntax highlighting and auto-formatting. To enable it for Tiltfile a file associ
 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/internal/tilt-prepare` command, the image build is running the `podman build`/`podman push` commands.
+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/jobs.md b/docs/book/src/reference/jobs.md
index ba4bb9ca4..a9a0957aa 100644
--- a/docs/book/src/reference/jobs.md
+++ b/docs/book/src/reference/jobs.md
@@ -32,7 +32,7 @@ Prow Presubmits:
     * GINKGO_SKIP: `[PR-Blocking] [Conformance] [K8s-Upgrade]`
   * [pull-cluster-api-e2e-full-main] `./scripts/ci-e2e.sh`
     * GINKGO_SKIP: `[PR-Blocking] [Conformance] [K8s-Upgrade]|[IPv6]`
-  * [pull-cluster-api-e2e-workload-upgrade-1-27-latest-main] `./scripts/ci-e2e.sh` FROM: `stable-1.27` TO: `ci/latest-1.28`
+  * [pull-cluster-api-e2e-workload-upgrade-1-28-latest-main] `./scripts/ci-e2e.sh` FROM: `stable-1.28` TO: `ci/latest-1.29`
     * GINKGO_FOCUS: `[K8s-Upgrade]`
   * [pull-cluster-api-e2e-scale-main-experimental] `./scripts/ci-e2e-scale.sh`
 
@@ -78,8 +78,6 @@ Prow Periodics:
 * [periodic-cluster-api-e2e-dualstack-and-ipv6-main] `./scripts/ci-e2e.sh`
   * DOCKER_IN_DOCKER_IPV6_ENABLED: `true`
   * GINKGO_SKIP: `[Conformance] [K8s-Upgrade]`
-* [periodic-cluster-api-e2e-workload-upgrade-1-22-1-23-main] `./scripts/ci-e2e.sh` FROM: `stable-1.22` TO: `stable-1.23`
-  * GINKGO_FOCUS: `[K8s-Upgrade]`
 * [periodic-cluster-api-e2e-workload-upgrade-1-23-1-24-main] `./scripts/ci-e2e.sh` FROM: `stable-1.23` TO: `stable-1.24`
   * GINKGO_FOCUS: `[K8s-Upgrade]`
 * [periodic-cluster-api-e2e-workload-upgrade-1-24-1-25-main] `./scripts/ci-e2e.sh` FROM: `stable-1.24` TO: `stable-1.25`
@@ -88,7 +86,9 @@ Prow Periodics:
   * GINKGO_FOCUS: `[K8s-Upgrade]`
 * [periodic-cluster-api-e2e-workload-upgrade-1-26-1-27-main] `./scripts/ci-e2e.sh` FROM: `stable-1.26` TO: `stable-1.27`
   * GINKGO_FOCUS: `[K8s-Upgrade]`
-* [periodic-cluster-api-e2e-workload-upgrade-1-27-latest-main] `./scripts/ci-e2e.sh` FROM: `stable-1.27` TO: `ci/latest-1.28`
+* [periodic-cluster-api-e2e-workload-upgrade-1-27-1-28-main] `./scripts/ci-e2e.sh` FROM: `stable-1.27` TO: `stable-1.28`
+  * GINKGO_FOCUS: `[K8s-Upgrade]`
+* [periodic-cluster-api-e2e-workload-upgrade-1-28-latest-main] `./scripts/ci-e2e.sh` FROM: `stable-1.28` TO: `ci/latest-1.29`
   * GINKGO_FOCUS: `[K8s-Upgrade]`
 * [cluster-api-push-images-nightly] Google Cloud Build: `make release-staging-nightly`
 
@@ -125,18 +125,18 @@ Prow Periodics:
 [pull-cluster-api-e2e-informing-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-informing-main
 [pull-cluster-api-e2e-full-dualstack-and-ipv6-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-full-dualstack-and-ipv6-main
 [pull-cluster-api-e2e-full-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-full-main
-[pull-cluster-api-e2e-workload-upgrade-1-27-latest-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-main-1-27-latest
+[pull-cluster-api-e2e-workload-upgrade-1-28-latest-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-main-1-28-latest
 [pull-cluster-api-e2e-scale-main-experimental]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-pr-e2e-scale-main-experimental
 [periodic-cluster-api-test-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-test-main
 [periodic-cluster-api-test-mink8s-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-test-mink8s-main
 [periodic-cluster-api-e2e-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main
 [periodic-cluster-api-e2e-mink8s-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-mink8s-main
 [periodic-cluster-api-e2e-dualstack-and-ipv6-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-dualstack-and-ipv6-main
-[periodic-cluster-api-e2e-workload-upgrade-1-22-1-23-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-22-1-23
 [periodic-cluster-api-e2e-workload-upgrade-1-23-1-24-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-23-1-24
 [periodic-cluster-api-e2e-workload-upgrade-1-24-1-25-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-24-1-25
 [periodic-cluster-api-e2e-workload-upgrade-1-25-1-26-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-25-1-26
 [periodic-cluster-api-e2e-workload-upgrade-1-26-1-27-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-26-1-27
-[periodic-cluster-api-e2e-workload-upgrade-1-27-latest-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-27-latest
+[periodic-cluster-api-e2e-workload-upgrade-1-27-1-28-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-27-1-28
+[periodic-cluster-api-e2e-workload-upgrade-1-28-latest-main]: https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api#capi-e2e-main-1-28-latest
 [cluster-api-push-images-nightly]: https://testgrid.k8s.io/sig-cluster-lifecycle-image-pushes#cluster-api-push-images-nightly
 [post-cluster-api-push-images]: https://testgrid.k8s.io/sig-cluster-lifecycle-image-pushes#post-cluster-api-push-images
diff --git a/docs/book/src/reference/versions.md b/docs/book/src/reference/versions.md
index 6d489141f..6856cac39 100644
--- a/docs/book/src/reference/versions.md
+++ b/docs/book/src/reference/versions.md
@@ -78,12 +78,13 @@ These diagrams show the relationships between components in a Cluster API releas
 | Kubernetes v1.19  | ✓ (only workload)    |                   |                   |                   |
 | Kubernetes v1.20  | ✓                    |                   |                   |                   |
 | Kubernetes v1.21  | ✓                    | ✓ (only workload) |                   |                   |
-| Kubernetes v1.22  | ✓                    | ✓ (only workload) | ✓ (only workload) | ✓ (only workload) |
+| Kubernetes v1.22  | ✓                    | ✓ (only workload) | ✓ (only workload) |                   |
 | Kubernetes v1.23* | ✓                    | ✓                 | ✓ (only workload) | ✓ (only workload) |
-| Kubernetes v1.24  | ✓                    | ✓                 | ✓                 | ✓                 |
+| Kubernetes v1.24  | ✓                    | ✓                 | ✓                 | ✓ (only workload) |
 | Kubernetes v1.25  | ✓                    | ✓                 | ✓                 | ✓                 |
 | Kubernetes v1.26  | ✓                    | ✓                 | ✓                 | ✓                 |
 | Kubernetes v1.27  |                      | ✓                 | ✓                 | ✓                 |
+| Kubernetes v1.28  |                      |                   | ✓                 | ✓                 |
 
 
 \* There is an issue with CRDs in Kubernetes v1.23.{0-2}. ClusterClass with patches is affected by that (for more details please see [this issue](https://github.com/kubernetes-sigs/cluster-api/issues/5990)). Therefore we recommend to use Kubernetes v1.23.3+ with ClusterClass.
@@ -101,12 +102,13 @@ The Core Provider also talks to API server of every Workload Cluster. Therefore,
 | Kubernetes v1.19 + kubeadm/v1beta2 | ✓ (only workload)    |                    |                    |                    |
 | Kubernetes v1.20 + kubeadm/v1beta2 | ✓                    |                    |                    |                    |
 | Kubernetes v1.21 + kubeadm/v1beta2 | ✓                    | ✓  (only workload) |                    |                    |
-| Kubernetes v1.22 + kubeadm/v1beta3 | ✓                    | ✓  (only workload) | ✓  (only workload) | ✓  (only workload) |
+| Kubernetes v1.22 + kubeadm/v1beta3 | ✓                    | ✓  (only workload) | ✓  (only workload) |                    |
 | Kubernetes v1.23 + kubeadm/v1beta3 | ✓                    | ✓                  | ✓  (only workload) | ✓  (only workload) |
-| Kubernetes v1.24 + kubeadm/v1beta3 | ✓                    | ✓                  | ✓                  | ✓                  |
+| Kubernetes v1.24 + kubeadm/v1beta3 | ✓                    | ✓                  | ✓                  | ✓  (only workload) |
 | Kubernetes v1.25 + kubeadm/v1beta3 | ✓                    | ✓                  | ✓                  | ✓                  |
 | Kubernetes v1.26 + kubeadm/v1beta3 | ✓                    | ✓                  | ✓                  | ✓                  |
 | Kubernetes v1.27 + kubeadm/v1beta3 |                      | ✓                  | ✓                  | ✓                  |
+| Kubernetes v1.28 + kubeadm/v1beta3 |                      |                    | ✓                  | ✓                  |
 
 The Kubeadm Bootstrap Provider generates kubeadm configuration using the API version recommended for the target Kubernetes version.
 
@@ -118,12 +120,13 @@ The Kubeadm Bootstrap Provider generates kubeadm configuration using the API ver
 | Kubernetes v1.19 + etcd/v3 | ✓ (only workload)    |                   |                   |                   |
 | Kubernetes v1.20 + etcd/v3 | ✓                    |                   |                   |                   |
 | Kubernetes v1.21 + etcd/v3 | ✓                    | ✓ (only workload) |                   |                   |
-| Kubernetes v1.22 + etcd/v3 | ✓                    | ✓ (only workload) | ✓ (only workload) | ✓ (only workload) |
+| Kubernetes v1.22 + etcd/v3 | ✓                    | ✓ (only workload) | ✓ (only workload) |                   |
 | Kubernetes v1.23 + etcd/v3 | ✓                    | ✓                 | ✓ (only workload) | ✓ (only workload) |
-| Kubernetes v1.24 + etcd/v3 | ✓                    | ✓                 | ✓                 | ✓                 |
+| Kubernetes v1.24 + etcd/v3 | ✓                    | ✓                 | ✓                 | ✓ (only workload) |
 | Kubernetes v1.25 + etcd/v3 | ✓                    | ✓                 | ✓                 | ✓                 |
 | Kubernetes v1.26 + etcd/v3 | ✓                    | ✓                 | ✓                 | ✓                 |
 | Kubernetes v1.27 + etcd/v3 |                      | ✓                 | ✓                 | ✓                 |
+| Kubernetes v1.28 + etcd/v3 |                      |                   | ✓                 | ✓                 |
 
 The Kubeadm Control Plane Provider talks to the API server and etcd members of every Workload Cluster whose control plane it owns. It uses the etcd v3 API.
 
@@ -144,6 +147,9 @@ The Kubeadm Control Plane requires the Kubeadm Bootstrap Provider.
 
 #### Kubernetes version specific notes
 
+**1.28**:
+* No specific notes
+
 **1.27**:
 * No specific notes
 
diff --git a/docs/release/release-tasks.md b/docs/release/release-tasks.md
index 03dd85bae..381fa115c 100644
--- a/docs/release/release-tasks.md
+++ b/docs/release/release-tasks.md
@@ -108,20 +108,19 @@ This comes down to changing occurrences of the old version to the new version, e
       1. Create a new `v1.4` `metadata.yaml` (`test/e2e/data/shared/v1.4/metadata.yaml`) by copying
    `test/e2e/data/shared/main/metadata.yaml`
       2. Add the new release to the main `metadata.yaml` (`test/e2e/data/shared/main/metadata.yaml`).
-      3. Add the new release to the root level `metadata.yaml`
-      4. Remove old `metadata.yaml`'s that are not used anymore in clusterctl upgrade tests.
+      3. Remove old `metadata.yaml`'s that are not used anymore in clusterctl upgrade tests.
    4. Adjust cluster templates in `test/e2e/data/infrastructure-docker`:
       1. Create a new `v1.4` folder. It should be created based on the `main` folder and only contain the templates
          we use in the clusterctl upgrade tests (as of today `cluster-template` and `cluster-template-topology`).
       2. Remove old folders that are not used anymore in clusterctl upgrade tests.
    5. Modify the test specs in `test/e2e/clusterctl_upgrade_test.go` (according to the versions we want to test described above).
       Please note that both `InitWithKubernetesVersion` and `WorkloadKubernetesVersion` should be the highest mgmt cluster version supported by the respective Cluster API version.
-2. Update `create-local-repository.py` and `tools/internal/tilt-prepare/main.go`: `v1.4.99` => `v1.5.99`.
+2. Update `create-local-repository.py` and `tools/tilt-prepare/main.go`: `v1.4.99` => `v1.5.99`.
 3. Make sure all tests are green (also run `pull-cluster-api-e2e-full-main` and `pull-cluster-api-e2e-workload-upgrade-1-23-latest-main`).
 
 Prior art: 
+- 1.3 - https://github.com/kubernetes-sigs/cluster-api/pull/6834/files
 - 1.4 - https://github.com/kubernetes-sigs/cluster-api/pull/7692/files
-- 1.5 - https://github.com/kubernetes-sigs/cluster-api/pull/8430/files
 
 #### Create a new GitHub milestone for the next release
 
@@ -426,7 +425,7 @@ While we add test coverage for the new release branch we will also drop the test
 3. Remove tests for old release branches according to our policy documented in [Support and guarantees](../../CONTRIBUTING.md#support-and-guarantees)
    For example, let's assume we just created tests for v1.4, then we can now drop test coverage for the release-1.1 branch.
 4. Verify the jobs and dashboards a day later by taking a look at: `https://testgrid.k8s.io/sig-cluster-lifecycle-cluster-api-1.4`
-5. Update `.github/workflows/weekly-security-scan.yaml` - to setup Trivy and govulncheck scanning - `.github/workflows/weekly-md-link-check.yaml` - to setup link checking in the CAPI book - and `.github/workflows/weekly-test-release.yaml` - to verify the release target is working - for the currently supported branches.
+5. Update `.github/workflows/weekly-image-scan.yaml` - to setup Trivy scanning - `.github/workflows/weekly-md-link-check.yaml` - to setup link checking in the CAPI book - and `.github/workflows/weekly-test-release.yaml` - to verify the release target is working - for the currently supported branches.
 
 
 Prior art:
diff --git a/exp/addons/api/v1beta1/clusterresourceset_types.go b/exp/addons/api/v1beta1/clusterresourceset_types.go
index 2800bf190..e64dccd29 100644
--- a/exp/addons/api/v1beta1/clusterresourceset_types.go
+++ b/exp/addons/api/v1beta1/clusterresourceset_types.go
@@ -140,5 +140,5 @@ type ClusterResourceSetList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &ClusterResourceSet{}, &ClusterResourceSetList{})
+	SchemeBuilder.Register(&ClusterResourceSet{}, &ClusterResourceSetList{})
 }
diff --git a/exp/addons/api/v1beta1/clusterresourcesetbinding_types.go b/exp/addons/api/v1beta1/clusterresourcesetbinding_types.go
index 299112258..d58c46461 100644
--- a/exp/addons/api/v1beta1/clusterresourcesetbinding_types.go
+++ b/exp/addons/api/v1beta1/clusterresourcesetbinding_types.go
@@ -152,5 +152,5 @@ type ClusterResourceSetBindingList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &ClusterResourceSetBinding{}, &ClusterResourceSetBindingList{})
+	SchemeBuilder.Register(&ClusterResourceSetBinding{}, &ClusterResourceSetBindingList{})
 }
diff --git a/exp/addons/api/v1beta1/groupversion_info.go b/exp/addons/api/v1beta1/groupversion_info.go
index f02006147..c21f20bde 100644
--- a/exp/addons/api/v1beta1/groupversion_info.go
+++ b/exp/addons/api/v1beta1/groupversion_info.go
@@ -20,26 +20,17 @@ limitations under the License.
 package v1beta1
 
 import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
 var (
 	// GroupVersion is group version used to register these objects.
 	GroupVersion = schema.GroupVersion{Group: "addons.cluster.x-k8s.io", Version: "v1beta1"}
 
-	// schemeBuilder is used to add go types to the GroupVersionKind scheme.
-	schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 
 	// AddToScheme adds the types in this group-version to the given scheme.
-	AddToScheme = schemeBuilder.AddToScheme
-
-	objectTypes = []runtime.Object{}
+	AddToScheme = SchemeBuilder.AddToScheme
 )
-
-func addKnownTypes(scheme *runtime.Scheme) error {
-	scheme.AddKnownTypes(GroupVersion, objectTypes...)
-	metav1.AddToGroupVersion(scheme, GroupVersion)
-	return nil
-}
diff --git a/exp/api/v1beta1/groupversion_info.go b/exp/api/v1beta1/groupversion_info.go
index fd5c24e95..c92eaed36 100644
--- a/exp/api/v1beta1/groupversion_info.go
+++ b/exp/api/v1beta1/groupversion_info.go
@@ -20,26 +20,17 @@ limitations under the License.
 package v1beta1
 
 import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
 var (
 	// GroupVersion is group version used to register these objects.
 	GroupVersion = schema.GroupVersion{Group: "cluster.x-k8s.io", Version: "v1beta1"}
 
-	// schemeBuilder is used to add go types to the GroupVersionKind scheme.
-	schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 
 	// AddToScheme adds the types in this group-version to the given scheme.
-	AddToScheme = schemeBuilder.AddToScheme
-
-	objectTypes = []runtime.Object{}
+	AddToScheme = SchemeBuilder.AddToScheme
 )
-
-func addKnownTypes(scheme *runtime.Scheme) error {
-	scheme.AddKnownTypes(GroupVersion, objectTypes...)
-	metav1.AddToGroupVersion(scheme, GroupVersion)
-	return nil
-}
diff --git a/exp/api/v1beta1/machinepool_types.go b/exp/api/v1beta1/machinepool_types.go
index a2fc2957b..3d08de826 100644
--- a/exp/api/v1beta1/machinepool_types.go
+++ b/exp/api/v1beta1/machinepool_types.go
@@ -249,5 +249,5 @@ type MachinePoolList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &MachinePool{}, &MachinePoolList{})
+	SchemeBuilder.Register(&MachinePool{}, &MachinePoolList{})
 }
diff --git a/exp/ipam/api/v1alpha1/groupversion_info.go b/exp/ipam/api/v1alpha1/groupversion_info.go
index 221e76501..7be93c0c9 100644
--- a/exp/ipam/api/v1alpha1/groupversion_info.go
+++ b/exp/ipam/api/v1alpha1/groupversion_info.go
@@ -20,26 +20,17 @@ limitations under the License.
 package v1alpha1
 
 import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
 var (
 	// GroupVersion is group version used to register these objects.
 	GroupVersion = schema.GroupVersion{Group: "ipam.cluster.x-k8s.io", Version: "v1alpha1"}
 
-	// schemeBuilder is used to add go types to the GroupVersionKind scheme.
-	schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 
 	// AddToScheme adds the types in this group-version to the given scheme.
-	AddToScheme = schemeBuilder.AddToScheme
-
-	objectTypes = []runtime.Object{}
+	AddToScheme = SchemeBuilder.AddToScheme
 )
-
-func addKnownTypes(scheme *runtime.Scheme) error {
-	scheme.AddKnownTypes(GroupVersion, objectTypes...)
-	metav1.AddToGroupVersion(scheme, GroupVersion)
-	return nil
-}
diff --git a/exp/ipam/api/v1alpha1/ipaddress_types.go b/exp/ipam/api/v1alpha1/ipaddress_types.go
index b40982d96..01af31d26 100644
--- a/exp/ipam/api/v1alpha1/ipaddress_types.go
+++ b/exp/ipam/api/v1alpha1/ipaddress_types.go
@@ -65,5 +65,5 @@ type IPAddressList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &IPAddress{}, &IPAddressList{})
+	SchemeBuilder.Register(&IPAddress{}, &IPAddressList{})
 }
diff --git a/exp/ipam/api/v1alpha1/ipaddressclaim_types.go b/exp/ipam/api/v1alpha1/ipaddressclaim_types.go
index 16df8605e..af259b415 100644
--- a/exp/ipam/api/v1alpha1/ipaddressclaim_types.go
+++ b/exp/ipam/api/v1alpha1/ipaddressclaim_types.go
@@ -76,5 +76,5 @@ type IPAddressClaimList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &IPAddressClaim{}, &IPAddressClaimList{})
+	SchemeBuilder.Register(&IPAddressClaim{}, &IPAddressClaimList{})
 }
diff --git a/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go b/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go
index ea5de78b8..88cc359b9 100644
--- a/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go
+++ b/exp/ipam/api/v1alpha1/zz_generated.deepcopy.go
@@ -22,7 +22,7 @@ limitations under the License.
 package v1alpha1
 
 import (
-	"k8s.io/apimachinery/pkg/runtime"
+	runtime "k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
diff --git a/exp/runtime/api/v1alpha1/extensionconfig_types.go b/exp/runtime/api/v1alpha1/extensionconfig_types.go
index bd8c3e5d0..de426e366 100644
--- a/exp/runtime/api/v1alpha1/extensionconfig_types.go
+++ b/exp/runtime/api/v1alpha1/extensionconfig_types.go
@@ -197,7 +197,7 @@ type ExtensionConfigList struct {
 }
 
 func init() {
-	objectTypes = append(objectTypes, &ExtensionConfig{}, &ExtensionConfigList{})
+	SchemeBuilder.Register(&ExtensionConfig{}, &ExtensionConfigList{})
 }
 
 const (
diff --git a/exp/runtime/api/v1alpha1/groupversion_info.go b/exp/runtime/api/v1alpha1/groupversion_info.go
index 856a1ef0b..bac33a979 100644
--- a/exp/runtime/api/v1alpha1/groupversion_info.go
+++ b/exp/runtime/api/v1alpha1/groupversion_info.go
@@ -20,26 +20,17 @@ limitations under the License.
 package v1alpha1
 
 import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
 var (
 	// GroupVersion is group version used to register these objects.
 	GroupVersion = schema.GroupVersion{Group: "runtime.cluster.x-k8s.io", Version: "v1alpha1"}
 
-	// schemeBuilder is used to add go types to the GroupVersionKind scheme.
-	schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
 
 	// AddToScheme adds the types in this group-version to the given scheme.
-	AddToScheme = schemeBuilder.AddToScheme
-
-	objectTypes = []runtime.Object{}
+	AddToScheme = SchemeBuilder.AddToScheme
 )
-
-func addKnownTypes(scheme *runtime.Scheme) error {
-	scheme.AddKnownTypes(GroupVersion, objectTypes...)
-	metav1.AddToGroupVersion(scheme, GroupVersion)
-	return nil
-}
diff --git a/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go
index 49f65c3e8..50a0cc037 100644
--- a/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go
+++ b/exp/runtime/api/v1alpha1/zz_generated.deepcopy.go
@@ -23,7 +23,7 @@ package v1alpha1
 
 import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
+	runtime "k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/cluster-api/api/v1beta1"
 )
 
diff --git a/go.mod b/go.mod
index ea491d378..782100a84 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/MakeNowJust/heredoc v1.0.0
 	github.com/Masterminds/sprig/v3 v3.2.3
 	github.com/adrg/xdg v0.4.0
-	github.com/blang/semver/v4 v4.0.0
+	github.com/blang/semver v3.5.1+incompatible
 	github.com/coredns/corefile-migration v1.0.20
 	github.com/davecgh/go-spew v1.1.1
 	github.com/docker/distribution v2.8.2+incompatible
@@ -20,20 +20,18 @@ require (
 	github.com/google/go-cmp v0.5.9
 	github.com/google/go-github/v53 v53.2.0
 	github.com/google/gofuzz v1.2.0
-	github.com/olekukonko/tablewriter v0.0.5
+	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/onsi/ginkgo/v2 v2.11.0
 	github.com/onsi/gomega v1.27.10
 	github.com/pkg/errors v0.9.1
-	github.com/prometheus/client_golang v1.16.0
 	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.16.0
 	github.com/valyala/fastjson v1.6.4
 	go.etcd.io/etcd/api/v3 v3.5.9
 	go.etcd.io/etcd/client/v3 v3.5.9
+	golang.org/x/net v0.14.0 // indirect
 	golang.org/x/oauth2 v0.11.0
-	golang.org/x/text v0.12.0
-	gomodules.xyz/jsonpatch/v2 v2.4.0
 	google.golang.org/grpc v1.55.0
 	k8s.io/api v0.27.2
 	k8s.io/apiextensions-apiserver v0.27.2
@@ -43,7 +41,6 @@ require (
 	k8s.io/cluster-bootstrap v0.27.2
 	k8s.io/component-base v0.27.2
 	k8s.io/klog/v2 v2.90.1
-	k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f
 	k8s.io/kubectl v0.27.2
 	k8s.io/utils v0.0.0-20230209194617-a36077c30491
 	sigs.k8s.io/controller-runtime v0.15.1
@@ -54,7 +51,6 @@ require (
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.2.0 // indirect
-	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
 	github.com/ajeddeloh/go-json v0.0.0-20200220154158-5ae607161559 // indirect
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
 	github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
@@ -62,32 +58,24 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chai2010/gettext-go v1.0.2 // indirect
-	github.com/cloudflare/circl v1.3.3 // indirect
 	github.com/coredns/caddy v1.1.0 // indirect
 	github.com/coreos/go-semver v0.3.0 // indirect
 	github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
 	github.com/coreos/go-systemd/v22 v22.4.0 // indirect
-	github.com/daviddengcn/go-colortext v1.0.0 // indirect
-	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
-	github.com/fatih/camelcase v1.0.0 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
-	github.com/fvbommel/sortorder v1.0.1 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
 	github.com/go-logr/zapr v1.2.4 // indirect
 	github.com/go-openapi/jsonpointer v0.19.6 // indirect
 	github.com/go-openapi/jsonreference v0.20.1 // indirect
 	github.com/go-openapi/swag v0.22.3 // indirect
-	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/btree v1.0.1 // indirect
 	github.com/google/cel-go v0.12.6 // indirect
-	github.com/google/gnostic v0.6.9 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
@@ -95,16 +83,13 @@ require (
 	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/jonboulle/clockwork v0.2.2 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
-	github.com/lithammer/dedent v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
-	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.0 // indirect
@@ -115,16 +100,14 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
-	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+	github.com/olekukonko/tablewriter v0.0.5
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+	github.com/prometheus/client_golang v1.16.0
 	github.com/prometheus/client_model v0.4.0 // indirect
 	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/rivo/uniseg v0.4.2 // indirect
-	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/spf13/afero v1.9.5 // indirect
 	github.com/spf13/cast v1.5.1 // indirect
@@ -140,11 +123,11 @@ require (
 	go.uber.org/zap v1.24.0 // indirect
 	go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
 	golang.org/x/crypto v0.12.0 // indirect
-	golang.org/x/net v0.14.0 // indirect
 	golang.org/x/sys v0.11.0 // indirect
 	golang.org/x/term v0.11.0 // indirect
+	golang.org/x/text v0.12.0
 	golang.org/x/time v0.3.0 // indirect
-	golang.org/x/tools v0.9.3 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.3.0
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
@@ -153,11 +136,32 @@ require (
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/cli-runtime v0.27.2 // indirect
-	k8s.io/component-helpers v0.27.2 // indirect
-	k8s.io/metrics v0.27.2 // indirect
+	k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.13.2 // indirect
-	sigs.k8s.io/kustomize/kustomize/v5 v5.0.1 // indirect
 	sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
 )
+
+require (
+	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
+	github.com/cloudflare/circl v1.3.3 // indirect
+	github.com/daviddengcn/go-colortext v1.0.0 // indirect
+	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
+	github.com/fatih/camelcase v1.0.0 // indirect
+	github.com/fvbommel/sortorder v1.0.1 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/google/gnostic v0.6.9 // indirect
+	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
+	github.com/jonboulle/clockwork v0.2.2 // indirect
+	github.com/lithammer/dedent v1.1.0 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	golang.org/x/tools v0.9.3 // indirect
+	k8s.io/component-helpers v0.27.2 // indirect
+	k8s.io/metrics v0.27.2 // indirect
+	sigs.k8s.io/kustomize/kustomize/v5 v5.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
index 0a67015d4..0de4131ee 100644
--- a/go.sum
+++ b/go.sum
@@ -80,6 +80,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@@ -822,8 +824,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
+gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/hack/kind-install-for-capd.sh b/hack/kind-install-for-capd.sh
index a0d3474c0..a9bc88882 100755
--- a/hack/kind-install-for-capd.sh
+++ b/hack/kind-install-for-capd.sh
@@ -31,8 +31,7 @@ if [[ "${TRACE-0}" == "1" ]]; then
 fi
 
 KIND_CLUSTER_NAME=${CAPI_KIND_CLUSTER_NAME:-"capi-test"}
-# See: https://kind.sigs.k8s.io/docs/user/configuration/#ip-family
-KIND_NETWORK_IPFAMILY=${KIND_NETWORK_IPFAMILY:-"dual"}
+
 
 # 1. If kind cluster already exists exit.
 if [[ "$(kind get clusters)" =~ .*"${KIND_CLUSTER_NAME}".* ]]; then
@@ -60,7 +59,7 @@ cat <<EOF | kind create cluster --name="$KIND_CLUSTER_NAME"  --config=-
 kind: Cluster
 apiVersion: kind.x-k8s.io/v1alpha4
 networking:
-  ipFamily: ${KIND_NETWORK_IPFAMILY}
+  ipFamily: dual
 nodes:
 - role: control-plane
   extraMounts:
diff --git a/hack/tools/conversion-verifier/main.go b/hack/tools/conversion-verifier/main.go
index ab1109014..02272b2e6 100644
--- a/hack/tools/conversion-verifier/main.go
+++ b/hack/tools/conversion-verifier/main.go
@@ -17,11 +17,11 @@ limitations under the License.
 package main
 
 import (
-	"fmt"
 	"go/types"
 	"path"
 	"strings"
 
+	"github.com/hashicorp/go-multierror"
 	"github.com/pkg/errors"
 	"k8s.io/klog/v2"
 	crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
@@ -70,7 +70,7 @@ func (s *storageVersionType) IsHub() bool {
 }
 
 func main() {
-	var errs []error
+	var result error
 
 	// Define the marker collector.
 	col := &markers.Collector{
@@ -118,7 +118,7 @@ func main() {
 				return
 			}
 			if _, ok := storageVersionTypes[info.Name]; ok {
-				errs = append(errs,
+				result = multierror.Append(result,
 					errors.Errorf("type %q has a redeclared storage version in package %q", info.Name, pkg.PkgPath),
 				)
 				return
@@ -197,24 +197,21 @@ func main() {
 		}
 
 		if !storageType.IsHub() {
-			errs = append(errs,
+			result = multierror.Append(result,
 				errors.Errorf("type %q in package %q marked as storage version but it's not convertible, missing Hub() method", name, storageType.pkg.PkgPath),
 			)
 		}
 		for _, decl := range storageType.otherDecls {
 			if !decl.IsConvertible() {
-				errs = append(errs,
+				result = multierror.Append(result,
 					errors.Errorf("type %q in package %q it's not convertible, missing ConvertFrom() and ConvertTo() methods", name, decl.pkg.PkgPath),
 				)
 			}
 		}
 	}
 
-	if len(errs) > 0 {
-		fmt.Printf("%d errors occurred:\n", len(errs))
-		for _, err := range errs {
-			fmt.Printf("\t* %s\n", err)
-		}
+	if result != nil {
+		klog.Exit(result.Error())
 	}
 }
 
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 5dc4772e6..9952d8978 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -8,7 +8,8 @@ replace sigs.k8s.io/cluster-api/test => ../../test
 
 require (
 	cloud.google.com/go/storage v1.31.0
-	github.com/blang/semver/v4 v4.0.0
+	github.com/blang/semver v3.5.1+incompatible
+	github.com/hashicorp/go-multierror v1.1.1
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/pflag v1.0.5
 	github.com/valyala/fastjson v1.6.4
@@ -45,6 +46,7 @@ require (
 	github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/cloudflare/circl v1.3.3 // indirect
 	github.com/coredns/caddy v1.1.0 // indirect
@@ -78,6 +80,7 @@ require (
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/huandu/xstrings v1.3.3 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
@@ -118,7 +121,7 @@ require (
 	golang.org/x/text v0.12.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.4.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 3ad40c9a6..424b1a675 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -81,6 +81,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@@ -277,10 +279,14 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -742,8 +748,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
+gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/hack/tools/internal/log-push/main.go b/hack/tools/log-push/main.go
similarity index 100%
rename from hack/tools/internal/log-push/main.go
rename to hack/tools/log-push/main.go
diff --git a/hack/tools/internal/log-push/main_test.go b/hack/tools/log-push/main_test.go
similarity index 100%
rename from hack/tools/internal/log-push/main_test.go
rename to hack/tools/log-push/main_test.go
diff --git a/hack/tools/mdbook/releaselink/releaselink.go b/hack/tools/mdbook/releaselink/releaselink.go
index 96d9cf99f..4bd707e19 100644
--- a/hack/tools/mdbook/releaselink/releaselink.go
+++ b/hack/tools/mdbook/releaselink/releaselink.go
@@ -28,7 +28,7 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"golang.org/x/tools/go/vcs"
 	"sigs.k8s.io/kubebuilder/docs/book/utils/plugin"
 
diff --git a/hack/tools/release/notes.go b/hack/tools/release/notes.go
index 478571212..fab10f357 100644
--- a/hack/tools/release/notes.go
+++ b/hack/tools/release/notes.go
@@ -63,7 +63,7 @@ var (
 		unknown,
 	}
 
-	repo = flag.String("repository", "kubernetes-sigs/cluster-api", "The repo to run the tool from.")
+	repo = flag.String("repository", "kubernetes-sigs/cluster-api", "The tag or commit to start from.")
 
 	fromTag = flag.String("from", "", "The tag or commit to start from.")
 
@@ -447,9 +447,6 @@ func modifyEntryTitle(title string, prefixes []string) string {
 // generateReleaseNoteEntry processes a commit into a PR line item for the release notes.
 func generateReleaseNoteEntry(c *commit) (*releaseNoteEntry, error) {
 	entry := &releaseNoteEntry{}
-	if c.body == "" {
-		c.body = "ERROR: BODY MISSING. FIX MANUALLY"
-	}
 	entry.title = trimTitle(c.body)
 	var fork string
 
diff --git a/hack/tools/internal/tilt-prepare/main.go b/hack/tools/tilt-prepare/main.go
similarity index 99%
rename from hack/tools/internal/tilt-prepare/main.go
rename to hack/tools/tilt-prepare/main.go
index bdd45d13c..5be7f25bb 100644
--- a/hack/tools/internal/tilt-prepare/main.go
+++ b/hack/tools/tilt-prepare/main.go
@@ -70,7 +70,7 @@ const (
 
 var (
 	// Defines the default version to be used for the provider CR if no version is specified in the tilt-provider.yaml|json file.
-	defaultProviderVersion = "v1.6.99"
+	defaultProviderVersion = "v1.5.99"
 
 	// This data struct mirrors a subset of info from the providers struct in the tilt file
 	// which is containing "hard-coded" tilt-provider.yaml files for the providers managed in the Cluster API repository.
diff --git a/hack/verify-container-images.sh b/hack/verify-container-images.sh
index 496e03faa..5d8977a7e 100755
--- a/hack/verify-container-images.sh
+++ b/hack/verify-container-images.sh
@@ -72,7 +72,7 @@ NC='\033[0m' # No
 
 if [ "$R1" -ne "0" ] || [ "$R2"  -ne "0" ] || [ "$R3" -ne "0" ] || [ "$R4" -ne "0"  ] || [ "$R5" -ne "0"  ] || [ "$R6" -ne "0" ]
 then
-  echo -e "${BRed}Check container images failed! There are vulnerabilities to be fixed${NC}"
+  echo -e "${BRed}Check container images failed! There are vulnerability to be fixed${NC}"
   exit 1
 fi
 
diff --git a/internal/contract/controlplane.go b/internal/contract/controlplane.go
index 7bbc306e7..df322ddcd 100644
--- a/internal/contract/controlplane.go
+++ b/internal/contract/controlplane.go
@@ -19,7 +19,7 @@ package contract
 import (
 	"sync"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/utils/pointer"
diff --git a/internal/controllers/machineset/machineset_preflight.go b/internal/controllers/machineset/machineset_preflight.go
index c9ce691e8..00d5d67fc 100644
--- a/internal/controllers/machineset/machineset_preflight.go
+++ b/internal/controllers/machineset/machineset_preflight.go
@@ -22,7 +22,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/internal/goproxy/goproxy.go b/internal/goproxy/goproxy.go
index 3448de0fa..a44bd0ce1 100644
--- a/internal/goproxy/goproxy.go
+++ b/internal/goproxy/goproxy.go
@@ -28,7 +28,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/util/wait"
 )
diff --git a/internal/util/kubeadm/kubeadm.go b/internal/util/kubeadm/kubeadm.go
index a35d98401..bd6902711 100644
--- a/internal/util/kubeadm/kubeadm.go
+++ b/internal/util/kubeadm/kubeadm.go
@@ -17,7 +17,7 @@ limitations under the License.
 // Package kubeadm contains utils related to kubeadm.
 package kubeadm
 
-import "github.com/blang/semver/v4"
+import "github.com/blang/semver"
 
 const (
 	// DefaultImageRepository is the new default Kubernetes image registry.
diff --git a/internal/webhooks/cluster.go b/internal/webhooks/cluster.go
index 61eff982d..2e0da4f57 100644
--- a/internal/webhooks/cluster.go
+++ b/internal/webhooks/cluster.go
@@ -24,7 +24,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/metadata.yaml b/metadata.yaml
index de46d5e8f..49d96b1c0 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -6,9 +6,6 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
-  - major: 1
-    minor: 6
-    contract: v1beta1
   - major: 1
     minor: 5
     contract: v1beta1
diff --git a/util/annotations/helpers.go b/util/annotations/helpers.go
index 47dc7fc6b..0ec9ef938 100644
--- a/util/annotations/helpers.go
+++ b/util/annotations/helpers.go
@@ -71,6 +71,7 @@ func AddAnnotations(o metav1.Object, desired map[string]string) bool {
 	annotations := o.GetAnnotations()
 	if annotations == nil {
 		annotations = make(map[string]string)
+		o.SetAnnotations(annotations)
 	}
 	hasChanged := false
 	for k, v := range desired {
@@ -79,7 +80,6 @@ func AddAnnotations(o metav1.Object, desired map[string]string) bool {
 			hasChanged = true
 		}
 	}
-	o.SetAnnotations(annotations)
 	return hasChanged
 }
 
diff --git a/util/collections/machine_collection.go b/util/collections/machine_collection.go
index 83a50ad94..2958ee5d6 100644
--- a/util/collections/machine_collection.go
+++ b/util/collections/machine_collection.go
@@ -30,7 +30,7 @@ package collections
 import (
 	"sort"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util/conditions"
diff --git a/util/collections/machine_filters.go b/util/collections/machine_filters.go
index 12c2e20f8..d85661012 100644
--- a/util/collections/machine_filters.go
+++ b/util/collections/machine_filters.go
@@ -19,7 +19,7 @@ package collections
 import (
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/selection"
diff --git a/util/util.go b/util/util.go
index d72947ecf..9edcf9123 100644
--- a/util/util.go
+++ b/util/util.go
@@ -27,7 +27,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
diff --git a/util/version/version.go b/util/version/version.go
index 601f934bd..277ed6105 100644
--- a/util/version/version.go
+++ b/util/version/version.go
@@ -22,7 +22,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 )
 


Test patch:
diff --git a/.github/workflows/weekly-test-release.yaml b/.github/workflows/weekly-test-release.yaml
index 77c9b30ff..0c4c6f662 100644
--- a/.github/workflows/weekly-test-release.yaml
+++ b/.github/workflows/weekly-test-release.yaml
@@ -32,7 +32,7 @@ jobs:
       - name: Calculate go version
         run: echo "go_version=$(make go-version)" >> $GITHUB_ENV
       - name: Set up Go
-        uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # tag=v4.1.0
+        uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # tag=v4.0.1
         with:
           go-version: ${{ env.go_version }}
       - name: Test release
diff --git a/bootstrap/kubeadm/types/utils_test.go b/bootstrap/kubeadm/types/utils_test.go
index 62a0a72d4..5153d834a 100644
--- a/bootstrap/kubeadm/types/utils_test.go
+++ b/bootstrap/kubeadm/types/utils_test.go
@@ -19,7 +19,7 @@ package utils
 import (
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
 	"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go
index 3b26d0130..cd230eb61 100644
--- a/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go
+++ b/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_webhook_test.go
@@ -698,11 +698,6 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) {
 		"/invalid-key": "foo",
 	}
 
-	beforeUseExperimentalRetryJoin := before.DeepCopy()
-	beforeUseExperimentalRetryJoin.Spec.KubeadmConfigSpec.UseExperimentalRetryJoin = true //nolint:staticcheck
-	updateUseExperimentalRetryJoin := before.DeepCopy()
-	updateUseExperimentalRetryJoin.Spec.KubeadmConfigSpec.UseExperimentalRetryJoin = false //nolint:staticcheck
-
 	tests := []struct {
 		name                  string
 		enableIgnitionFeature bool
@@ -1055,12 +1050,6 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) {
 			before:                before,
 			kcp:                   invalidMetadata,
 		},
-		{
-			name:      "should allow changes to useExperimentalRetryJoin",
-			expectErr: false,
-			before:    beforeUseExperimentalRetryJoin,
-			kcp:       updateUseExperimentalRetryJoin,
-		},
 	}
 
 	for _, tt := range tests {
diff --git a/controlplane/kubeadm/internal/controllers/controller_test.go b/controlplane/kubeadm/internal/controllers/controller_test.go
index 6f1808c47..8f8f73472 100644
--- a/controlplane/kubeadm/internal/controllers/controller_test.go
+++ b/controlplane/kubeadm/internal/controllers/controller_test.go
@@ -28,7 +28,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -1332,7 +1332,7 @@ kubernetesVersion: metav1.16.1`,
 				Status: internal.ClusterStatus{},
 			},
 		},
-		ssaCache: ssa.NewCache(),
+		disableInPlacePropagation: true,
 	}
 
 	result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(kcp)})
diff --git a/controlplane/kubeadm/internal/controllers/fakes_test.go b/controlplane/kubeadm/internal/controllers/fakes_test.go
index ebc04e404..7f5a962e2 100644
--- a/controlplane/kubeadm/internal/controllers/fakes_test.go
+++ b/controlplane/kubeadm/internal/controllers/fakes_test.go
@@ -20,7 +20,7 @@ import (
 	"context"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
diff --git a/controlplane/kubeadm/internal/controllers/scale_test.go b/controlplane/kubeadm/internal/controllers/scale_test.go
index 393e08de4..a74cf0025 100644
--- a/controlplane/kubeadm/internal/controllers/scale_test.go
+++ b/controlplane/kubeadm/internal/controllers/scale_test.go
@@ -213,13 +213,14 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 			managementCluster:         fmc,
 			managementClusterUncached: fmc,
 			recorder:                  record.NewFakeRecorder(32),
+			disableInPlacePropagation: true,
 		}
 
 		controlPlane, adoptableMachineFound, err := r.initControlPlaneScope(ctx, cluster, kcp)
 		g.Expect(err).ToNot(HaveOccurred())
 		g.Expect(adoptableMachineFound).To(BeFalse())
 
-		result, err := r.scaleUpControlPlane(context.Background(), controlPlane)
+		result, err := r.reconcile(context.Background(), controlPlane)
 		g.Expect(err).ToNot(HaveOccurred())
 		g.Expect(result).To(Equal(ctrl.Result{RequeueAfter: preflightFailedRequeueAfter}))
 
diff --git a/controlplane/kubeadm/internal/controllers/upgrade_test.go b/controlplane/kubeadm/internal/controllers/upgrade_test.go
index 78e4db4e9..ea591525a 100644
--- a/controlplane/kubeadm/internal/controllers/upgrade_test.go
+++ b/controlplane/kubeadm/internal/controllers/upgrade_test.go
@@ -35,7 +35,6 @@ import (
 	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
 	"sigs.k8s.io/cluster-api/internal/test/builder"
-	"sigs.k8s.io/cluster-api/internal/util/ssa"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/collections"
 )
@@ -94,7 +93,7 @@ func TestKubeadmControlPlaneReconciler_RolloutStrategy_ScaleUp(t *testing.T) {
 				Status: internal.ClusterStatus{Nodes: 1},
 			},
 		},
-		ssaCache: ssa.NewCache(),
+		disableInPlacePropagation: true,
 	}
 	controlPlane := &internal.ControlPlane{
 		KCP:      kcp,
diff --git a/controlplane/kubeadm/internal/workload_cluster_coredns_test.go b/controlplane/kubeadm/internal/workload_cluster_coredns_test.go
index 38e6aa1f4..bac6124e7 100644
--- a/controlplane/kubeadm/internal/workload_cluster_coredns_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_coredns_test.go
@@ -19,7 +19,7 @@ package internal
 import (
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
diff --git a/controlplane/kubeadm/internal/workload_cluster_etcd_test.go b/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
index e80e869a5..9972cd283 100644
--- a/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
@@ -21,7 +21,7 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
diff --git a/controlplane/kubeadm/internal/workload_cluster_rbac_test.go b/controlplane/kubeadm/internal/workload_cluster_rbac_test.go
index 3463e10ad..f4f93e3d6 100644
--- a/controlplane/kubeadm/internal/workload_cluster_rbac_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_rbac_test.go
@@ -20,7 +20,7 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/controlplane/kubeadm/internal/workload_cluster_test.go b/controlplane/kubeadm/internal/workload_cluster_test.go
index b0e900387..f1540168a 100644
--- a/controlplane/kubeadm/internal/workload_cluster_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_test.go
@@ -21,7 +21,7 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
diff --git a/docs/book/src/developer/testing.md b/docs/book/src/developer/testing.md
index 73f6ac482..adb54a126 100644
--- a/docs/book/src/developer/testing.md
+++ b/docs/book/src/developer/testing.md
@@ -316,7 +316,7 @@ analyzing them via Grafana.
    Just click on the downwards arrow, enter either a ProwJob URL, a GCS path or a local folder and click on `Import Logs`.
    This will retrieve the logs and push them to Loki. Alternatively, the logs can be imported via:
    ```bash
-   go run ./hack/tools/internal/log-push --log-path=<log-path>
+   go run ./hack/tools/log-push --log-path=<log-path>
    ```
    Examples for log paths:
     * ProwJob URL: `https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/6189/pull-cluster-api-e2e-main/1496954690603061248`
diff --git a/internal/goproxy/goproxy_test.go b/internal/goproxy/goproxy_test.go
index f25f69b83..c914fa6f2 100644
--- a/internal/goproxy/goproxy_test.go
+++ b/internal/goproxy/goproxy_test.go
@@ -25,7 +25,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 )
 
diff --git a/internal/util/kubeadm/kubeadm_test.go b/internal/util/kubeadm/kubeadm_test.go
index 2f4639b9f..4e1f7c81f 100644
--- a/internal/util/kubeadm/kubeadm_test.go
+++ b/internal/util/kubeadm/kubeadm_test.go
@@ -20,7 +20,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 )
 
diff --git a/scripts/ci-e2e-lib.sh b/scripts/ci-e2e-lib.sh
index bf673e009..3a5af5016 100644
--- a/scripts/ci-e2e-lib.sh
+++ b/scripts/ci-e2e-lib.sh
@@ -204,9 +204,9 @@ EOL
 # the actual test run less sensible to the network speed.
 kind:prepullAdditionalImages () {
   # Pulling cert manager images so we can pre-load in kind nodes
-  kind::prepullImage "quay.io/jetstack/cert-manager-cainjector:v1.12.3"
-  kind::prepullImage "quay.io/jetstack/cert-manager-webhook:v1.12.3"
-  kind::prepullImage "quay.io/jetstack/cert-manager-controller:v1.12.3"
+  kind::prepullImage "quay.io/jetstack/cert-manager-cainjector:v1.12.2"
+  kind::prepullImage "quay.io/jetstack/cert-manager-webhook:v1.12.2"
+  kind::prepullImage "quay.io/jetstack/cert-manager-controller:v1.12.2"
 }
 
 # kind:prepullImage pre-pull a docker image if no already present locally.
diff --git a/test/e2e/cluster_upgrade_runtimesdk_test.go b/test/e2e/cluster_upgrade_runtimesdk_test.go
index b8a6af160..5596311b3 100644
--- a/test/e2e/cluster_upgrade_runtimesdk_test.go
+++ b/test/e2e/cluster_upgrade_runtimesdk_test.go
@@ -20,7 +20,7 @@ limitations under the License.
 package e2e
 
 import (
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"k8s.io/utils/pointer"
diff --git a/test/e2e/clusterctl_upgrade.go b/test/e2e/clusterctl_upgrade.go
index 8d65f5c32..71bf3da0b 100644
--- a/test/e2e/clusterctl_upgrade.go
+++ b/test/e2e/clusterctl_upgrade.go
@@ -29,11 +29,9 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/discovery"
 	"k8s.io/klog/v2"
@@ -336,11 +334,6 @@ func ClusterctlUpgradeSpec(ctx context.Context, inputGetter func() ClusterctlUpg
 
 		By("THE MANAGEMENT CLUSTER WITH THE OLDER VERSION OF PROVIDERS IS UP&RUNNING!")
 
-		machineCRD := &apiextensionsv1.CustomResourceDefinition{}
-		if err := managementClusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: "machines.cluster.x-k8s.io"}, machineCRD); err != nil {
-			Expect(err).ToNot(HaveOccurred(), "failed to retrieve a machine CRD")
-		}
-
 		Byf("Creating a namespace for hosting the %s test workload cluster", specName)
 		testNamespace, testCancelWatches = framework.CreateNamespaceAndWatchEvents(ctx, framework.CreateNamespaceAndWatchEventsInput{
 			Creator:   managementClusterProxy.GetClient(),
@@ -394,34 +387,13 @@ func ClusterctlUpgradeSpec(ctx context.Context, inputGetter func() ClusterctlUpg
 			input.PreWaitForCluster(managementClusterProxy, testNamespace.Name, workLoadClusterName)
 		}
 
-		// Build GroupVersionKind for Machine resources
-		machineListGVK := schema.GroupVersionKind{
-			Group: machineCRD.Spec.Group,
-			Kind:  machineCRD.Spec.Names.ListKind,
-		}
-
-		// Pick the storage version
-		for _, version := range machineCRD.Spec.Versions {
-			if version.Storage {
-				machineListGVK.Version = version.Name
-				break
-			}
-		}
-
 		By("Waiting for the machines to exist")
 		Eventually(func() (int64, error) {
 			var n int64
-			machineList := &unstructured.UnstructuredList{}
-			machineList.SetGroupVersionKind(machineListGVK)
-			if err := managementClusterProxy.GetClient().List(
-				ctx,
-				machineList,
-				client.InNamespace(testNamespace.Name),
-				client.MatchingLabels{clusterv1.ClusterNameLabel: workLoadClusterName},
-			); err == nil {
-				for _, m := range machineList.Items {
-					_, found, err := unstructured.NestedMap(m.Object, "status", "nodeRef")
-					if err == nil && found {
+			machineList := &clusterv1alpha3.MachineList{}
+			if err := managementClusterProxy.GetClient().List(ctx, machineList, client.InNamespace(testNamespace.Name), client.MatchingLabels{clusterv1.ClusterNameLabel: workLoadClusterName}); err == nil {
+				for _, machine := range machineList.Items {
+					if machine.Status.NodeRef != nil {
 						n++
 					}
 				}
@@ -439,7 +411,7 @@ func ClusterctlUpgradeSpec(ctx context.Context, inputGetter func() ClusterctlUpg
 		// Get the workloadCluster before the management cluster is upgraded to make sure that the upgrade did not trigger
 		// any unexpected rollouts.
 		preUpgradeMachineList := &unstructured.UnstructuredList{}
-		preUpgradeMachineList.SetGroupVersionKind(machineListGVK)
+		preUpgradeMachineList.SetGroupVersionKind(clusterv1alpha3.GroupVersion.WithKind("MachineList"))
 		err = managementClusterProxy.GetClient().List(
 			ctx,
 			preUpgradeMachineList,
diff --git a/test/e2e/clusterctl_upgrade_test.go b/test/e2e/clusterctl_upgrade_test.go
index e61680076..2a1b7bdea 100644
--- a/test/e2e/clusterctl_upgrade_test.go
+++ b/test/e2e/clusterctl_upgrade_test.go
@@ -66,7 +66,7 @@ var _ = Describe("When testing clusterctl upgrades (v1.0=>current)", func() {
 	})
 })
 
-var _ = Describe("When testing clusterctl upgrades (v1.4=>current)", func() {
+var _ = Describe("When testing clusterctl upgrades (v1.3=>current)", func() {
 	ClusterctlUpgradeSpec(ctx, func() ClusterctlUpgradeSpecInput {
 		return ClusterctlUpgradeSpecInput{
 			E2EConfig:              e2eConfig,
@@ -75,17 +75,21 @@ var _ = Describe("When testing clusterctl upgrades (v1.4=>current)", func() {
 			ArtifactFolder:         artifactFolder,
 			SkipCleanup:            skipCleanup,
 			InfrastructureProvider: pointer.String("docker"),
-			InitWithBinary:         "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/clusterctl-{OS}-{ARCH}",
+			InitWithBinary:         "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/clusterctl-{OS}-{ARCH}",
 			// We have to pin the providers because with `InitWithProvidersContract` the test would
-			// use the latest version for the contract (which is v1.5.X for v1beta1).
-			InitWithCoreProvider:            "cluster-api:v1.4.5",
-			InitWithBootstrapProviders:      []string{"kubeadm:v1.4.5"},
-			InitWithControlPlaneProviders:   []string{"kubeadm:v1.4.5"},
-			InitWithInfrastructureProviders: []string{"docker:v1.4.5"},
-			InitWithProvidersContract:       "v1beta1",
-			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/e2e/data/infrastructure-docker/v1.4/bases.
-			InitWithKubernetesVersion: "v1.27.3",
-			WorkloadKubernetesVersion: "v1.27.3",
+			// use the latest version for the contract (which is v1.4.X for v1beta1).
+			InitWithCoreProvider:            "cluster-api:v1.3.6",
+			InitWithBootstrapProviders:      []string{"kubeadm:v1.3.6"},
+			InitWithControlPlaneProviders:   []string{"kubeadm:v1.3.6"},
+			InitWithInfrastructureProviders: []string{"docker:v1.3.6"},
+			// We have to set this to an empty array as clusterctl v1.3 doesn't support
+			// runtime extension providers. If we don't do this the test will automatically
+			// try to deploy the latest version of our test-extension from docker.yaml.
+			InitWithRuntimeExtensionProviders: []string{},
+			InitWithProvidersContract:         "v1beta1",
+			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/data/infrastructure-docker/v1.3/bases.
+			InitWithKubernetesVersion: "v1.26.4",
+			WorkloadKubernetesVersion: "v1.26.4",
 			MgmtFlavor:                "topology",
 			WorkloadFlavor:            "",
 			// This check ensures that ownerReference apiVersions are updated for all types after the upgrade.
@@ -103,7 +107,7 @@ var _ = Describe("When testing clusterctl upgrades (v1.4=>current)", func() {
 	})
 })
 
-var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.4=>current) [ClusterClass]", func() {
+var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.3=>current) [ClusterClass]", func() {
 	ClusterctlUpgradeSpec(ctx, func() ClusterctlUpgradeSpecInput {
 		return ClusterctlUpgradeSpecInput{
 			E2EConfig:              e2eConfig,
@@ -112,17 +116,21 @@ var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.4=>cur
 			ArtifactFolder:         artifactFolder,
 			SkipCleanup:            skipCleanup,
 			InfrastructureProvider: pointer.String("docker"),
-			InitWithBinary:         "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/clusterctl-{OS}-{ARCH}",
+			InitWithBinary:         "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/clusterctl-{OS}-{ARCH}",
 			// We have to pin the providers because with `InitWithProvidersContract` the test would
-			// use the latest version for the contract (which is v1.5.X for v1beta1).
-			InitWithCoreProvider:            "cluster-api:v1.4.5",
-			InitWithBootstrapProviders:      []string{"kubeadm:v1.4.5"},
-			InitWithControlPlaneProviders:   []string{"kubeadm:v1.4.5"},
-			InitWithInfrastructureProviders: []string{"docker:v1.4.5"},
-			InitWithProvidersContract:       "v1beta1",
-			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/e2e/data/infrastructure-docker/v1.4/bases.
-			InitWithKubernetesVersion: "v1.27.3",
-			WorkloadKubernetesVersion: "v1.27.3",
+			// use the latest version for the contract (which is v1.4.X for v1beta1).
+			InitWithCoreProvider:            "cluster-api:v1.3.6",
+			InitWithBootstrapProviders:      []string{"kubeadm:v1.3.6"},
+			InitWithControlPlaneProviders:   []string{"kubeadm:v1.3.6"},
+			InitWithInfrastructureProviders: []string{"docker:v1.3.6"},
+			// We have to set this to an empty array as clusterctl v1.3 doesn't support
+			// runtime extension providers. If we don't do this the test will automatically
+			// try to deploy the latest version of our test-extension from docker.yaml.
+			InitWithRuntimeExtensionProviders: []string{},
+			InitWithProvidersContract:         "v1beta1",
+			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/data/infrastructure-docker/v1.3/bases.
+			InitWithKubernetesVersion: "v1.26.4",
+			WorkloadKubernetesVersion: "v1.26.4",
 			MgmtFlavor:                "topology",
 			WorkloadFlavor:            "topology",
 			// This check ensures that ownerReference apiVersions are updated for all types after the upgrade.
@@ -140,7 +148,7 @@ var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.4=>cur
 	})
 })
 
-var _ = Describe("When testing clusterctl upgrades (v1.5=>current)", func() {
+var _ = Describe("When testing clusterctl upgrades (v1.4=>current)", func() {
 	ClusterctlUpgradeSpec(ctx, func() ClusterctlUpgradeSpecInput {
 		return ClusterctlUpgradeSpecInput{
 			E2EConfig:                 e2eConfig,
@@ -149,9 +157,9 @@ var _ = Describe("When testing clusterctl upgrades (v1.5=>current)", func() {
 			ArtifactFolder:            artifactFolder,
 			SkipCleanup:               skipCleanup,
 			InfrastructureProvider:    pointer.String("docker"),
-			InitWithBinary:            "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/clusterctl-{OS}-{ARCH}",
+			InitWithBinary:            "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/clusterctl-{OS}-{ARCH}",
 			InitWithProvidersContract: "v1beta1",
-			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/e2e/data/infrastructure-docker/v1.5/bases.
+			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/data/infrastructure-docker/v1.4/bases.
 			InitWithKubernetesVersion: "v1.27.3",
 			WorkloadKubernetesVersion: "v1.27.3",
 			MgmtFlavor:                "topology",
@@ -171,7 +179,7 @@ var _ = Describe("When testing clusterctl upgrades (v1.5=>current)", func() {
 	})
 })
 
-var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.5=>current) [ClusterClass]", func() {
+var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.4=>current) [ClusterClass]", func() {
 	ClusterctlUpgradeSpec(ctx, func() ClusterctlUpgradeSpecInput {
 		return ClusterctlUpgradeSpecInput{
 			E2EConfig:                 e2eConfig,
@@ -180,9 +188,9 @@ var _ = Describe("When testing clusterctl upgrades using ClusterClass (v1.5=>cur
 			ArtifactFolder:            artifactFolder,
 			SkipCleanup:               skipCleanup,
 			InfrastructureProvider:    pointer.String("docker"),
-			InitWithBinary:            "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/clusterctl-{OS}-{ARCH}",
+			InitWithBinary:            "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/clusterctl-{OS}-{ARCH}",
 			InitWithProvidersContract: "v1beta1",
-			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/e2e/data/infrastructure-docker/v1.5/bases.
+			// NOTE: If this version is changed here the image and SHA must also be updated in all DockerMachineTemplates in `test/data/infrastructure-docker/v1.4/bases.
 			InitWithKubernetesVersion: "v1.27.3",
 			WorkloadKubernetesVersion: "v1.27.3",
 			MgmtFlavor:                "topology",
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 0e4990ed8..c0c46ae23 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -21,7 +21,7 @@ import (
 	"fmt"
 	"path/filepath"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/ginkgo/v2"
 	"github.com/onsi/gomega/types"
 	corev1 "k8s.io/api/core/v1"
diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml
index 618ad842a..dc11eb281 100644
--- a/test/e2e/config/docker.yaml
+++ b/test/e2e/config/docker.yaml
@@ -23,11 +23,11 @@ images:
   loadBehavior: tryLoad
 - name: gcr.io/k8s-staging-cluster-api/test-extension-{ARCH}:dev
   loadBehavior: tryLoad
-- name: quay.io/jetstack/cert-manager-cainjector:v1.12.3
+- name: quay.io/jetstack/cert-manager-cainjector:v1.12.2
   loadBehavior: tryLoad
-- name: quay.io/jetstack/cert-manager-webhook:v1.12.3
+- name: quay.io/jetstack/cert-manager-webhook:v1.12.2
   loadBehavior: tryLoad
-- name: quay.io/jetstack/cert-manager-controller:v1.12.3
+- name: quay.io/jetstack/cert-manager-controller:v1.12.2
   loadBehavior: tryLoad
 
 providers:
@@ -44,25 +44,25 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1.0/metadata.yaml"
-  - name: v1.4.5 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/core-components.yaml"
+  - name: v1.3.6 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/core-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
-    - old: --metrics-addr=127.0.0.1:8080
-      new: --metrics-addr=:8080
+      - old: --metrics-addr=127.0.0.1:8080
+        new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.4/metadata.yaml"
-  - name: v1.5.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/core-components.yaml"
+      - sourcePath: "../data/shared/v1.3/metadata.yaml"
+  - name: v1.4.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/core-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
-    - old: --metrics-addr=127.0.0.1:8080
-      new: --metrics-addr=:8080
+      - old: --metrics-addr=127.0.0.1:8080
+        new: --metrics-addr=:8080
     files:
-      - sourcePath: "../data/shared/v1.5/metadata.yaml"
-  - name: v1.6.99 # next; use manifest from source files
+      - sourcePath: "../data/shared/v1.4/metadata.yaml"
+  - name: v1.5.99 # next; use manifest from source files
     value: ../../../config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -82,25 +82,25 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1.0/metadata.yaml"
-  - name: v1.4.5 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/bootstrap-components.yaml"
+  - name: v1.3.6 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/bootstrap-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.4/metadata.yaml"
-  - name: v1.5.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/bootstrap-components.yaml"
+    - sourcePath: "../data/shared/v1.3/metadata.yaml"
+  - name: v1.4.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/bootstrap-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.5/metadata.yaml"
-  - name: v1.6.99 # next; use manifest from source files
+    - sourcePath: "../data/shared/v1.4/metadata.yaml"
+  - name: v1.5.99 # next; use manifest from source files
     value: ../../../bootstrap/kubeadm/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -120,25 +120,25 @@ providers:
       new: --metrics-addr=:8080
     files:
     - sourcePath: "../data/shared/v1.0/metadata.yaml"
-  - name: v1.4.5 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/control-plane-components.yaml"
+  - name: v1.3.6 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/control-plane-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.4/metadata.yaml"
-  - name: v1.5.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/control-plane-components.yaml"
+    - sourcePath: "../data/shared/v1.3/metadata.yaml"
+  - name: v1.4.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/control-plane-components.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.5/metadata.yaml"
-  - name: v1.6.99 # next; use manifest from source files
+    - sourcePath: "../data/shared/v1.4/metadata.yaml"
+  - name: v1.5.99 # next; use manifest from source files
     value: ../../../controlplane/kubeadm/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -159,31 +159,31 @@ providers:
     files:
     - sourcePath: "../data/shared/v1.0/metadata.yaml"
     - sourcePath: "../data/infrastructure-docker/v1.0/cluster-template.yaml"
-  - name: v1.4.5 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.5/infrastructure-components-development.yaml"
+  - name: v1.3.6 # supported release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.3.6/infrastructure-components-development.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.4/metadata.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.4/cluster-template.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.4/cluster-template-topology.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.4/clusterclass-quick-start.yaml"
-  - name: v1.5.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
-    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.5.0/infrastructure-components-development.yaml"
+    - sourcePath: "../data/shared/v1.3/metadata.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.3/cluster-template.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.3/cluster-template-topology.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.3/clusterclass-quick-start.yaml"
+  - name: v1.4.0 # latest published release in the v1beta1 series; this is used for v1beta1 --> main clusterctl upgrades test only.
+    value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.4.0/infrastructure-components-development.yaml"
     type: "url"
     contract: v1beta1
     replacements:
     - old: --metrics-addr=127.0.0.1:8080
       new: --metrics-addr=:8080
     files:
-    - sourcePath: "../data/shared/v1.5/metadata.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.5/cluster-template.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.5/cluster-template-topology.yaml"
-    - sourcePath: "../data/infrastructure-docker/v1.5/clusterclass-quick-start.yaml"
-  - name: v1.6.99 # next; use manifest from source files
+    - sourcePath: "../data/shared/v1.4/metadata.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.4/cluster-template.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.4/cluster-template-topology.yaml"
+    - sourcePath: "../data/infrastructure-docker/v1.4/clusterclass-quick-start.yaml"
+  - name: v1.5.99 # next; use manifest from source files
     value: ../../../test/infrastructure/docker/config/default
     replacements:
     - old: --metrics-bind-addr=localhost:8080
@@ -213,21 +213,21 @@ providers:
 - name: in-memory
   type: InfrastructureProvider
   versions:
-    - name: v1.6.99 # next; use manifest from source files
+    - name: v1.5.99 # next; use manifest from source files
       value: ../../../test/infrastructure/inmemory/config/default
       replacements:
         - old: --metrics-bind-addr=localhost:8080
           new: "--metrics-bind-addr=:8080\n        - --logging-format=json"
       files:
-      # Add cluster templates
-      - sourcePath: "../data/infrastructure-inmemory/main/clusterclass-in-memory.yaml"
-      - sourcePath: "../data/infrastructure-inmemory/main/cluster-template.yaml"
-      - sourcePath: "../data/shared/main/metadata.yaml"
+        # Add cluster templates
+        - sourcePath: "../data/infrastructure-inmemory/main/clusterclass-in-memory.yaml"
+        - sourcePath: "../data/infrastructure-inmemory/main/cluster-template.yaml"
+        - sourcePath: "../data/shared/main/metadata.yaml"
 
 - name: test-extension
   type: RuntimeExtensionProvider
   versions:
-    - name: v1.6.99 # next; use manifest from source files
+    - name: v1.5.99 # next; use manifest from source files
       value: ../../../test/extension/config/default
       files:
       - sourcePath: "../data/shared/main/metadata.yaml"
diff --git a/test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-kcp.yaml b/test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-kcp.yaml
similarity index 79%
rename from test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-kcp.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-kcp.yaml
index 300791672..d23e53435 100644
--- a/test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-kcp.yaml
+++ b/test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-kcp.yaml
@@ -56,6 +56,8 @@ metadata:
 spec:
   template:
     spec:
+      # NOTE: If the Kubernetes version is changed in `clusterctl_upgrade_test.go` the image and SHA must be updated here.
+      customImage: "kindest/node:v1.26.4@sha256:f4c0d87be03d6bea69f5e5dc0adb678bb498a190ee5c38422bf751541cebe92e"
       extraMounts:
         - containerPath: "/var/run/docker.sock"
           hostPath: "/var/run/docker.sock"
@@ -85,7 +87,15 @@ spec:
         # host.docker.internal is required by kubetest when running on MacOS because of the way ports are proxied.
         certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal]
     initConfiguration:
-      nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
+      nodeRegistration:
+        criSocket: unix:///var/run/containerd/containerd.sock
+        kubeletExtraArgs:
+          eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+          fail-swap-on: "false"
     joinConfiguration:
-      nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
+      nodeRegistration:
+        criSocket: unix:///var/run/containerd/containerd.sock
+        kubeletExtraArgs:
+          eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+          fail-swap-on: "false"
   version: "${KUBERNETES_VERSION}"
diff --git a/test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-topology.yaml b/test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-topology.yaml
similarity index 65%
rename from test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-topology.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-topology.yaml
index 529fc15f6..5d03ea309 100644
--- a/test/e2e/data/infrastructure-docker/v1.5/bases/cluster-with-topology.yaml
+++ b/test/e2e/data/infrastructure-docker/v1.3/bases/cluster-with-topology.yaml
@@ -18,11 +18,9 @@ spec:
     controlPlane:
       metadata:
         labels:
-          Cluster.topology.controlPlane.label: "Cluster.topology.controlPlane.labelValue"
-          # Note: this label is propagated to Nodes.
-          Cluster.topology.controlPlane.label.node.cluster.x-k8s.io: "Cluster.topology.controlPlane.nodeLabelValue"
+          label-from-cluster-topology: "value-from-cluster-topology"
         annotations:
-          Cluster.topology.controlPlane.annotation: "Cluster.topology.controlPlane.annotationValue"
+          annotation-from-cluster-topology: "value-from-cluster-topology"
       nodeDeletionTimeout: "30s"
       nodeVolumeDetachTimeout: "5m"
       replicas: ${CONTROL_PLANE_MACHINE_COUNT}
@@ -32,18 +30,15 @@ spec:
         name: "md-0"
         metadata:
           labels:
-            Cluster.topology.machineDeployment.label: "Cluster.topology.machineDeployment.labelValue"
-            # Note: this label is propagated to Nodes.
-            Cluster.topology.machineDeployment.label.node.cluster.x-k8s.io: "Cluster.topology.machineDeployment.nodeLabelValue"
+            label-from-cluster-topology: "value-from-cluster-topology"
           annotations:
-            Cluster.topology.machineDeployment.annotation: "Cluster.topology.machineDeployment.annotationValue"
+            annotation-from-cluster-topology: "value-from-cluster-topology"
         nodeDeletionTimeout: "30s"
         nodeVolumeDetachTimeout: "5m"
         minReadySeconds: 5
         replicas: ${WORKER_MACHINE_COUNT}
         failureDomain: fd4
         strategy:
-          type: RollingUpdate
           rollingUpdate:
             maxSurge: "20%"
             maxUnavailable: 0
diff --git a/test/e2e/data/infrastructure-docker/v1.5/bases/crs.yaml b/test/e2e/data/infrastructure-docker/v1.3/bases/crs.yaml
similarity index 100%
rename from test/e2e/data/infrastructure-docker/v1.5/bases/crs.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/bases/crs.yaml
diff --git a/test/e2e/data/infrastructure-docker/v1.5/bases/md.yaml b/test/e2e/data/infrastructure-docker/v1.3/bases/md.yaml
similarity index 75%
rename from test/e2e/data/infrastructure-docker/v1.5/bases/md.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/bases/md.yaml
index 5d42a2cf5..8cd39f3ea 100644
--- a/test/e2e/data/infrastructure-docker/v1.5/bases/md.yaml
+++ b/test/e2e/data/infrastructure-docker/v1.3/bases/md.yaml
@@ -8,6 +8,8 @@ metadata:
 spec:
   template:
     spec:
+      # NOTE: If the Kubernetes version is changed in `clusterctl_upgrade_test.go` the image and SHA must be updated here.
+      customImage: "kindest/node:v1.26.4@sha256:f4c0d87be03d6bea69f5e5dc0adb678bb498a190ee5c38422bf751541cebe92e"
       extraMounts:
         - containerPath: "/var/run/docker.sock"
           hostPath: "/var/run/docker.sock"
@@ -23,7 +25,11 @@ spec:
   template:
     spec:
       joinConfiguration:
-        nodeRegistration: {} # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
+        nodeRegistration:
+          criSocket: unix:///var/run/containerd/containerd.sock
+          kubeletExtraArgs:
+            eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+            fail-swap-on: "false"
 ---
 # MachineDeployment object
 apiVersion: cluster.x-k8s.io/v1beta1
diff --git a/test/e2e/data/infrastructure-docker/v1.5/cluster-template-topology/kustomization.yaml b/test/e2e/data/infrastructure-docker/v1.3/cluster-template-topology/kustomization.yaml
similarity index 100%
rename from test/e2e/data/infrastructure-docker/v1.5/cluster-template-topology/kustomization.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/cluster-template-topology/kustomization.yaml
diff --git a/test/e2e/data/infrastructure-docker/v1.5/cluster-template/kustomization.yaml b/test/e2e/data/infrastructure-docker/v1.3/cluster-template/kustomization.yaml
similarity index 100%
rename from test/e2e/data/infrastructure-docker/v1.5/cluster-template/kustomization.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/cluster-template/kustomization.yaml
diff --git a/test/e2e/data/infrastructure-docker/v1.5/clusterclass-quick-start.yaml b/test/e2e/data/infrastructure-docker/v1.3/clusterclass-quick-start.yaml
similarity index 58%
rename from test/e2e/data/infrastructure-docker/v1.5/clusterclass-quick-start.yaml
rename to test/e2e/data/infrastructure-docker/v1.3/clusterclass-quick-start.yaml
index 3d747dd49..84a9847fc 100644
--- a/test/e2e/data/infrastructure-docker/v1.5/clusterclass-quick-start.yaml
+++ b/test/e2e/data/infrastructure-docker/v1.3/clusterclass-quick-start.yaml
@@ -6,9 +6,9 @@ spec:
   controlPlane:
     metadata:
       labels:
-        ClusterClass.controlPlane.label: "ClusterClass.controlPlane.labelValue"
+        label-from-clusterclass: "value-from-clusterclass"
       annotations:
-        ClusterClass.controlPlane.annotation: "ClusterClass.controlPlane.annotationValue"
+        annotation-from-clusterclass: "value-from-clusterclass"
     ref:
       apiVersion: controlplane.cluster.x-k8s.io/v1beta1
       kind: KubeadmControlPlaneTemplate
@@ -35,9 +35,9 @@ spec:
       template:
         metadata:
           labels:
-            ClusterClass.machineDeployment.label: "ClusterClass.machineDeployment.labelValue"
+            label-from-clusterclass: "value-from-clusterclass"
           annotations:
-            ClusterClass.machineDeployment.annotation: "ClusterClass.machineDeployment.annotationValue"
+            annotation-from-clusterclass: "value-from-clusterclass"
         bootstrap:
           ref:
             apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
@@ -100,18 +100,6 @@ spec:
       openAPIV3Schema:
         type: boolean
         default: true
-  - name: externalCloudProvider
-    required: false
-    schema:
-      openAPIV3Schema:
-        type: boolean
-        default: false
-  - name: ipv6Primary
-    required: false
-    schema:
-      openAPIV3Schema:
-        type: boolean
-        default: false
   patches:
   - name: lbImageRepository
     definitions:
@@ -126,6 +114,44 @@ spec:
         valueFrom:
           template: |
             imageRepository: {{ .lbImageRepository }}
+  # We have to pin the cgroupDriver to cgroupfs for Kubernetes < v1.24 because kind does not support systemd for those versions, but kubeadm >= 1.21 defaults to systemd.
+  - name: cgroupDriver-controlPlane
+    description: |
+      Sets the cgroupDriver to cgroupfs if a Kubernetes version < v1.24 is referenced.
+      This is required because kind and the node images do not support the default
+      systemd cgroupDriver for kubernetes < v1.24.
+    enabledIf: '{{ semverCompare "<= v1.23" .builtin.controlPlane.version }}'
+    definitions:
+    - selector:
+        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+        kind: KubeadmControlPlaneTemplate
+        matchResources:
+          controlPlane: true
+      jsonPatches:
+      - op: add
+        path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/nodeRegistration/kubeletExtraArgs/cgroup-driver"
+        value: cgroupfs
+      - op: add
+        path: "/spec/template/spec/kubeadmConfigSpec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cgroup-driver"
+        value: cgroupfs
+  - name: cgroupDriver-machineDeployment
+    description: |
+      Sets the cgroupDriver to cgroupfs if a Kubernetes version < v1.24 is referenced.
+      This is required because kind and the node images do not support the default
+      systemd cgroupDriver for kubernetes < v1.24.
+    enabledIf: '{{ semverCompare "<= v1.23" .builtin.machineDeployment.version }}'
+    definitions:
+    - selector:
+        apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+        kind: KubeadmConfigTemplate
+        matchResources:
+          machineDeploymentClass:
+            names:
+            - default-worker
+      jsonPatches:
+      - op: add
+        path: "/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cgroup-driver"
+        value: cgroupfs
   - name: etcdImageTag
     description: "Sets tag to use for the etcd image in the KubeadmControlPlane."
     definitions:
@@ -155,33 +181,6 @@ spec:
         valueFrom:
           template: |
             imageTag: {{ .coreDNSImageTag }}
-  - name: customImage
-    description: "Sets the container image that is used for running dockerMachines for the controlPlane and default-worker machineDeployments."
-    definitions:
-      - selector:
-          apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
-          kind: DockerMachineTemplate
-          matchResources:
-            machineDeploymentClass:
-              names:
-                - default-worker
-        jsonPatches:
-          - op: add
-            path: "/spec/template/spec/customImage"
-            valueFrom:
-              template: |
-                kindest/node:{{ .builtin.machineDeployment.version | replace "+" "_" }}
-      - selector:
-          apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
-          kind: DockerMachineTemplate
-          matchResources:
-            controlPlane: true
-        jsonPatches:
-          - op: add
-            path: "/spec/template/spec/customImage"
-            valueFrom:
-              template: |
-                kindest/node:{{ .builtin.controlPlane.version | replace "+" "_" }}
   - name: preloadImages
     description: |
       Sets the container images to preload to the node that is used for running dockerMachines.
@@ -230,54 +229,6 @@ spec:
       - op: add
         path: "/spec/template/spec/kubeadmConfigSpec/joinConfiguration/nodeRegistration/taints"
         value: []
-  - name: controlPlaneExternalCloudProvider
-    enabledIf: "{{ .externalCloudProvider }}"
-    description: "Configures kubelet to run with an external cloud provider for control plane nodes."
-    definitions:
-      - selector:
-          apiVersion: controlplane.cluster.x-k8s.io/v1beta1
-          kind: KubeadmControlPlaneTemplate
-          matchResources:
-            controlPlane: true
-        jsonPatches:
-          - op: add
-            path: "/spec/template/spec/kubeadmConfigSpec/joinConfiguration/nodeRegistration/kubeletExtraArgs"
-            value:
-              cloud-provider: "external"
-          - op: add
-            path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/nodeRegistration/kubeletExtraArgs"
-            value:
-              cloud-provider: "external"
-  - name: machineDeploymentExternalCloudProvider
-    enabledIf: "{{ .externalCloudProvider }}"
-    description: "Configures kubelet to run with an external cloud provider for machineDeployment nodes."
-    definitions:
-      - selector:
-          apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
-          kind: KubeadmConfigTemplate
-          matchResources:
-            machineDeploymentClass:
-              names:
-                - '*-worker'
-        jsonPatches:
-          - op: add
-            path: "/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs"
-            value:
-              cloud-provider: "external"
-  - name: localEndpointIPv6
-    enabledIf: "{{ .ipv6Primary }}"
-    description: "Configures KCP to use IPv6 for its localAPIEndpoint."
-    definitions:
-      - selector:
-          apiVersion: controlplane.cluster.x-k8s.io/v1beta1
-          kind: KubeadmControlPlaneTemplate
-          matchResources:
-            controlPlane: true
-        jsonPatches:
-          - op: add
-            path: "/spec/template/spec/kubeadmConfigSpec/initConfiguration/localAPIEndpoint"
-            value:
-              advertiseAddress: '::'
   - name: podSecurityStandard
     description: "Adds an admission configuration for PodSecurity to the kube-apiserver."
     definitions:
@@ -329,17 +280,8 @@ apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: DockerClusterTemplate
 metadata:
   name: quick-start-cluster
-  labels:
-    InfrastructureClusterTemplate.label: "InfrastructureClusterTemplate.labelValue"
-  annotations:
-    InfrastructureClusterTemplate.annotation: "InfrastructureClusterTemplate.annotationValue"
 spec:
   template:
-    metadata:
-      labels:
-        InfrastructureClusterTemplate.template.label: "InfrastructureClusterTemplate.template.labelValue"
-      annotations:
-        InfrastructureClusterTemplate.template.annotation: "InfrastructureClusterTemplate.template.annotationValue"
     spec:
       failureDomains:
         fd1:
@@ -363,26 +305,12 @@ kind: KubeadmControlPlaneTemplate
 apiVersion: controlplane.cluster.x-k8s.io/v1beta1
 metadata:
   name: quick-start-control-plane
-  labels:
-    ControlPlaneTemplate.label: "ControlPlaneTemplate.labelValue"
-  annotations:
-    ControlPlaneTemplate.annotation: "ControlPlaneTemplate.annotationValue"
 spec:
   template:
-    metadata:
-      labels:
-        ControlPlaneTemplate.template.label: "ControlPlaneTemplate.template.labelValue"
-      annotations:
-        ControlPlaneTemplate.template.annotation: "ControlPlaneTemplate.template.annotationValue"
     spec:
       rolloutBefore:
         certificatesExpiryDays: 21
       machineTemplate:
-        metadata:
-          labels:
-            ControlPlaneTemplate.machineTemplate.label: "ControlPlaneTemplate.machineTemplate.labelValue"
-          annotations:
-            ControlPlaneTemplate.machineTemplate.annotation: "ControlPlaneTemplate.machineTemplate.annotationValue"
         nodeDrainTimeout: 1s
       kubeadmConfigSpec:
         clusterConfiguration:
@@ -390,32 +318,31 @@ spec:
             extraArgs: { enable-hostpath-provisioner: 'true' }
           apiServer:
             # host.docker.internal is required by kubetest when running on MacOS because of the way ports are proxied.
-            certSANs: [localhost, host.docker.internal, "::", "::1", "127.0.0.1", "0.0.0.0"]
+            certSANs: [localhost, 127.0.0.1, 0.0.0.0, host.docker.internal]
         initConfiguration:
-          nodeRegistration: # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
-            kubeletExtraArgs: # having a not empty kubeletExtraArgs is required for the externalCloudProvider patch to work
+          nodeRegistration:
+            # We have to set the criSocket to containerd as kubeadm defaults to docker runtime if both containerd and docker sockets are found
+            criSocket: unix:///var/run/containerd/containerd.sock
+            kubeletExtraArgs:
               eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+              fail-swap-on: "false"
         joinConfiguration:
-          nodeRegistration: # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
-            kubeletExtraArgs: # having a not empty kubeletExtraArgs is required for the externalCloudProvider patch to work
+          nodeRegistration:
+            # We have to set the criSocket to containerd as kubeadm defaults to docker runtime if both containerd and docker sockets are found
+            criSocket: unix:///var/run/containerd/containerd.sock
+            kubeletExtraArgs:
               eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+              fail-swap-on: "false"
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: DockerMachineTemplate
 metadata:
   name: quick-start-control-plane
-  labels:
-    InfraMachineTemplate.controlPlane.label: "InfraMachineTemplate.controlPlane.labelValue"
-  annotations:
-    InfraMachineTemplate.controlPlane.annotation: "InfraMachineTemplate.controlPlane.annotationValue"
 spec:
   template:
-    metadata:
-      labels:
-        InfraMachineTemplate.controlPlane.template.label: "InfraMachineTemplate.controlPlane.template.labelValue"
-      annotations:
-        InfraMachineTemplate.controlPlane.template.annotation: "InfraMachineTemplate.controlPlane.template.annotationValue"
     spec:
+      # NOTE: If the Kubernetes version is changed in `clusterctl_upgrade_test.go` the image and SHA must be updated here.
+      customImage: "kindest/node:v1.26.4@sha256:f4c0d87be03d6bea69f5e5dc0adb678bb498a190ee5c38422bf751541cebe92e"
       extraMounts:
       - containerPath: "/var/run/docker.sock"
         hostPath: "/var/run/docker.sock"
@@ -424,18 +351,11 @@ apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: DockerMachineTemplate
 metadata:
   name: quick-start-default-worker-machinetemplate
-  labels:
-    InfraMachineTemplate.machineDeployment.label: "InfraMachineTemplate.machineDeployment.labelValue"
-  annotations:
-    InfraMachineTemplate.machineDeployment.annotation: "InfraMachineTemplate.machineDeployment.annotationValue"
 spec:
   template:
-    metadata:
-      labels:
-        InfraMachineTemplate.machineDeployment.template.label: "InfraMachineTemplate.machineDeployment.template.labelValue"
-      annotations:
-        InfraMachineTemplate.machineDeployment.template.annotation: "InfraMachineTemplate.machineDeployment.template.annotationValue"
     spec:
+      # NOTE: If the Kubernetes version is changed in `clusterctl_upgrade_test.go` the image and SHA must be updated here.
+      customImage: "kindest/node:v1.26.4@sha256:f4c0d87be03d6bea69f5e5dc0adb678bb498a190ee5c38422bf751541cebe92e"
       extraMounts:
       - containerPath: "/var/run/docker.sock"
         hostPath: "/var/run/docker.sock"
@@ -444,19 +364,14 @@ apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
 kind: KubeadmConfigTemplate
 metadata:
   name: quick-start-default-worker-bootstraptemplate
-  labels:
-    BootstrapConfigTemplate.machineDeployment.label: "BootstrapConfigTemplate.machineDeployment.labelValue"
-  annotations:
-    BootstrapConfigTemplate.machineDeployment.annotation: "BootstrapConfigTemplate.machineDeployment.annotationValue"
 spec:
   template:
-    metadata:
-      labels:
-        BootstrapConfigTemplate.machineDeployment.template.label: "BootstrapConfigTemplate.machineDeployment.template.labelValue"
-      annotations:
-        BootstrapConfigTemplate.machineDeployment.template.annotation: "BootstrapConfigTemplate.machineDeployment.template.annotationValue"
     spec:
       joinConfiguration:
-        nodeRegistration: # node registration parameters are automatically injected by CAPD according to the kindest/node image in use.
-          kubeletExtraArgs: # having a not empty kubeletExtraArgs is required for the externalCloudProvider to work
+        nodeRegistration:
+          # We have to set the criSocket to containerd as kubeadm defaults to docker runtime if both containerd and docker sockets are found
+          criSocket: unix:///var/run/containerd/containerd.sock
+          kubeletExtraArgs:
             eviction-hard: 'nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%'
+            fail-swap-on: "false"
+
diff --git a/test/e2e/data/shared/main/metadata.yaml b/test/e2e/data/shared/main/metadata.yaml
index 0fedda628..87cd651cb 100644
--- a/test/e2e/data/shared/main/metadata.yaml
+++ b/test/e2e/data/shared/main/metadata.yaml
@@ -1,9 +1,6 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
-  - major: 1
-    minor: 6
-    contract: v1beta1
   - major: 1
     minor: 5
     contract: v1beta1
diff --git a/test/e2e/data/shared/v1.5/metadata.yaml b/test/e2e/data/shared/v1.3/metadata.yaml
similarity index 81%
rename from test/e2e/data/shared/v1.5/metadata.yaml
rename to test/e2e/data/shared/v1.3/metadata.yaml
index 87cd651cb..ff9cdc101 100644
--- a/test/e2e/data/shared/v1.5/metadata.yaml
+++ b/test/e2e/data/shared/v1.3/metadata.yaml
@@ -1,12 +1,6 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
-  - major: 1
-    minor: 5
-    contract: v1beta1
-  - major: 1
-    minor: 4
-    contract: v1beta1
   - major: 1
     minor: 3
     contract: v1beta1
diff --git a/test/extension/handlers/topologymutation/handler.go b/test/extension/handlers/topologymutation/handler.go
index dc61ce921..1cae18815 100644
--- a/test/extension/handlers/topologymutation/handler.go
+++ b/test/extension/handlers/topologymutation/handler.go
@@ -25,7 +25,7 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/test/framework/clusterctl/clusterctl_helpers.go b/test/framework/clusterctl/clusterctl_helpers.go
index d970a5e3f..0b59feb29 100644
--- a/test/framework/clusterctl/clusterctl_helpers.go
+++ b/test/framework/clusterctl/clusterctl_helpers.go
@@ -315,7 +315,6 @@ func ApplyClusterTemplateAndWait(ctx context.Context, input ApplyClusterTemplate
 		ClusterName:                  input.ConfigCluster.ClusterName,
 		Namespace:                    input.ConfigCluster.Namespace,
 		CNIManifestPath:              input.CNIManifestPath,
-		Flavor:                       input.ConfigCluster.Flavor,
 		WaitForClusterIntervals:      input.WaitForClusterIntervals,
 		WaitForControlPlaneIntervals: input.WaitForControlPlaneIntervals,
 		WaitForMachineDeployments:    input.WaitForMachineDeployments,
@@ -334,7 +333,6 @@ type ApplyCustomClusterTemplateAndWaitInput struct {
 	ClusterName                  string
 	Namespace                    string
 	CNIManifestPath              string
-	Flavor                       string
 	WaitForClusterIntervals      []interface{}
 	WaitForControlPlaneIntervals []interface{}
 	WaitForMachineDeployments    []interface{}
diff --git a/test/framework/clusterctl/repository.go b/test/framework/clusterctl/repository.go
index 42c855a54..ff2067828 100644
--- a/test/framework/clusterctl/repository.go
+++ b/test/framework/clusterctl/repository.go
@@ -28,7 +28,7 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 
diff --git a/test/framework/daemonset_helpers.go b/test/framework/daemonset_helpers.go
index a8322309a..40510638e 100644
--- a/test/framework/daemonset_helpers.go
+++ b/test/framework/daemonset_helpers.go
@@ -19,7 +19,7 @@ package framework
 import (
 	"context"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/v1"
diff --git a/test/framework/kubernetesversions/versions.go b/test/framework/kubernetesversions/versions.go
index 4af7cc3f8..6340229a1 100644
--- a/test/framework/kubernetesversions/versions.go
+++ b/test/framework/kubernetesversions/versions.go
@@ -23,7 +23,7 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 )
 
diff --git a/test/go.mod b/test/go.mod
index c3f1c4840..1cc32a7ed 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -5,7 +5,7 @@ go 1.20
 replace sigs.k8s.io/cluster-api => ../
 
 require (
-	github.com/blang/semver/v4 v4.0.0
+	github.com/blang/semver v3.5.1+incompatible
 	github.com/docker/docker v24.0.5+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/emicklei/go-restful/v3 v3.10.2
@@ -51,6 +51,7 @@ require (
 	github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.3 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/cloudflare/circl v1.3.3 // indirect
@@ -142,7 +143,7 @@ require (
 	golang.org/x/text v0.12.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/tools v0.9.3 // indirect
-	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
diff --git a/test/go.sum b/test/go.sum
index 37c0fb146..4978eb7a9 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -85,6 +85,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@@ -843,8 +845,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc=
+gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/test/infrastructure/docker/exp/internal/docker/nodepool.go b/test/infrastructure/docker/exp/internal/docker/nodepool.go
index 12920be70..08683dea8 100644
--- a/test/infrastructure/docker/exp/internal/docker/nodepool.go
+++ b/test/infrastructure/docker/exp/internal/docker/nodepool.go
@@ -25,7 +25,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/klog/v2"
diff --git a/test/infrastructure/docker/internal/docker/machine.go b/test/infrastructure/docker/internal/docker/machine.go
index 96c9df5ed..c65c9a882 100644
--- a/test/infrastructure/docker/internal/docker/machine.go
+++ b/test/infrastructure/docker/internal/docker/machine.go
@@ -26,7 +26,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
diff --git a/test/infrastructure/docker/internal/provisioning/cloudinit/adapter_test.go b/test/infrastructure/docker/internal/provisioning/cloudinit/adapter_test.go
index 1a4fdcea8..759d0550b 100644
--- a/test/infrastructure/docker/internal/provisioning/cloudinit/adapter_test.go
+++ b/test/infrastructure/docker/internal/provisioning/cloudinit/adapter_test.go
@@ -19,7 +19,7 @@ package cloudinit
 import (
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 
 	"sigs.k8s.io/cluster-api/test/infrastructure/docker/internal/provisioning"
diff --git a/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles.go b/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles.go
index aa47d0db2..6e31a29f9 100644
--- a/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles.go
+++ b/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles.go
@@ -25,7 +25,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	"sigs.k8s.io/yaml"
 
diff --git a/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles_test.go b/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles_test.go
index 28902c292..28077c652 100644
--- a/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles_test.go
+++ b/test/infrastructure/docker/internal/provisioning/cloudinit/writefiles_test.go
@@ -21,7 +21,7 @@ import (
 	"compress/gzip"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	"github.com/google/go-cmp/cmp"
 	. "github.com/onsi/gomega"
 
diff --git a/test/infrastructure/kind/mapper.go b/test/infrastructure/kind/mapper.go
index d6db884d7..4bdb5e77f 100644
--- a/test/infrastructure/kind/mapper.go
+++ b/test/infrastructure/kind/mapper.go
@@ -37,7 +37,7 @@ package kind
 import (
 	"fmt"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 
 	clusterapicontainer "sigs.k8s.io/cluster-api/util/container"
 )
diff --git a/test/infrastructure/kind/mapper_test.go b/test/infrastructure/kind/mapper_test.go
index 863669112..42bf2556d 100644
--- a/test/infrastructure/kind/mapper_test.go
+++ b/test/infrastructure/kind/mapper_test.go
@@ -19,7 +19,7 @@ package kind
 import (
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 )
 
diff --git a/util/annotations/helpers_test.go b/util/annotations/helpers_test.go
index de973cfb9..9793fcf87 100644
--- a/util/annotations/helpers_test.go
+++ b/util/annotations/helpers_test.go
@@ -22,13 +22,12 @@ import (
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
 func TestAddAnnotations(t *testing.T) {
 	g := NewWithT(t)
 
-	testcases := []struct {
+	var testcases = []struct {
 		name     string
 		obj      metav1.Object
 		input    map[string]string
@@ -142,39 +141,6 @@ func TestAddAnnotations(t *testing.T) {
 			},
 			changed: true,
 		},
-		{
-			name: "should add annotations to an empty unstructured",
-			obj:  &unstructured.Unstructured{},
-			input: map[string]string{
-				"foo": "buzz",
-			},
-			expected: map[string]string{
-				"foo": "buzz",
-			},
-			changed: true,
-		},
-		{
-			name: "should add annotations to a non empty unstructured",
-			obj: &unstructured.Unstructured{
-				Object: map[string]interface{}{
-					"metadata": map[string]interface{}{
-						"annotations": map[string]interface{}{
-							"foo": "bar",
-						},
-					},
-				},
-			},
-			input: map[string]string{
-				"thing1": "thing2",
-				"buzz":   "blah",
-			},
-			expected: map[string]string{
-				"foo":    "bar",
-				"thing1": "thing2",
-				"buzz":   "blah",
-			},
-			changed: true,
-		},
 	}
 
 	for _, tc := range testcases {
diff --git a/util/util_test.go b/util/util_test.go
index 748ca0cbc..c736c02b6 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -21,7 +21,7 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
diff --git a/util/version/version_test.go b/util/version/version_test.go
index abe9501b0..a76ea9d2c 100644
--- a/util/version/version_test.go
+++ b/util/version/version_test.go
@@ -19,7 +19,7 @@ package version
 import (
 	"testing"
 
-	"github.com/blang/semver/v4"
+	"github.com/blang/semver"
 	. "github.com/onsi/gomega"
 )