Repository: kubernetes-sigs/cluster-api
Pull request #7152 (2022-09-01 18:13:18)

🌱 logging: adjust reconcilers to log object owners 
Signed-off-by: Stefan Büringer buringerst@vmware.com

<!-- please add a 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**:
With this PR we're logging all relevant k/v pairs for all our resources.

This PR also contains 1-2 smaller other logging fixes and an improvement to our Tiltfile so the ServiceAccount of Grafana is **always** deployed before the Grafana deployment.

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #


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

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

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["sbueringer"]} -->
sbueringer: Puhsed unit tests and documented the new fund in the migration doc.

Patch:
diff --git a/Makefile b/Makefile
index 0974f5188..73d2a0a21 100644
--- a/Makefile
+++ b/Makefile
@@ -176,9 +176,6 @@ CLUSTERCTL_IMG ?= $(REGISTRY)/$(CLUSTERCTL_IMAGE_NAME)
 TEST_EXTENSION_IMAGE_NAME ?= test-extension
 TEST_EXTENSION_IMG ?= $(REGISTRY)/$(TEST_EXTENSION_IMAGE_NAME)
 
-# kind
-CAPI_KIND_CLUSTER_NAME ?= capi-test
-
 # It is set by Prow GIT_TAG, a git-based tag of the form vYYYYMMDD-hash, e.g., v20210120-v0.3.10-308-gc61521971
 
 TAG ?= dev
@@ -687,10 +684,6 @@ test-capd-junit: $(SETUP_ENVTEST) $(GOTESTSUM) ## Run unit and integration tests
 kind-cluster: ## Create a new kind cluster designed for development with Tilt
 	hack/kind-install-for-capd.sh
 
-.PHONY: tilt-up
-tilt-up: kind-cluster ## Start tilt and build kind cluster if needed.
-	tilt up
-
 .PHONY: docker-build-e2e
 docker-build-e2e: ## Rebuild all Cluster API provider images to be used in the e2e tests
 	$(MAKE) docker-build REGISTRY=gcr.io/k8s-staging-cluster-api PULL_POLICY=IfNotPresent
@@ -755,7 +748,7 @@ manifest-modification: # Set the manifest images to the staging/production bucke
 	$(MAKE) set-manifest-pull-policy PULL_POLICY=IfNotPresent TARGET_RESOURCE="./bootstrap/kubeadm/config/default/manager_pull_policy.yaml"
 	$(MAKE) set-manifest-pull-policy PULL_POLICY=IfNotPresent TARGET_RESOURCE="./controlplane/kubeadm/config/default/manager_pull_policy.yaml"
 
-.PHONY: manifest-modification-dev
+.PHONE: manifest-modification-dev
 manifest-modification-dev: # Set the manifest images to the staging bucket.
 	$(MAKE) set-manifest-image \
 		MANIFEST_IMG=$(REGISTRY)/$(CAPD_IMAGE_NAME) MANIFEST_TAG=$(RELEASE_TAG) \
@@ -956,32 +949,15 @@ set-manifest-image:
 ##@ clean:
 
 .PHONY: clean
-clean: ## Remove generated binaries, GitBook files, Helm charts, and Tilt build files
+clean: ## Remove all generated files
 	$(MAKE) clean-bin
 	$(MAKE) clean-book
-	$(MAKE) clean-charts
-	$(MAKE) clean-tilt
-
-.PHONY: clean-kind
-clean-kind: ## Cleans up the kind cluster with the name $CAPI_KIND_CLUSTER_NAME
-	kind delete cluster --name="$(CAPI_KIND_CLUSTER_NAME)" || true
 
 .PHONY: clean-bin
 clean-bin: ## Remove all generated binaries
 	rm -rf $(BIN_DIR)
 	rm -rf $(TOOLS_BIN_DIR)
 
-.PHONY: clean-tilt
-clean-tilt: ## Remove all files generated by Tilt
-	rm -rf ./.tiltbuild
-	rm -rf ./controlplane/kubeadm/.tiltbuild
-	rm -rf ./bootstrap/kubeadm/.tiltbuild
-	rm -rf ./test/infrastructure/docker/.tiltbuild
-
-.PHONY: clean-charts
-clean-charts: ## Remove all local copies of Helm charts in ./hack/observability
-	(for path in "./hack/observability/*"; do rm -rf $$path/charts ; done)
-
 .PHONY: clean-book
 clean-book: ## Remove all generated GitBook files
 	rm -rf ./docs/book/_book
diff --git a/Tiltfile b/Tiltfile
index b5f95b689..6ce86ae3e 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -474,7 +474,7 @@ def deploy_observability():
 
     if "grafana" in settings.get("deploy_observability", []):
         k8s_yaml(read_file("./.tiltbuild/yaml/grafana.observability.yaml"), allow_duplicates = True)
-        k8s_resource(workload = "grafana", port_forwards = "3001:3000", extra_pod_selectors = [{"app": "grafana"}], labels = ["observability"])
+        k8s_resource(workload = "grafana", port_forwards = "3001:3000", extra_pod_selectors = [{"app": "grafana"}], labels = ["observability"], objects = ["grafana:serviceaccount"])
 
     if "prometheus" in settings.get("deploy_observability", []):
         k8s_yaml(read_file("./.tiltbuild/yaml/prometheus.observability.yaml"), allow_duplicates = True)
diff --git a/bootstrap/kubeadm/config/rbac/role.yaml b/bootstrap/kubeadm/config/rbac/role.yaml
index 256ee6b63..df06d19c5 100644
--- a/bootstrap/kubeadm/config/rbac/role.yaml
+++ b/bootstrap/kubeadm/config/rbac/role.yaml
@@ -42,6 +42,7 @@ rules:
   - machinepools/status
   - machines
   - machines/status
+  - machinesets
   verbs:
   - get
   - list
diff --git a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
index 7a4743826..608fd093a 100644
--- a/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
+++ b/bootstrap/kubeadm/internal/controllers/kubeadmconfig_controller.go
@@ -52,6 +52,7 @@ import (
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
+	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 	"sigs.k8s.io/cluster-api/util/secret"
@@ -74,7 +75,7 @@ type InitLocker interface {
 }
 
 // +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=kubeadmconfigs;kubeadmconfigs/status;kubeadmconfigs/finalizers,verbs=get;list;watch;create;update;patch;delete
-// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status;machines;machines/status;machinepools;machinepools/status,verbs=get;list;watch
+// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status;machinesets;machines;machines/status;machinepools;machinepools/status,verbs=get;list;watch
 // +kubebuilder:rbac:groups="",resources=secrets;events;configmaps,verbs=get;list;watch;create;update;patch;delete
 
 // KubeadmConfigReconciler reconciles a KubeadmConfig object.
@@ -160,6 +161,13 @@ func (r *KubeadmConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		return ctrl.Result{}, err
 	}
 
+	// AddOwners adds the owners of KubeadmConfig as k/v pairs to the logger.
+	// Specifically, it will add KubeadmControlPlane, MachineSet and MachineDeployment.
+	ctx, log, err := clog.AddOwners(ctx, r.Client, config)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
 	// Look up the owner of this kubeadm config if there is one
 	configOwner, err := bsutil.GetConfigOwner(ctx, r.Client, config)
 	if apierrors.IsNotFound(err) {
diff --git a/cmd/clusterctl/cmd/init.go b/cmd/clusterctl/cmd/init.go
index 32aef5cee..f005e4bd2 100644
--- a/cmd/clusterctl/cmd/init.go
+++ b/cmd/clusterctl/cmd/init.go
@@ -78,7 +78,12 @@ var initCmd = &cobra.Command{
 		clusterctl init --infrastructure=aws,vsphere
 
 		# Initialize a management cluster with a custom target namespace for the provider resources.
-		clusterctl init --infrastructure aws --target-namespace foo`),
+		clusterctl init --infrastructure aws --target-namespace foo
+
+		# Lists the container images required for initializing the management cluster.
+		#
+		# Note: This command is a dry-run; it won't perform any action other than printing to screen.
+		clusterctl init --infrastructure aws --list-images`),
 	Args: cobra.NoArgs,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		return runInit()
@@ -86,17 +91,17 @@ var initCmd = &cobra.Command{
 }
 
 func init() {
-	initCmd.PersistentFlags().StringVar(&initOpts.kubeconfig, "kubeconfig", "",
+	initCmd.Flags().StringVar(&initOpts.kubeconfig, "kubeconfig", "",
 		"Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.")
-	initCmd.PersistentFlags().StringVar(&initOpts.kubeconfigContext, "kubeconfig-context", "",
+	initCmd.Flags().StringVar(&initOpts.kubeconfigContext, "kubeconfig-context", "",
 		"Context to be used within the kubeconfig file. If empty, current context will be used.")
-	initCmd.PersistentFlags().StringVar(&initOpts.coreProvider, "core", "",
+	initCmd.Flags().StringVar(&initOpts.coreProvider, "core", "",
 		"Core provider version (e.g. cluster-api:v1.1.5) to add to the management cluster. If unspecified, Cluster API's latest release is used.")
-	initCmd.PersistentFlags().StringSliceVarP(&initOpts.infrastructureProviders, "infrastructure", "i", nil,
+	initCmd.Flags().StringSliceVarP(&initOpts.infrastructureProviders, "infrastructure", "i", nil,
 		"Infrastructure providers and versions (e.g. aws:v0.5.0) to add to the management cluster.")
-	initCmd.PersistentFlags().StringSliceVarP(&initOpts.bootstrapProviders, "bootstrap", "b", nil,
+	initCmd.Flags().StringSliceVarP(&initOpts.bootstrapProviders, "bootstrap", "b", nil,
 		"Bootstrap providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, Kubeadm bootstrap provider's latest release is used.")
-	initCmd.PersistentFlags().StringSliceVarP(&initOpts.controlPlaneProviders, "control-plane", "c", nil,
+	initCmd.Flags().StringSliceVarP(&initOpts.controlPlaneProviders, "control-plane", "c", nil,
 		"Control plane providers and versions (e.g. kubeadm:v1.1.5) to add to the management cluster. If unspecified, the Kubeadm control plane provider's latest release is used.")
 	initCmd.Flags().StringVarP(&initOpts.targetNamespace, "target-namespace", "n", "",
 		"The target namespace where the providers should be deployed. If unspecified, the provider components' default namespace is used.")
@@ -107,11 +112,10 @@ func init() {
 	initCmd.Flags().BoolVar(&initOpts.validate, "validate", true,
 		"If true, clusterctl will validate that the deployments will succeed on the management cluster.")
 
+	// TODO: Move this to a sub-command or similar, it shouldn't really be a flag.
 	initCmd.Flags().BoolVar(&initOpts.listImages, "list-images", false,
 		"Lists the container images required for initializing the management cluster (without actually installing the providers)")
-	_ = initCmd.Flags().MarkDeprecated("list-images", "use 'clusterctl init list-images' instead.")
 
-	initCmd.AddCommand(initListImagesCmd)
 	RootCmd.AddCommand(initCmd)
 }
 
diff --git a/cmd/clusterctl/cmd/init_list_images.go b/cmd/clusterctl/cmd/init_list_images.go
deleted file mode 100644
index d9ae308d8..000000000
--- a/cmd/clusterctl/cmd/init_list_images.go
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-Copyright 2022 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package cmd
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client"
-)
-
-var initListImagesCmd = &cobra.Command{
-	Use:   "list-images",
-	Short: "Lists the container images required for initializing the management cluster",
-	Long: LongDesc(`
-		Lists the container images required for initializing the management cluster.
-
-		See https://cluster-api.sigs.k8s.io for more details.`),
-
-	Example: Examples(`
-		# Lists the container images required for initializing the management cluster.
-		#
-		# Note: This command is a dry-run; it won't perform any action other than printing to screen.
-		clusterctl init list-images --infrastructure aws
-
-		# List infrastructure, bootstrap, control-plane and core images
-		clusterctl init list-images --infrastructure vcd --bootstrap kubeadm --control-plane nested --core cluster-api:v1.2.0
-	`),
-	Args: cobra.NoArgs,
-	RunE: func(cmd *cobra.Command, args []string) error {
-		return runInitListImages()
-	},
-}
-
-func runInitListImages() error {
-	c, err := client.New(cfgFile)
-	if err != nil {
-		return err
-	}
-
-	options := client.InitOptions{
-		Kubeconfig:              client.Kubeconfig{Path: initOpts.kubeconfig, Context: initOpts.kubeconfigContext},
-		CoreProvider:            initOpts.coreProvider,
-		BootstrapProviders:      initOpts.bootstrapProviders,
-		ControlPlaneProviders:   initOpts.controlPlaneProviders,
-		InfrastructureProviders: initOpts.infrastructureProviders,
-		LogUsageInstructions:    false,
-	}
-
-	images, err := c.InitImages(options)
-	if err != nil {
-		return err
-	}
-
-	for _, i := range images {
-		fmt.Println(i)
-	}
-	return nil
-}
diff --git a/docs/book/src/clusterctl/commands/additional-commands.md b/docs/book/src/clusterctl/commands/additional-commands.md
index 8c7432bbb..3d6ebcaff 100644
--- a/docs/book/src/clusterctl/commands/additional-commands.md
+++ b/docs/book/src/clusterctl/commands/additional-commands.md
@@ -22,7 +22,3 @@ or in the provided directory.
 # clusterctl version
 
 Print clusterctl version.
-
-# clusterctl init list-images
-
-Lists the container images required for initializing the management cluster.
\ No newline at end of file
diff --git a/docs/book/src/clusterctl/commands/commands.md b/docs/book/src/clusterctl/commands/commands.md
index ce538d059..ce0dd440b 100644
--- a/docs/book/src/clusterctl/commands/commands.md
+++ b/docs/book/src/clusterctl/commands/commands.md
@@ -1,23 +1,22 @@
 # clusterctl commands
 
-| Command                                                                              | Description                                                                                                |
-|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
-| [`clusterctl alpha rollout`](alpha-rollout.md)                                       | Manages the rollout of Cluster API resources. For example: MachineDeployments.                             |
-| [`clusterctl alpha topology plan`](alpha-topology-plan.md)                           | Describes the changes to a cluster topology for a given input.                                             |
-| [`clusterctl backup`](additional-commands.md#clusterctl-backup)                      | Backup Cluster API objects and all their dependencies from a management cluster.                           |
-| [`clusterctl completion`](completion.md)                                             | Output shell completion code for the specified shell (bash or zsh).                                        |
-| [`clusterctl config`](additional-commands.md#clusterctl-config-repositories)         | Display clusterctl configuration.                                                                          |
-| [`clusterctl delete`](delete.md)                                                     | Delete one or more providers from the management cluster.                                                  |
-| [`clusterctl describe cluster`](describe-cluster.md)                                 | Describe workload clusters.                                                                                |
-| [`clusterctl generate cluster`](generate-cluster.md)                                 | Generate templates for creating workload clusters.                                                         |
-| [`clusterctl generate provider`](generate-provider.md)                               | Generate templates for provider components.                                                                |
-| [`clusterctl generate yaml`](generate-yaml.md)                                       | Process yaml using clusterctl's yaml processor.                                                            |
-| [`clusterctl get kubeconfig`](get-kubeconfig.md)                                     | Gets the kubeconfig file for accessing a workload cluster.                                                 |
-| [`clusterctl help`](additional-commands.md#clusterctl-help)                          | Help about any command.                                                                                    |
-| [`clusterctl init`](init.md)                                                         | Initialize a management cluster.                                                                           |
-| [`clusterctl init list-images`](additional-commands.md#clusterctl-init-list-images)  | Lists the container images required for initializing the management cluster.                               |
-| [`clusterctl move`](move.md)                                                         | Move Cluster API objects and all their dependencies between management clusters.                           |
-| [`clusterctl restore`](additional-commands.md#clusterctl-restore)                    | Restore Cluster API objects from file by glob.                                                             |
-| [`clusterctl upgrade plan`](upgrade.md#upgrade-plan)                                 | Provide a list of recommended target versions for upgrading Cluster API providers in a management cluster. |
-| [`clusterctl upgrade apply`](upgrade.md#upgrade-apply)                               | Apply new versions of Cluster API core and providers in a management cluster.                              |
-| [`clusterctl version`](additional-commands.md#clusterctl-version)                    | Print clusterctl version.                                                                                  |
+| Command                                                                      | Description                                                                                                |
+|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
+| [`clusterctl alpha rollout`](alpha-rollout.md)                               | Manages the rollout of Cluster API resources. For example: MachineDeployments.                             |
+| [`clusterctl alpha topology plan`](alpha-topology-plan.md)                   | Describes the changes to a cluster topology for a given input.                                             |
+| [`clusterctl backup`](additional-commands.md#clusterctl-backup)              | Backup Cluster API objects and all their dependencies from a management cluster.                           |
+| [`clusterctl completion`](completion.md)                                     | Output shell completion code for the specified shell (bash or zsh).                                        |
+| [`clusterctl config`](additional-commands.md#clusterctl-config-repositories) | Display clusterctl configuration.                                                                          |
+| [`clusterctl delete`](delete.md)                                             | Delete one or more providers from the management cluster.                                                  |
+| [`clusterctl describe cluster`](describe-cluster.md)                         | Describe workload clusters.                                                                                |
+| [`clusterctl generate cluster`](generate-cluster.md)                         | Generate templates for creating workload clusters.                                                         |
+| [`clusterctl generate provider`](generate-provider.md)                       | Generate templates for provider components.                                                                |
+| [`clusterctl generate yaml`](generate-yaml.md)                               | Process yaml using clusterctl's yaml processor.                                                            |
+| [`clusterctl get kubeconfig`](get-kubeconfig.md)                             | Gets the kubeconfig file for accessing a workload cluster.                                                 |
+| [`clusterctl help`](additional-commands.md#clusterctl-help)                  | Help about any command.                                                                                    |
+| [`clusterctl init`](init.md)                                                 | Initialize a management cluster.                                                                           |
+| [`clusterctl move`](move.md)                                                 | Move Cluster API objects and all their dependencies between management clusters.                           |
+| [`clusterctl restore`](additional-commands.md#clusterctl-restore)            | Restore Cluster API objects from file by glob.                                                             |
+| [`clusterctl upgrade plan`](upgrade.md#upgrade-plan)                         | Provide a list of recommended target versions for upgrading Cluster API providers in a management cluster. |
+| [`clusterctl upgrade apply`](upgrade.md#upgrade-apply)                       | Apply new versions of Cluster API core and providers in a management cluster.                              |
+| [`clusterctl version`](additional-commands.md#clusterctl-version)            | Print clusterctl version.                                                                                  |
diff --git a/docs/book/src/developer/logging.md b/docs/book/src/developer/logging.md
index ec28fb715..c5afde93b 100644
--- a/docs/book/src/developer/logging.md
+++ b/docs/book/src/developer/logging.md
@@ -180,6 +180,11 @@ Will return logs from the `capi-controller-manager` which are parsed in json. Pa
 ```
 Will return logs from the `capi-controller-manager` that are associated with the Cluster `my-cluster`.
 
+```
+{app="capi-controller-manager"} | json | cluster_name="my-cluster" | v <= 2
+```
+Will return logs from the `capi-controller-manager` that are associated with the Cluster `my-cluster` with log level <= 2.
+
 ```
 {app="capi-controller-manager"} | json | cluster_name="my-cluster" reconcileID="6f6ad971-bdb6-4fa3-b803-xxxxxxxxxxxx"
 ```
diff --git a/docs/book/src/developer/providers/v1.2-to-v1.3.md b/docs/book/src/developer/providers/v1.2-to-v1.3.md
index 6ff96e2e1..44e058430 100644
--- a/docs/book/src/developer/providers/v1.2-to-v1.3.md
+++ b/docs/book/src/developer/providers/v1.2-to-v1.3.md
@@ -19,7 +19,6 @@ in Cluster API are kept in sync with the versions used by `sigs.k8s.io/controlle
 ### Deprecation
 
 - `sigs.k8s.io/cluster-api/controllers/external.CloneTemplate` has been deprecated and will be removed in a future release. Please use `sigs.k8s.io/cluster-api/controllers/external.CreateFromTemplate` instead.
-- `clusterctl init --list-images` has been deprecated and will be removed in a future release. Please use `clusterctl init list-images` instead.
 
 ### Removals
 
@@ -36,3 +35,6 @@ in Cluster API are kept in sync with the versions used by `sigs.k8s.io/controlle
   * the default test timeout has been [changed to 1h](https://onsi.github.io/ginkgo/MIGRATING_TO_V2#timeout-behavior)
   * the `--junit-report` argument [replaces JUnit custom reporter](https://onsi.github.io/ginkgo/MIGRATING_TO_V2#improved-reporting-infrastructure) code
   * see the ["Update tests to Ginkgo v2" PR](https://github.com/kubernetes-sigs/cluster-api/pull/6906) for a reference example
+- Custer API introduced new [logging guidelines](../../developer/logging.md). All reconcilers in the core repository were updated
+  to [log the entire object hierarchy](../../developer/logging.md#keyvalue-pairs). It would be great if providers would be adjusted 
+  as well to make it possible to cross-reference log entries across providers (please see CAPD for an infra provider reference implementation). 
diff --git a/docs/book/src/developer/tilt.md b/docs/book/src/developer/tilt.md
index 562b86a4c..1b9172cc9 100644
--- a/docs/book/src/developer/tilt.md
+++ b/docs/book/src/developer/tilt.md
@@ -273,12 +273,12 @@ With this config, the respective managers will be invoked with:
 manager --logging-format=json
 ```
 
-### Create a kind cluster and run Tilt!
+### Run Tilt!
 
-To create a pre-configured kind cluster (if you have not already done so) and launch your development environment, run
+To launch your development environment, run
 
 ```bash
-make tilt-up
+tilt up
 ```
 
 This will open the command-line HUD as well as a web browser interface. You can monitor Tilt's status in either
@@ -308,22 +308,6 @@ kustomize_substitutions:
   WORKER_MACHINE_COUNT: 3
 ```
 
-### Cleaning up your kind cluster and development environment
-
-After stopping Tilt, you can clean up your kind cluster and development environment by running
-
-```bash
-make clean-kind
-```
-
-To remove all generated files, run
-
-```bash
-make clean
-```
-
-Note that you must run `make clean` or `make clean-charts` to fetch new versions of charts deployed using `deploy_observability` in `tilt-settings.yaml`.
-
 <h1>Use of clusterctl</h1>
 
 When the worker cluster has been created using tilt, `clusterctl` should not be used for management
diff --git a/exp/internal/controllers/machinepool_controller_phases.go b/exp/internal/controllers/machinepool_controller_phases.go
index c24f4b42b..60a687ddf 100644
--- a/exp/internal/controllers/machinepool_controller_phases.go
+++ b/exp/internal/controllers/machinepool_controller_phases.go
@@ -134,7 +134,7 @@ func (r *MachinePoolReconciler) reconcileExternal(ctx context.Context, cluster *
 	// Add watcher for external object, if there isn't one already.
 	_, loaded := r.externalWatchers.LoadOrStore(obj.GroupVersionKind().String(), struct{}{})
 	if !loaded && r.controller != nil {
-		log.Info("Adding watcher on external object", "gvk", obj.GroupVersionKind())
+		log.Info("Adding watcher on external object", "groupVersionKind", obj.GroupVersionKind())
 		err := r.controller.Watch(
 			&source.Kind{Type: obj},
 			&handler.EnqueueRequestForOwner{OwnerType: &expv1.MachinePool{}},
diff --git a/hack/kind-install-for-capd.sh b/hack/kind-install-for-capd.sh
index 63a614181..edaec2cb7 100755
--- a/hack/kind-install-for-capd.sh
+++ b/hack/kind-install-for-capd.sh
@@ -26,12 +26,7 @@ set -o errexit
 set -o nounset
 set -o pipefail
 
-KIND_CLUSTER_NAME=${CAPI_KIND_CLUSTER_NAME:-"capi-test"}
-
-if [[ "$(kind get clusters)" =~ .*"${KIND_CLUSTER_NAME}".* ]]; then
-  echo "kind cluster already exists, moving on"
-  exit 0
-fi
+NAME=${CAPI_KIND_CLUSTER_NAME:-"capi-test"}
 
 # create registry container unless it already exists
 reg_name='kind-registry'
@@ -44,7 +39,7 @@ if [ "${running}" != 'true' ]; then
 fi
 
 # create a cluster with the local registry enabled in containerd
-cat <<EOF | kind create cluster --name="$KIND_CLUSTER_NAME"  --config=-
+cat <<EOF | kind create cluster --name="$NAME"  --config=-
 kind: Cluster
 apiVersion: kind.x-k8s.io/v1alpha4
 nodes:
diff --git a/hack/observability/kube-state-metrics/crd-config.yaml b/hack/observability/kube-state-metrics/crd-config.yaml
index dc5aa9cf2..1a8f20eab 100644
--- a/hack/observability/kube-state-metrics/crd-config.yaml
+++ b/hack/observability/kube-state-metrics/crd-config.yaml
@@ -458,15 +458,6 @@ spec:
           - availableReplicas
           nilIsZero: true
         type: Gauge
-    - name: status_replicas_ready
-      help: The number of ready replicas per machinedeployment.
-      each:
-        gauge:
-          path:
-          - status
-          - readyReplicas
-          nilIsZero: true
-        type: Gauge
     - name: status_replicas_unavailable
       help: The number of unavailable replicas per machinedeployment.
       each:
diff --git a/hack/observability/kube-state-metrics/metrics/machinedeployment.yaml b/hack/observability/kube-state-metrics/metrics/machinedeployment.yaml
index 9f94745c8..bc3811a4b 100644
--- a/hack/observability/kube-state-metrics/metrics/machinedeployment.yaml
+++ b/hack/observability/kube-state-metrics/metrics/machinedeployment.yaml
@@ -87,15 +87,6 @@
           - availableReplicas
           nilIsZero: true
         type: Gauge
-    - name: status_replicas_ready
-      help: The number of ready replicas per machinedeployment.
-      each:
-        gauge:
-          path:
-          - status
-          - readyReplicas
-          nilIsZero: true
-        type: Gauge
     - name: status_replicas_unavailable
       help: The number of unavailable replicas per machinedeployment.
       each:
diff --git a/hack/observability/promtail/values.yaml b/hack/observability/promtail/values.yaml
index 7e788303a..c8dfa06c7 100644
--- a/hack/observability/promtail/values.yaml
+++ b/hack/observability/promtail/values.yaml
@@ -2,19 +2,18 @@
 
 config:
   # publish data to loki
-  clients:
-    - url: http://loki:3100/loki/api/v1/push
+  lokiAddress: http://loki:3100/loki/api/v1/push
 
   snippets:
     pipelineStages:
-      # Parse cluster and machine to make them available as labels.
-      - cri: {}
-      - json:
-          expressions:
-            controller:
-            cluster: join('/',[Cluster.namespace,Cluster.name])
-            machine: join('/',[Machine.namespace,Machine.name])
-      - labels:
+    # Parse cluster and machine to make them available as labels.
+    - cri: { }
+    - json:
+        expressions:
           controller:
-          cluster:
-          machine:
+          cluster: join('/',[Cluster.namespace,Cluster.name])
+          machine: join('/',[Machine.namespace,Machine.name])
+    - labels:
+        controller:
+        cluster:
+        machine:
diff --git a/hack/observability/visualizer/kustomization.yaml b/hack/observability/visualizer/kustomization.yaml
index e3a14d42e..a1764d3cf 100644
--- a/hack/observability/visualizer/kustomization.yaml
+++ b/hack/observability/visualizer/kustomization.yaml
@@ -3,7 +3,7 @@ resources:
 
 helmCharts:
   - name: cluster-api-visualizer
-    repo: https://jont828.github.io/cluster-api-visualizer/charts
+    repo: https://raw.githubusercontent.com/Jont828/cluster-api-visualizer/main/helm/repo
     releaseName: visualizer
     namespace: observability
     valuesFile: values.yaml
diff --git a/hack/observability/visualizer/values.yaml b/hack/observability/visualizer/values.yaml
index afc05bd56..89a773df6 100644
--- a/hack/observability/visualizer/values.yaml
+++ b/hack/observability/visualizer/values.yaml
@@ -4,6 +4,8 @@ image:
   repository: ghcr.io/jont828
   name: cluster-api-visualizer
   pullPolicy: Always
+  # Overrides the image tag whose default is the chart appVersion.
+  tag: "v0.1.0-alpha.2"
 
 label: 
   key: app
diff --git a/internal/controllers/machine/machine_controller.go b/internal/controllers/machine/machine_controller.go
index a3570fb85..41c49ab37 100644
--- a/internal/controllers/machine/machine_controller.go
+++ b/internal/controllers/machine/machine_controller.go
@@ -50,6 +50,7 @@ import (
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/collections"
 	"sigs.k8s.io/cluster-api/util/conditions"
+	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -137,8 +138,6 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
 }
 
 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
-	log := ctrl.LoggerFrom(ctx)
-
 	// Fetch the Machine instance
 	m := &clusterv1.Machine{}
 	if err := r.Client.Get(ctx, req.NamespacedName, m); err != nil {
@@ -152,6 +151,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
 		return ctrl.Result{}, err
 	}
 
+	// AddOwners adds the owners of Machine as k/v pairs to the logger.
+	// Specifically, it will add KubeadmControlPlane, MachineSet and MachineDeployment.
+	ctx, log, err := clog.AddOwners(ctx, r.Client, m)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
 	log = log.WithValues("Cluster", klog.KRef(m.ObjectMeta.Namespace, m.Spec.ClusterName))
 	ctx = ctrl.LoggerInto(ctx, log)
 
@@ -434,7 +440,7 @@ func (r *Reconciler) isNodeDrainAllowed(m *clusterv1.Machine) bool {
 }
 
 func (r *Reconciler) nodeDrainTimeoutExceeded(machine *clusterv1.Machine) bool {
-	// if the NodeDrainTimeout type is not set by user
+	// if the NodeDrainTineout type is not set by user
 	if machine.Spec.NodeDrainTimeout == nil || machine.Spec.NodeDrainTimeout.Seconds() <= 0 {
 		return false
 	}
diff --git a/internal/controllers/machine/machine_controller_phases.go b/internal/controllers/machine/machine_controller_phases.go
index 571bf1e9c..243eb7912 100644
--- a/internal/controllers/machine/machine_controller_phases.go
+++ b/internal/controllers/machine/machine_controller_phases.go
@@ -98,7 +98,7 @@ func (r *Reconciler) reconcileExternal(ctx context.Context, cluster *clusterv1.C
 	obj, err := external.Get(ctx, r.Client, ref, m.Namespace)
 	if err != nil {
 		if apierrors.IsNotFound(errors.Cause(err)) {
-			log.Info("could not find external ref, requeueing", "refGVK", ref.GroupVersionKind(), "refName", ref.Name, "Machine", klog.KObj(m))
+			log.Info("could not find external ref, requeueing", ref.Kind, klog.KRef(m.Namespace, ref.Name))
 			return external.ReconcileOutput{RequeueAfter: externalReadyWait}, nil
 		}
 		return external.ReconcileOutput{}, err
diff --git a/internal/controllers/machineset/machineset_controller.go b/internal/controllers/machineset/machineset_controller.go
index e9ab72f2f..591454c0d 100644
--- a/internal/controllers/machineset/machineset_controller.go
+++ b/internal/controllers/machineset/machineset_controller.go
@@ -46,6 +46,7 @@ import (
 	"sigs.k8s.io/cluster-api/util/collections"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
+	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -118,8 +119,6 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
 }
 
 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
-	log := ctrl.LoggerFrom(ctx)
-
 	machineSet := &clusterv1.MachineSet{}
 	if err := r.Client.Get(ctx, req.NamespacedName, machineSet); err != nil {
 		if apierrors.IsNotFound(err) {
@@ -131,6 +130,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
 		return ctrl.Result{}, err
 	}
 
+	// AddOwners adds the owners of MachineSet as k/v pairs to the logger.
+	// Specifically, it will add MachineDeployment.
+	ctx, log, err := clog.AddOwners(ctx, r.Client, machineSet)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
 	log = log.WithValues("Cluster", klog.KRef(machineSet.ObjectMeta.Namespace, machineSet.Spec.ClusterName))
 	ctx = ctrl.LoggerInto(ctx, log)
 
diff --git a/internal/controllers/topology/cluster/patches/variables/errors.go b/internal/controllers/topology/cluster/patches/variables/errors.go
deleted file mode 100644
index 0adad2e3a..000000000
--- a/internal/controllers/topology/cluster/patches/variables/errors.go
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
-Copyright 2022 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package variables
-
-const notFoundReason = "variable not found"
-
-type errorWithReason interface {
-	Reason() string
-}
-
-// IsNotFoundError checks if the passed error is a notFoundError.
-func IsNotFoundError(err error) bool {
-	if e, ok := err.(errorWithReason); ok {
-		return e.Reason() == notFoundReason
-	}
-	return false
-}
-
-// notFoundError is used when a variable is not found.
-type notFoundError struct {
-	reason  string
-	message string
-}
-
-func (e notFoundError) Error() string {
-	return e.message
-}
-
-// Reason returns the notFoundReason.
-func (e notFoundError) Reason() string {
-	return e.reason
-}
diff --git a/internal/controllers/topology/cluster/patches/variables/value.go b/internal/controllers/topology/cluster/patches/variables/value.go
index 83319688d..eaf171151 100644
--- a/internal/controllers/topology/cluster/patches/variables/value.go
+++ b/internal/controllers/topology/cluster/patches/variables/value.go
@@ -17,7 +17,6 @@ limitations under the License.
 package variables
 
 import (
-	"fmt"
 	"strconv"
 	"strings"
 
@@ -47,7 +46,7 @@ func GetVariableValue(variables map[string]apiextensionsv1.JSON, variablePath st
 	// Get the variable.
 	value, ok := variables[variableNameSegment.path]
 	if !ok {
-		return nil, notFoundError{reason: notFoundReason, message: fmt.Sprintf("variable %q does not exist", variableName)}
+		return nil, errors.Errorf("variable %q does not exist", variableName)
 	}
 
 	// Return the value, if variablePath points to a top-level variable, i.e. hos no relativePath and no
@@ -89,7 +88,7 @@ func GetVariableValue(variables map[string]apiextensionsv1.JSON, variablePath st
 
 		// Return if the variable does not exist.
 		if !variable.Exists(pathSegment.path) {
-			return nil, notFoundError{reason: notFoundReason, message: fmt.Sprintf("variable %q does not exist: failed to lookup segment %q", variablePath, pathSegment.path)}
+			return nil, errors.Errorf("variable %q does not exist: failed to lookup segment %q", variablePath, pathSegment.path)
 		}
 
 		// Get the variable from the variable object.
diff --git a/internal/controllers/topology/machineset/machineset_controller.go b/internal/controllers/topology/machineset/machineset_controller.go
index 217bb49ee..7ecc75af5 100644
--- a/internal/controllers/topology/machineset/machineset_controller.go
+++ b/internal/controllers/topology/machineset/machineset_controller.go
@@ -33,6 +33,7 @@ import (
 	tlog "sigs.k8s.io/cluster-api/internal/log"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
+	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -86,8 +87,6 @@ func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opt
 // We don't have to set the finalizer, as it's already set during MachineSet creation
 // in the MachineSet controller.
 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
-
 	// Fetch the MachineSet instance.
 	ms := &clusterv1.MachineSet{}
 	if err := r.Client.Get(ctx, req.NamespacedName, ms); err != nil {
@@ -99,6 +98,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		return ctrl.Result{}, errors.Wrapf(err, "failed to get MachineSet/%s", req.NamespacedName.Name)
 	}
 
+	// AddOwners adds the owners of MachineSet as k/v pairs to the logger.
+	// Specifically, it will add MachineDeployment.
+	ctx, log, err := clog.AddOwners(ctx, r.Client, ms)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
 	log = log.WithValues("Cluster", klog.KRef(ms.Namespace, ms.Spec.ClusterName))
 	ctx = ctrl.LoggerInto(ctx, log)
 
diff --git a/util/log/doc.go b/util/log/doc.go
new file mode 100644
index 000000000..af33e7436
--- /dev/null
+++ b/util/log/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package log provides log utils.
+package log
diff --git a/util/log/log.go b/util/log/log.go
new file mode 100644
index 000000000..76011328b
--- /dev/null
+++ b/util/log/log.go
@@ -0,0 +1,106 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package log provides log utils.
+package log
+
+import (
+	"context"
+
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+// AddOwners adds the owners of an Object based on OwnerReferences as k/v pairs to the logger in ctx.
+// Note: If an owner is a MachineSet we also add the owners from the MachineSet OwnerReferences.
+func AddOwners(ctx context.Context, c client.Client, obj metav1.Object) (context.Context, logr.Logger, error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	owners, err := getOwners(ctx, c, obj)
+	if err != nil {
+		return nil, logr.Logger{}, errors.Wrapf(err, "failed to add object hierarchy to logger")
+	}
+
+	// Add owners as k/v pairs.
+	keysAndValues := []interface{}{}
+	addedKinds := sets.NewString()
+	for _, owner := range owners {
+		// Don't add duplicate kinds.
+		if addedKinds.Has(owner.Kind) {
+			continue
+		}
+
+		keysAndValues = append(keysAndValues, owner.Kind, klog.KRef(owner.Namespace, owner.Name))
+		addedKinds.Insert(owner.Kind)
+	}
+	log = log.WithValues(keysAndValues...)
+
+	ctx = ctrl.LoggerInto(ctx, log)
+	return ctx, log, nil
+}
+
+// owner represents an owner of an object.
+type owner struct {
+	Kind      string
+	Name      string
+	Namespace string
+}
+
+// getOwners returns owners of an Object based on OwnerReferences.
+// Note: If an owner is a MachineSet we also return the owners from the MachineSet OwnerReferences.
+func getOwners(ctx context.Context, c client.Client, obj metav1.Object) ([]owner, error) {
+	owners := []owner{}
+	for _, ownerRef := range obj.GetOwnerReferences() {
+		owners = append(owners, owner{
+			Kind:      ownerRef.Kind,
+			Namespace: obj.GetNamespace(),
+			Name:      ownerRef.Name,
+		})
+
+		// continue if the ownerRef does not point to a MachineSet.
+		if ownerRef.Kind != "MachineSet" {
+			continue
+		}
+
+		// get owners of the MachineSet.
+		var ms clusterv1.MachineSet
+		if err := c.Get(ctx, client.ObjectKey{Namespace: obj.GetNamespace(), Name: ownerRef.Name}, &ms); err != nil {
+			// continue if the MachineSet doesn't exist.
+			if apierrors.IsNotFound(err) {
+				continue
+			}
+			return nil, errors.Wrapf(err, "failed to get owners: failed to get MachineSet %s", klog.KRef(obj.GetNamespace(), ownerRef.Name))
+		}
+
+		for _, ref := range ms.GetOwnerReferences() {
+			owners = append(owners, owner{
+				Kind:      ref.Kind,
+				Namespace: obj.GetNamespace(),
+				Name:      ref.Name,
+			})
+		}
+	}
+
+	return owners, nil
+}
diff --git a/util/record/recorder.go b/util/record/recorder.go
index 634eb2615..76aad1244 100644
--- a/util/record/recorder.go
+++ b/util/record/recorder.go
@@ -46,20 +46,20 @@ func InitFromRecorder(recorder record.EventRecorder) {
 
 // Event constructs an event from the given information and puts it in the queue for sending.
 func Event(object runtime.Object, reason, message string) {
-	defaultRecorder.Event(object, corev1.EventTypeNormal, cases.Title(language.Und, cases.NoLower).String(reason), message)
+	defaultRecorder.Event(object, corev1.EventTypeNormal, cases.Title(language.Und).String(reason), message)
 }
 
 // Eventf is just like Event, but with Sprintf for the message field.
 func Eventf(object runtime.Object, reason, message string, args ...interface{}) {
-	defaultRecorder.Eventf(object, corev1.EventTypeNormal, cases.Title(language.Und, cases.NoLower).String(reason), message, args...)
+	defaultRecorder.Eventf(object, corev1.EventTypeNormal, cases.Title(language.Und).String(reason), message, args...)
 }
 
 // Warn constructs a warning event from the given information and puts it in the queue for sending.
 func Warn(object runtime.Object, reason, message string) {
-	defaultRecorder.Event(object, corev1.EventTypeWarning, cases.Title(language.Und, cases.NoLower).String(reason), message)
+	defaultRecorder.Event(object, corev1.EventTypeWarning, cases.Title(language.Und).String(reason), message)
 }
 
 // Warnf is just like Warn, but with Sprintf for the message field.
 func Warnf(object runtime.Object, reason, message string, args ...interface{}) {
-	defaultRecorder.Eventf(object, corev1.EventTypeWarning, cases.Title(language.Und, cases.NoLower).String(reason), message, args...)
+	defaultRecorder.Eventf(object, corev1.EventTypeWarning, cases.Title(language.Und).String(reason), message, args...)
 }


Test patch:
diff --git a/test/e2e/cluster_upgrade_runtimesdk.go b/test/e2e/cluster_upgrade_runtimesdk.go
index b10b1845b..c812998eb 100644
--- a/test/e2e/cluster_upgrade_runtimesdk.go
+++ b/test/e2e/cluster_upgrade_runtimesdk.go
@@ -488,7 +488,7 @@ func runtimeHookTestHandler(ctx context.Context, c client.Client, namespace, clu
 	Eventually(func() bool {
 		return blockingCondition()
 	}, intervals...).Should(BeFalse(),
-		fmt.Sprintf("ClusterTopology reconcile did not proceed as expected when calling %s", hookName))
+		fmt.Sprintf("ClusterTopology reconcile did proceed as expected when calling %s", hookName))
 }
 
 // clusterConditionShowsHookBlocking checks if the TopologyReconciled condition message contains both the hook name and hookFailedMessage.
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/bases/cluster-with-topology.yaml b/test/e2e/data/infrastructure-docker/v1beta1/bases/cluster-with-topology.yaml
index 9943a7408..0498dc57f 100644
--- a/test/e2e/data/infrastructure-docker/v1beta1/bases/cluster-with-topology.yaml
+++ b/test/e2e/data/infrastructure-docker/v1beta1/bases/cluster-with-topology.yaml
@@ -27,9 +27,9 @@ spec:
         replicas: ${WORKER_MACHINE_COUNT}
         failureDomain: fd4
     variables:
-      # We set an empty value to use the default tag kubeadm init is using.
     - name: etcdImageTag
-      value: ""
       # We set an empty value to use the default tag kubeadm init is using.
+      value: ""
     - name: coreDNSImageTag
-      value: ""
\ No newline at end of file
+      # We set an empty value to use the default tag kubeadm init is using.
+      value: ""
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/cluster-runtimesdk.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/cluster-runtimesdk.yaml
new file mode 100644
index 000000000..b0d438e58
--- /dev/null
+++ b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/cluster-runtimesdk.yaml
@@ -0,0 +1,8 @@
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: '${CLUSTER_NAME}'
+  namespace: default
+spec:
+  topology:
+    class: "quick-start-runtimesdk"
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/kustomization.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/kustomization.yaml
index 2884b0190..4e0073d2c 100644
--- a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/kustomization.yaml
+++ b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/kustomization.yaml
@@ -1,6 +1,5 @@
 resources:
-- ../cluster-template-upgrades-runtimesdk
-- ./mp-cgroupfs.yaml
+- ../cluster-template-upgrades-cgroupfs
 
 patches:
-- ./mp-default-cgroupfs.yaml
+- ./cluster-runtimesdk.yaml
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-cgroupfs.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-cgroupfs.yaml
deleted file mode 100644
index b9a5097f1..000000000
--- a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-cgroupfs.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-# KubeadmConfigTemplate referenced by the MachinePool for cgroupfs
-apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
-kind: KubeadmConfig
-metadata:
-  name: "${CLUSTER_NAME}-mp-0-config-cgroupfs"
-spec:
-  joinConfiguration:
-    nodeRegistration:
-      kubeletExtraArgs:
-        # We have to pin the cgroupDriver to cgroupfs as kubeadm >=1.21 defaults to systemd
-        # kind will implement systemd support in: https://github.com/kubernetes-sigs/kind/issues/1726
-        cgroup-driver: cgroupfs
-        eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-default-cgroupfs.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-default-cgroupfs.yaml
deleted file mode 100644
index 08d825254..000000000
--- a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk-cgroupfs/mp-default-cgroupfs.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: cluster.x-k8s.io/v1beta1
-kind: MachinePool
-metadata:
-  name: "${CLUSTER_NAME}-mp-0"
-spec:
-  template:
-    spec:
-      bootstrap:
-        configRef:
-          name: "${CLUSTER_NAME}-mp-0-config-cgroupfs"
\ No newline at end of file
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/cluster-runtimesdk.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/cluster-runtimesdk.yaml
index 8bbc2426a..b0d438e58 100644
--- a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/cluster-runtimesdk.yaml
+++ b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/cluster-runtimesdk.yaml
@@ -3,29 +3,6 @@ kind: Cluster
 metadata:
   name: '${CLUSTER_NAME}'
   namespace: default
-  labels:
-    cni: "${CLUSTER_NAME}-crs-0"
 spec:
-  clusterNetwork:
-    services:
-      cidrBlocks: ['${DOCKER_SERVICE_CIDRS}']
-    pods:
-      cidrBlocks: ['${DOCKER_POD_CIDRS}']
-    serviceDomain: '${DOCKER_SERVICE_DOMAIN}'
   topology:
     class: "quick-start-runtimesdk"
-    version: "${KUBERNETES_VERSION}"
-    controlPlane:
-      metadata: {}
-      nodeDeletionTimeout: "30s"
-      replicas: ${CONTROL_PLANE_MACHINE_COUNT}
-    workers:
-      machineDeployments:
-        - class: "default-worker"
-          name: "md-0"
-          nodeDeletionTimeout: "30s"
-          replicas: ${WORKER_MACHINE_COUNT}
-          failureDomain: fd4
-    variables:
-      - name: kubeadmControlPlaneMaxSurge
-        value: "1"
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/kustomization.yaml b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/kustomization.yaml
index 2c1eba0a1..8555d6df9 100644
--- a/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/kustomization.yaml
+++ b/test/e2e/data/infrastructure-docker/v1beta1/cluster-template-upgrades-runtimesdk/kustomization.yaml
@@ -1,4 +1,5 @@
 resources:
-  - ../bases/crs.yaml
-  - ../bases/mp.yaml
-  - ./cluster-runtimesdk.yaml
\ No newline at end of file
+- ../cluster-template-upgrades
+
+patches:
+- ./cluster-runtimesdk.yaml
diff --git a/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start-runtimesdk.yaml b/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start-runtimesdk.yaml
index 33a3131c9..933f76af8 100644
--- a/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start-runtimesdk.yaml
+++ b/test/e2e/data/infrastructure-docker/v1beta1/clusterclass-quick-start-runtimesdk.yaml
@@ -34,19 +34,27 @@ spec:
             name: quick-start-default-worker-machinetemplate
   variables:
   - name: lbImageRepository
-    required: false
+    required: true
     schema:
       openAPIV3Schema:
         type: string
         default: kindest
-  - name: imageRepository
+  - name: etcdImageTag
     required: true
     schema:
       openAPIV3Schema:
         type: string
-        default: "k8s.gcr.io"
-        example: "k8s.gcr.io"
-        description: "imageRepository sets the container registry to pull images from. If empty, `k8s.gcr.io` will be used by default."
+        default: ""
+        example: "3.5.3-0"
+        description: "etcdImageTag sets the tag for the etcd image."
+  - name: coreDNSImageTag
+    required: true
+    schema:
+      openAPIV3Schema:
+        type: string
+        default: ""
+        example: "v1.8.5"
+        description: "coreDNSImageTag sets the tag for the coreDNS image."
   - name: kubeadmControlPlaneMaxSurge
     required: false
     schema:
@@ -60,6 +68,114 @@ spec:
     external:
       generateExtension: generate-patches.k8s-upgrade-with-runtimesdk
       validateExtension: validate-topology.k8s-upgrade-with-runtimesdk
+  # 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:
+    - selector:
+        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+        kind: KubeadmControlPlaneTemplate
+        matchResources:
+          controlPlane: true
+      jsonPatches:
+      - op: add
+        path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/etcd"
+        valueFrom:
+          template: |
+            local:
+              imageTag: {{ .etcdImageTag }}
+  - name: coreDNSImageTag
+    description: "Sets tag to use for the etcd image in the KubeadmControlPlane."
+    definitions:
+    - selector:
+        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+        kind: KubeadmControlPlaneTemplate
+        matchResources:
+          controlPlane: true
+      jsonPatches:
+      - op: add
+        path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/dns"
+        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: kubeadmControlPlaneMaxSurge
+    description: "Sets the maxSurge value used for rolloutStrategy in the KubeadmControlPlane."
+    enabledIf: '{{ ne .kubeadmControlPlaneMaxSurge "" }}'
+    definitions:
+    - selector:
+        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+        kind: KubeadmControlPlaneTemplate
+        matchResources:
+          controlPlane: true
+      jsonPatches:
+      - op: add
+        path: /spec/template/spec/rolloutStrategy/rollingUpdate/maxSurge
+        valueFrom:
+          template: "{{ .kubeadmControlPlaneMaxSurge }}"
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: DockerClusterTemplate
diff --git a/test/extension/config/tilt/hookresponses-configmap.yaml b/test/extension/config/tilt/hookresponses-configmap.yaml
index 2c2e4670e..298db9fb0 100644
--- a/test/extension/config/tilt/hookresponses-configmap.yaml
+++ b/test/extension/config/tilt/hookresponses-configmap.yaml
@@ -6,7 +6,11 @@ metadata:
 data:
   AfterClusterUpgrade-preloadedResponse: '{"Status": "Success"}'
   AfterControlPlaneInitialized-preloadedResponse: '{"Status": "Success"}'
-  AfterControlPlaneUpgrade-preloadedResponse: '{"Status": "Success"}'
-  BeforeClusterCreate-preloadedResponse: '{"Status": "Success"}'
-  BeforeClusterDelete-preloadedResponse: '{"Status": "Success"}'
-  BeforeClusterUpgrade-preloadedResponse: '{"Status": "Success"}'
+  AfterControlPlaneUpgrade-preloadedResponse: '{"Status": "Success", "RetryAfterSeconds":
+    5}'
+  BeforeClusterCreate-preloadedResponse: '{"Status": "Success", "RetryAfterSeconds":
+    5}'
+  BeforeClusterDelete-preloadedResponse: '{"Status": "Success", "RetryAfterSeconds":
+    5}'
+  BeforeClusterUpgrade-preloadedResponse: '{"Status": "Success", "RetryAfterSeconds":
+    5}'
diff --git a/test/extension/handlers/topologymutation/handler.go b/test/extension/handlers/topologymutation/handler.go
index 330d6a55e..805adc47e 100644
--- a/test/extension/handlers/topologymutation/handler.go
+++ b/test/extension/handlers/topologymutation/handler.go
@@ -19,267 +19,66 @@ package topologymutation
 
 import (
 	"context"
-	"fmt"
 	"strconv"
-	"strings"
 
-	"github.com/blang/semver"
-	"github.com/pkg/errors"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
-	intstrutil "k8s.io/apimachinery/pkg/util/intstr"
 	ctrl "sigs.k8s.io/controller-runtime"
 
-	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
-	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
 	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
 	patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
 	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
-	"sigs.k8s.io/cluster-api/util/version"
 )
 
 // NewHandler returns a new topology mutation Handler.
 func NewHandler(scheme *runtime.Scheme) *Handler {
 	return &Handler{
-		// Add the apiGroups being handled to the decoder
 		decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(
 			infrav1.GroupVersion,
-			controlplanev1.GroupVersion,
-			bootstrapv1.GroupVersion,
 		),
 	}
 }
 
-var (
-	cgroupDriverCgroupfs            = "cgroupfs"
-	cgroupDriverPatchVersionCeiling = semver.Version{Major: 1, Minor: 24}
-)
-
 // Handler is a topology mutation handler.
 type Handler struct {
 	decoder runtime.Decoder
 }
 
-// GeneratePatches generates patches for the given request.
+// GeneratePatches returns a function that generates patches for the given request.
 func (h *Handler) GeneratePatches(ctx context.Context, req *runtimehooksv1.GeneratePatchesRequest, resp *runtimehooksv1.GeneratePatchesResponse) {
 	log := ctrl.LoggerFrom(ctx)
-	log.Info("GeneratePatches is called")
-	walkTemplates(h.decoder, req, resp, func(obj runtime.Object, variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error {
-		holderRefGV, err := schema.ParseGroupVersion(holderRef.APIVersion)
-		if err != nil {
-			return errors.Wrapf(err, "error generating patches - HolderReference apiVersion %q is not in valid format", holderRef.APIVersion)
-		}
-		log = log.WithValues(
-			"template", logRef{
-				Group:   obj.GetObjectKind().GroupVersionKind().Group,
-				Version: obj.GetObjectKind().GroupVersionKind().Version,
-				Kind:    obj.GetObjectKind().GroupVersionKind().Kind,
-			},
-			"holder", logRef{
-				Group:     holderRefGV.Group,
-				Version:   holderRefGV.Version,
-				Kind:      holderRef.Kind,
-				Namespace: holderRef.Namespace,
-				Name:      holderRef.Name,
-			},
-		)
-		log.Info("Patching template")
-
-		switch obj := obj.(type) {
-		case *infrav1.DockerClusterTemplate:
-			if err := patchDockerClusterTemplate(ctx, obj, variables); err != nil {
-				log.Error(err, "error patching DockerClusterTemplate")
-				return errors.Wrap(err, "error patching DockerClusterTemplate")
-			}
-		case *controlplanev1.KubeadmControlPlaneTemplate:
-			err := patchKubeadmControlPlaneTemplate(ctx, obj, variables)
-			if err != nil {
-				log.Error(err, "error patching KubeadmControlPlaneTemplate")
-				return errors.Wrapf(err, "error patching KubeadmControlPlaneTemplate")
-			}
+	log.Info("GeneratePatches called")
 
-		case *bootstrapv1.KubeadmConfigTemplate:
-			if err := patchKubeadmConfigTemplate(ctx, obj, variables); err != nil {
-				log.Error(err, "error patching KubeadmConfigTemplate")
-				return errors.Wrap(err, "error patching KubeadmConfigTemplate")
-			}
-		case *infrav1.DockerMachineTemplate:
-			if err := patchDockerMachineTemplate(ctx, obj, variables); err != nil {
-				log.Error(err, "error patching DockerMachineTemplate")
-				return errors.Wrap(err, "error patching DockerMachineTemplate")
+	walkTemplates(h.decoder, req, resp, func(obj runtime.Object, variables map[string]apiextensionsv1.JSON) error {
+		if dockerClusterTemplate, ok := obj.(*infrav1.DockerClusterTemplate); ok {
+			if err := patchDockerClusterTemplate(dockerClusterTemplate, variables); err != nil {
+				return err
 			}
 		}
+
 		return nil
 	})
 }
 
-func patchKubeadmControlPlaneTemplate(ctx context.Context, kcpTemplate *controlplanev1.KubeadmControlPlaneTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
-	log := ctrl.LoggerFrom(ctx)
-
-	// 1) If the Kubernetes version from builtin.controlPlane.version is below 1.24.0 set "cgroup-driver": "cgroupfs" to
-	//    - kubeadmConfigSpec.InitConfiguration.NodeRegistration.KubeletExtraArgs
-	//    - kubeadmConfigSpec.JoinConfiguration.NodeRegistration.KubeletExtraArgs
-	found, cpVersion, err := getStringValueForVariable(templateVariables, "builtin.controlPlane.version")
-	if err != nil {
-		return errors.Wrap(err, "could not set cgroup-driver to control plane template kubeletExtraArgs")
-	}
-	// This is a required variable. Return an error if it's not found.
-	if !found {
-		return errors.New("could not set cgroup-driver to control plane template kubeletExtraArgs: variable \"builtin.controlPlane.version\" not found")
-	}
-
-	controlPlaneVersion, err := version.ParseMajorMinorPatchTolerant(cpVersion)
+// patchDockerClusterTemplate patches the DockerClusterTepmlate.
+func patchDockerClusterTemplate(dockerClusterTemplate *infrav1.DockerClusterTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
+	// Get the variable value as JSON string.
+	value, err := patchvariables.GetVariableValue(templateVariables, "lbImageRepository")
 	if err != nil {
 		return err
 	}
-	if version.Compare(controlPlaneVersion, cgroupDriverPatchVersionCeiling) == -1 {
-		log.Info(fmt.Sprintf("Setting KubeadmControlPlaneTemplate cgroup-driver to %q", cgroupDriverCgroupfs))
-		// Set the cgroupDriver in the InitConfiguration.
-		if kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration == nil {
-			kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration = &bootstrapv1.InitConfiguration{}
-		}
-		if kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration.KubeletExtraArgs == nil {
-			kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{}
-		}
-		kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration.KubeletExtraArgs["cgroup-driver"] = cgroupDriverCgroupfs
-
-		// Set the cgroupDriver in the JoinConfiguration.
-		if kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration == nil {
-			kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration = &bootstrapv1.JoinConfiguration{}
-		}
-		if kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.KubeletExtraArgs == nil {
-			kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{}
-		}
-		kcpTemplate.Spec.Template.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration.KubeletExtraArgs["cgroup-driver"] = cgroupDriverCgroupfs
-	}
-
-	// 2) Patch RolloutStrategy RollingUpdate MaxSurge with the value from the Cluster Topology variable.
-	//    If this is unset continue as this variable is not required.
-	found, kcpControlPlaneMaxSurge, err := getStringValueForVariable(templateVariables, "kubeadmControlPlaneMaxSurge")
-	if err != nil {
-		return errors.Wrap(err, "could not set KubeadmControlPlaneTemplate MaxSurge")
-	}
-	if found {
-		// This has to be converted to IntOrString type.
-		kubeadmControlPlaneMaxSurgeIntOrString := intstrutil.Parse(kcpControlPlaneMaxSurge)
-		log.Info(fmt.Sprintf("Setting KubeadmControlPlaneMaxSurge to %q", kubeadmControlPlaneMaxSurgeIntOrString.String()))
-		if kcpTemplate.Spec.Template.Spec.RolloutStrategy == nil {
-			kcpTemplate.Spec.Template.Spec.RolloutStrategy = &controlplanev1.RolloutStrategy{}
-		}
-		if kcpTemplate.Spec.Template.Spec.RolloutStrategy.RollingUpdate == nil {
-			kcpTemplate.Spec.Template.Spec.RolloutStrategy.RollingUpdate = &controlplanev1.RollingUpdate{}
-		}
-		kcpTemplate.Spec.Template.Spec.RolloutStrategy.RollingUpdate.MaxSurge = &kubeadmControlPlaneMaxSurgeIntOrString
-	}
-	return nil
-}
 
-// patchDockerClusterTemplate patches the DockerClusterTemplate.
-func patchDockerClusterTemplate(_ context.Context, dockerClusterTemplate *infrav1.DockerClusterTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
-	found, lbImageRepo, err := getStringValueForVariable(templateVariables, "lbImageRepository")
-	if err != nil {
-		return errors.Wrap(err, "could not set DockerClusterTemplate loadBalancer imageRepository")
-	}
-	if found {
-		dockerClusterTemplate.Spec.Template.Spec.LoadBalancer.ImageRepository = lbImageRepo
-	}
-	return nil
-}
-
-func patchKubeadmConfigTemplate(ctx context.Context, k *bootstrapv1.KubeadmConfigTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
-	log := ctrl.LoggerFrom(ctx)
-	// Only patch the customImage if this DockerMachineTemplate belongs to a MachineDeployment with class "default-class"
-	found, mdClass, err := getStringValueForVariable(templateVariables, "builtin.machineDeployment.class")
+	// Unquote the JSON string.
+	stringValue, err := strconv.Unquote(string(value.Raw))
 	if err != nil {
-		return errors.Wrap(err, "could not set cgroup-driver to KubeadmConfigTemplate template kubeletExtraArgs")
-	}
-
-	// This is a required variable. Return an error if it's not found.
-	if !found {
-		return errors.New("could not set cgroup-driver to KubeadmConfigTemplate template kubeletExtraArgs: variable \"builtin.machineDeployment.class\" not found")
+		return err
 	}
 
-	if mdClass == "default-worker" {
-		// If the Kubernetes version from builtin.machineDeployment.version is below 1.24.0 set "cgroup-driver": "cgroupDriverCgroupfs" to
-		//    - InitConfiguration.KubeletExtraArgs
-		//    - JoinConfiguration.KubeletExtraArgs
-		found, mdVersion, err := getStringValueForVariable(templateVariables, "builtin.machineDeployment.version")
-		if err != nil {
-			return errors.Wrap(err, "could not set cgroup-driver to KubeadmConfigTemplate template kubeletExtraArgs")
-		}
-
-		// This is a required variable. Return an error if it's not found.
-		if !found {
-			return errors.New("could not set cgroup-driver to KubeadmConfigTemplate template kubeletExtraArgs: variable \"builtin.machineDeployment.version\" not found")
-		}
-		machineDeploymentVersion, err := version.ParseMajorMinorPatchTolerant(mdVersion)
-		if err != nil {
-			return errors.Wrap(err, "could not set cgroup-driver to KubeadmConfigTemplate template kubeletExtraArgs")
-		}
-		if version.Compare(machineDeploymentVersion, cgroupDriverPatchVersionCeiling) == -1 {
-			log.Info(fmt.Sprintf("Setting KubeadmConfigTemplate cgroup-driver to %q", cgroupDriverCgroupfs))
-			// Set the cgroupDriver in the InitConfiguration.
-			if k.Spec.Template.Spec.InitConfiguration == nil {
-				k.Spec.Template.Spec.InitConfiguration = &bootstrapv1.InitConfiguration{}
-			}
-			if k.Spec.Template.Spec.InitConfiguration.NodeRegistration.KubeletExtraArgs == nil {
-				k.Spec.Template.Spec.InitConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{}
-			}
-			k.Spec.Template.Spec.InitConfiguration.NodeRegistration.KubeletExtraArgs["cgroup-driver"] = cgroupDriverCgroupfs
-
-			// Set the cgroupDriver in the JoinConfiguration.
-			if k.Spec.Template.Spec.JoinConfiguration == nil {
-				k.Spec.Template.Spec.JoinConfiguration = &bootstrapv1.JoinConfiguration{}
-			}
-			if k.Spec.Template.Spec.JoinConfiguration.NodeRegistration.KubeletExtraArgs == nil {
-				k.Spec.Template.Spec.JoinConfiguration.NodeRegistration.KubeletExtraArgs = map[string]string{}
-			}
-
-			k.Spec.Template.Spec.JoinConfiguration.NodeRegistration.KubeletExtraArgs["cgroup-driver"] = cgroupDriverCgroupfs
-		}
-	}
+	dockerClusterTemplate.Spec.Template.Spec.LoadBalancer.ImageRepository = stringValue
 	return nil
 }
 
-func patchDockerMachineTemplate(ctx context.Context, dockerMachineTemplate *infrav1.DockerMachineTemplate, templateVariables map[string]apiextensionsv1.JSON) error {
-	log := ctrl.LoggerFrom(ctx)
-	found, cpVersion, err := getStringValueForVariable(templateVariables, "builtin.controlPlane.version")
-	if err != nil {
-		return errors.Wrap(err, "could not set customImage to control plane dockerMachineTemplate")
-	}
-	if found {
-		_, err := version.ParseMajorMinorPatchTolerant(cpVersion)
-		if err != nil {
-			return errors.Wrap(err, "could not parse control plane version")
-		}
-		customImage := fmt.Sprintf("kindest/node:%s", strings.ReplaceAll(cpVersion, "+", "_"))
-		log.Info(fmt.Sprintf("Setting MachineDeployment custom image to %q", customImage))
-		dockerMachineTemplate.Spec.Template.Spec.CustomImage = customImage
-		// return early if we have successfully patched a control plane dockerMachineTemplate
-		return nil
-	}
-
-	// If the DockerMachineTemplate does not belong to a ControlPlane check if it belongs to a MachineDeployment
-	found, mdVersion, err := getStringValueForVariable(templateVariables, "builtin.machineDeployment.version")
-	if err != nil {
-		return errors.Wrap(err, "could not set customImage to MachineDeployment DockerMachineTemplate")
-	}
-	if found {
-		_, err := version.ParseMajorMinorPatchTolerant(mdVersion)
-		if err != nil {
-			return errors.Wrap(err, "could not parse MachineDeployment version")
-		}
-		customImage := fmt.Sprintf("kindest/node:%s", strings.ReplaceAll(mdVersion, "+", "_"))
-		log.Info(fmt.Sprintf("Setting MachineDeployment customImage to %q", customImage))
-		dockerMachineTemplate.Spec.Template.Spec.CustomImage = customImage
-		return nil
-	}
-	// If the Docker Machine didn't have variables for either a control plane or a machineDeployment return an error.
-	return errors.New("no version variables found for DockerMachineTemplate patch")
-}
-
 // ValidateTopology returns a function that validates the given request.
 func (h *Handler) ValidateTopology(ctx context.Context, _ *runtimehooksv1.ValidateTopologyRequest, resp *runtimehooksv1.ValidateTopologyResponse) {
 	log := ctrl.LoggerFrom(ctx)
@@ -287,20 +86,3 @@ func (h *Handler) ValidateTopology(ctx context.Context, _ *runtimehooksv1.Valida
 
 	resp.Status = runtimehooksv1.ResponseStatusSuccess
 }
-
-// getStringValueForVariable Get the variable value as JSON string.
-func getStringValueForVariable(templateVariables map[string]apiextensionsv1.JSON, variableName string) (bool, string, error) {
-	value, err := patchvariables.GetVariableValue(templateVariables, variableName)
-	if err != nil {
-		if patchvariables.IsNotFoundError(err) {
-			return false, "", nil
-		}
-		return false, "", err
-	}
-	// Unquote the JSON string.
-	stringValue, err := strconv.Unquote(string(value.Raw))
-	if err != nil {
-		return true, "", err
-	}
-	return true, stringValue, nil
-}
diff --git a/test/extension/handlers/topologymutation/handler_test.go b/test/extension/handlers/topologymutation/handler_test.go
deleted file mode 100644
index efb3e4b19..000000000
--- a/test/extension/handlers/topologymutation/handler_test.go
+++ /dev/null
@@ -1,391 +0,0 @@
-/*
-Copyright 2022 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package topologymutation
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"testing"
-
-	. "github.com/onsi/gomega"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-
-	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
-	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
-	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
-	. "sigs.k8s.io/cluster-api/internal/test/matchers"
-	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
-)
-
-var (
-	testScheme = runtime.NewScheme()
-)
-
-func init() {
-	_ = infrav1.AddToScheme(testScheme)
-	_ = controlplanev1.AddToScheme(testScheme)
-	_ = bootstrapv1.AddToScheme(testScheme)
-}
-
-func TestHandler_GeneratePatches(t *testing.T) {
-	g := NewWithT(t)
-	h := NewHandler(testScheme)
-	emptyVars := []runtimehooksv1.Variable{}
-	controlPlaneVarsV123WithMaxSurge := []runtimehooksv1.Variable{
-		builtInVariablesForControlPlane("v1.23.0"),
-		newVariable("kubeadmControlPlaneMaxSurge", "3"),
-	}
-	controlPlaneVarsV123 := []runtimehooksv1.Variable{
-		builtInVariablesForControlPlane("v1.23.0"),
-	}
-	controlPlaneVarsInvalidVersion := []runtimehooksv1.Variable{
-		builtInVariablesForControlPlane("v1.23.x"),
-	}
-	machineDeploymentVarsInvalidVersion := []runtimehooksv1.Variable{
-		builtInVariablesForControlPlane("v1.23.x"),
-	}
-	lbImageRepositoryVar := []runtimehooksv1.Variable{
-		newVariable("lbImageRepository", "docker.io"),
-	}
-	controlPlaneVarsV124 := []runtimehooksv1.Variable{
-		builtInVariablesForControlPlane("v1.24.0"),
-	}
-	machineDeploymentVars123 := []runtimehooksv1.Variable{
-		builtInVariablesForMachineDeployment("default-worker", "v1.23.0"),
-	}
-	machineDeploymentVars124 := []runtimehooksv1.Variable{
-		builtInVariablesForMachineDeployment("default-worker", "v1.24.0"),
-	}
-	kubeadmControlPlaneTemplate := controlplanev1.KubeadmControlPlaneTemplate{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "KubeadmControlPlaneTemplate",
-			APIVersion: controlplanev1.GroupVersion.String(),
-		},
-	}
-	dockerMachineTemplate := infrav1.DockerMachineTemplate{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "DockerMachineTemplate",
-			APIVersion: infrav1.GroupVersion.String(),
-		},
-	}
-	dockerClusterTemplate := infrav1.DockerClusterTemplate{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "DockerClusterTemplate",
-			APIVersion: infrav1.GroupVersion.String(),
-		},
-	}
-	kubeadmConfigTemplate := bootstrapv1.KubeadmConfigTemplate{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "KubeadmConfigTemplate",
-			APIVersion: bootstrapv1.GroupVersion.String(),
-		},
-	}
-	tests := []struct {
-		name             string
-		requestItems     []runtimehooksv1.GeneratePatchesRequestItem
-		expectedResponse *runtimehooksv1.GeneratePatchesResponse
-	}{
-		{
-			name: "With version 1.23 for all objects KubeadmControlPlane, KubeadmConfigTemplate, DockerClusterTemplate and DockerMachineTemplate",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmControlPlaneTemplate, controlPlaneVarsV123WithMaxSurge),
-				requestItem("2", dockerMachineTemplate, controlPlaneVarsV123WithMaxSurge),
-				requestItem("3", dockerMachineTemplate, machineDeploymentVars123),
-				requestItem("4", dockerClusterTemplate, lbImageRepositoryVar),
-				requestItem("5", kubeadmConfigTemplate, machineDeploymentVars123),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusSuccess,
-				},
-				Items: []runtimehooksv1.GeneratePatchesResponseItem{
-					responseItem("1", `[
-{"op":"add","path":"/spec/template/spec/kubeadmConfigSpec/initConfiguration","value":{"localAPIEndpoint":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}},
-{"op":"add","path":"/spec/template/spec/kubeadmConfigSpec/joinConfiguration","value":{"discovery":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}},
-{"op":"add","path":"/spec/template/spec/rolloutStrategy","value":{"rollingUpdate":{"maxSurge":3}}}
-]`),
-					responseItem("2", `[
-{"op":"add","path":"/spec/template/spec/customImage","value":"kindest/node:v1.23.0"}
-]`),
-					responseItem("3", `[
-{"op":"add","path":"/spec/template/spec/customImage","value":"kindest/node:v1.23.0"}
-]`),
-
-					responseItem("4", `[
-{"op":"add","path":"/spec/template/spec/loadBalancer/imageRepository","value":"docker.io"}
-]`),
-					responseItem("5", `[
-{"op":"add","path":"/spec/template/spec/joinConfiguration","value":{"discovery":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}},
-{"op":"add","path":"/spec/template/spec/initConfiguration","value":{"localAPIEndpoint":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}}
-]`),
-				},
-			},
-		},
-		{
-			name: "With version 1.24 for CP and MD no patch for KubeadmControlPlane, KubeadmConfigTemplate",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmControlPlaneTemplate, controlPlaneVarsV124),
-				requestItem("4", kubeadmConfigTemplate, machineDeploymentVars124),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusSuccess,
-				},
-				Items: []runtimehooksv1.GeneratePatchesResponseItem{
-					responseItem("1", `[]`),
-					responseItem("4", `[]`),
-				},
-			},
-		},
-		{
-			name: "With version 1.24 for CP and v1.23 for MD no patch for KubeadmControlPlane, patch KubeadmConfigTemplate",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmControlPlaneTemplate, controlPlaneVarsV124),
-				requestItem("4", kubeadmConfigTemplate, machineDeploymentVars123),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusSuccess,
-				},
-				Items: []runtimehooksv1.GeneratePatchesResponseItem{
-					responseItem("1", `[]`),
-					responseItem("4", `[
-{"op":"add","path":"/spec/template/spec/joinConfiguration","value":{"discovery":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}},
-{"op":"add","path":"/spec/template/spec/initConfiguration","value":{"localAPIEndpoint":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}}
-]`),
-				},
-			},
-		},
-		{
-			name: "With version 1.23 for CP and v1.24 for MD patch KubeadmControlPlane, no patch for KubeadmConfigTemplate",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmControlPlaneTemplate, controlPlaneVarsV123),
-				requestItem("4", kubeadmConfigTemplate, machineDeploymentVars124),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusSuccess,
-				},
-				Items: []runtimehooksv1.GeneratePatchesResponseItem{
-					responseItem("1", `[
-{"op":"add","path":"/spec/template/spec/kubeadmConfigSpec/initConfiguration","value":{"localAPIEndpoint":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}},
-{"op":"add","path":"/spec/template/spec/kubeadmConfigSpec/joinConfiguration","value":{"discovery":{},"nodeRegistration":{"kubeletExtraArgs":{"cgroup-driver":"cgroupfs"}}}}
-]`),
-					responseItem("4", `[]`),
-				},
-			},
-		},
-		{
-			name: "Error with an invalid version for the KubeadmControlPlane patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("2", kubeadmControlPlaneTemplate, controlPlaneVarsInvalidVersion),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with an invalid version for the machine deployment KubeadmConfigTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmConfigTemplate, machineDeploymentVarsInvalidVersion),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with an invalid version for the machine deployment DockerMachineTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", dockerMachineTemplate, machineDeploymentVarsInvalidVersion),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with an invalid version for the control plane DockerMachineTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", dockerMachineTemplate, controlPlaneVarsInvalidVersion),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with no variables found the control plane DockerMachineTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", dockerMachineTemplate, emptyVars),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with no variables found for the MachineDeployment DockerMachineTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", dockerMachineTemplate, emptyVars),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with no variables found for the KubeadmControlPlane patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmControlPlaneTemplate, emptyVars),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "Error with no variables found for the KubeadmConfigTemplate patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", kubeadmConfigTemplate, emptyVars),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusFailure,
-				},
-			},
-		},
-		{
-			name: "No error and no patch with no variables found for the DockerCluster patch",
-			requestItems: []runtimehooksv1.GeneratePatchesRequestItem{
-				requestItem("1", dockerClusterTemplate, emptyVars),
-			},
-			expectedResponse: &runtimehooksv1.GeneratePatchesResponse{
-				CommonResponse: runtimehooksv1.CommonResponse{
-					Status: runtimehooksv1.ResponseStatusSuccess,
-				},
-				Items: []runtimehooksv1.GeneratePatchesResponseItem{
-					responseItem("1", `[]`),
-				},
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			response := &runtimehooksv1.GeneratePatchesResponse{}
-			request := &runtimehooksv1.GeneratePatchesRequest{Items: tt.requestItems}
-			h.GeneratePatches(context.Background(), request, response)
-
-			// Expect all response fields to be as expected. responseItems are ignored here and tested below.
-			// Ignore the message to not compare error strings.
-			g.Expect(tt.expectedResponse).To(EqualObject(response, IgnorePaths{{"items"}, {"message"}}))
-			// For each item in the response check that the patches are the same.
-			// Note that the order of the individual patch operations in Items[].Patch is not determinate so we unmarshal
-			// to an array and check that the arrays hold equivalent items.
-			for i, item := range response.Items {
-				expectedItem := tt.expectedResponse.Items[i]
-				g.Expect(item.PatchType).To(Equal(expectedItem.PatchType))
-				g.Expect(item.UID).To(Equal(expectedItem.UID))
-
-				var actualPatchOps []map[string]interface{}
-				var expectedPatchOps []map[string]interface{}
-				g.Expect(json.Unmarshal(item.Patch, &actualPatchOps)).To(Succeed())
-				g.Expect(json.Unmarshal(expectedItem.Patch, &expectedPatchOps)).To(Succeed())
-				g.Expect(actualPatchOps).To(ConsistOf(expectedPatchOps))
-			}
-		})
-	}
-}
-
-// toJSONCompact is used to be able to write JSON values in a readable manner.
-func toJSONCompact(value string) []byte {
-	var compactValue bytes.Buffer
-	if err := json.Compact(&compactValue, []byte(value)); err != nil {
-		panic(err)
-	}
-	return compactValue.Bytes()
-}
-
-// toJSON marshals the object and returns a byte array. This function panics on any error.
-func toJSON(val interface{}) []byte {
-	jsonStr, err := json.Marshal(val)
-	if err != nil {
-		panic(err)
-	}
-	return jsonStr
-}
-
-// requestItem returns a GeneratePatchesRequestItem with the given uid, variables and object.
-func requestItem(uid string, object interface{}, variables []runtimehooksv1.Variable) runtimehooksv1.GeneratePatchesRequestItem {
-	return runtimehooksv1.GeneratePatchesRequestItem{
-		UID:       types.UID(uid),
-		Variables: variables,
-		Object: runtime.RawExtension{
-			Raw: toJSON(object),
-		},
-	}
-}
-
-// responseItem returns a GeneratePatchesResponseItem of PatchType JSONPatch with the passed uid and patch.
-func responseItem(uid, patch string) runtimehooksv1.GeneratePatchesResponseItem {
-	return runtimehooksv1.GeneratePatchesResponseItem{
-		UID:       types.UID(uid),
-		PatchType: runtimehooksv1.JSONPatchType,
-		Patch:     toJSONCompact(patch),
-	}
-}
-
-// builtInVariablesForControlPlane returns a runtimehooksv1.Variable for a control plane with the passed version.
-func builtInVariablesForControlPlane(version string) runtimehooksv1.Variable {
-	vars := map[string]interface{}{
-		"controlPlane": map[string]interface{}{
-			"version": version,
-		},
-	}
-	return newVariable("builtin", vars)
-}
-
-// newVariable returns a runtimehooksv1.Variable with the passed name and value.
-func newVariable(name string, value interface{}) runtimehooksv1.Variable {
-	return runtimehooksv1.Variable{
-		Name:  name,
-		Value: apiextensionsv1.JSON{Raw: toJSON(value)},
-	}
-}
-
-// builtInVariablesForMachineDeployment returns a runtimehooksv1.Variable for a machineDeployment with the passed class and version.
-func builtInVariablesForMachineDeployment(class, version string) runtimehooksv1.Variable {
-	vars := map[string]interface{}{
-		"machineDeployment": map[string]interface{}{
-			"version": version,
-			"class":   class,
-		},
-	}
-	return newVariable("builtin", vars)
-}
diff --git a/test/extension/handlers/topologymutation/lib.go b/test/extension/handlers/topologymutation/lib.go
index 75d34e5d2..66f7eac31 100644
--- a/test/extension/handlers/topologymutation/lib.go
+++ b/test/extension/handlers/topologymutation/lib.go
@@ -19,7 +19,6 @@ package topologymutation
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/pkg/errors"
 	"gomodules.xyz/jsonpatch/v2"
@@ -31,7 +30,7 @@ import (
 )
 
 // walkTemplates walks through all templates of a GeneratePatchesRequest and calls the mutateFunc.
-func walkTemplates(decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesRequest, resp *runtimehooksv1.GeneratePatchesResponse, mutateFunc func(obj runtime.Object, variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error) {
+func walkTemplates(decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesRequest, resp *runtimehooksv1.GeneratePatchesResponse, mutateFunc func(obj runtime.Object, variables map[string]apiextensionsv1.JSON) error) {
 	globalVariables := patchvariables.ToMap(req.Variables)
 
 	for _, requestItem := range req.Items {
@@ -50,7 +49,7 @@ func walkTemplates(decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesR
 
 		original := obj.DeepCopyObject()
 
-		if err := mutateFunc(obj, templateVariables, requestItem.HolderReference); err != nil {
+		if err := mutateFunc(obj, templateVariables); err != nil {
 			resp.Status = runtimehooksv1.ResponseStatusFailure
 			resp.Message = err.Error()
 			return
@@ -99,29 +98,3 @@ func createPatch(original, modified runtime.Object) ([]byte, error) {
 
 	return patchBytes, nil
 }
-
-// logRef is used to correctly render a reference with GroupVersionKind, Namespace and Name for both JSON and text logging.
-type logRef struct {
-	Group     string `json:"group,omitempty"`
-	Version   string `json:"version,omitempty"`
-	Kind      string `json:"kind,omitempty"`
-	Namespace string `json:"namespace,omitempty"`
-	Name      string `json:"name,omitempty"`
-}
-
-func (l logRef) String() string {
-	var parts []string
-	for _, s := range []string{l.Group, l.Version, l.Kind, l.Namespace, l.Name} {
-		if strings.TrimSpace(s) != "" {
-			parts = append(parts, s)
-		}
-	}
-	return strings.Join(parts, "/")
-}
-
-// MarshalLog ensures that loggers with support for structured output will log
-// as a struct by removing the String method via a custom type.
-func (l logRef) MarshalLog() interface{} {
-	type logRefWithoutStringFunc logRef
-	return logRefWithoutStringFunc(l)
-}
diff --git a/test/extension/main.go b/test/extension/main.go
index 7fcebe38e..a02d7b153 100644
--- a/test/extension/main.go
+++ b/test/extension/main.go
@@ -28,14 +28,11 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	cliflag "k8s.io/component-base/cli/flag"
 	"k8s.io/component-base/logs"
-	_ "k8s.io/component-base/logs/json/register"
 	"k8s.io/klog/v2"
 	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
-	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
 	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
 	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
 	"sigs.k8s.io/cluster-api/exp/runtime/server"
@@ -60,8 +57,6 @@ var (
 
 func init() {
 	_ = infrav1.AddToScheme(scheme)
-	_ = controlplanev1.AddToScheme(scheme)
-	_ = bootstrapv1.AddToScheme(scheme)
 
 	// Register the RuntimeHook types into the catalog.
 	_ = runtimehooksv1.AddToCatalog(catalog)
@@ -118,7 +113,6 @@ func main() {
 		os.Exit(1)
 	}
 
-	// Add the scheme with registered types to the topologyMutationHandler
 	topologyMutationHandler := topologymutation.NewHandler(scheme)
 
 	if err := webhookServer.AddExtensionHandler(server.ExtensionHandler{
diff --git a/test/infrastructure/docker/config/rbac/role.yaml b/test/infrastructure/docker/config/rbac/role.yaml
index 8eead72ea..69bc731b9 100644
--- a/test/infrastructure/docker/config/rbac/role.yaml
+++ b/test/infrastructure/docker/config/rbac/role.yaml
@@ -18,6 +18,7 @@ rules:
   resources:
   - clusters
   - machines
+  - machinesets
   verbs:
   - get
   - list
diff --git a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go b/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
index c524dcfe2..1992ce56a 100644
--- a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
+++ b/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
@@ -83,6 +83,7 @@ func (r *DockerMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl.Re
 	}
 
 	log = log.WithValues("MachinePool", machinePool.Name)
+	ctx = ctrl.LoggerInto(ctx, log)
 
 	// Fetch the Cluster.
 	cluster, err := util.GetClusterFromMetadata(ctx, r.Client, machinePool.ObjectMeta)
diff --git a/test/infrastructure/docker/internal/controllers/dockermachine_controller.go b/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
index 4a0f73f7b..520d7ff26 100644
--- a/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
+++ b/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
@@ -43,6 +43,7 @@ import (
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
+	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -56,7 +57,7 @@ type DockerMachineReconciler struct {
 
 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=dockermachines,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=dockermachines/status;dockermachines/finalizers,verbs=get;update;patch
-// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;machines,verbs=get;list;watch
+// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;machinesets;machines,verbs=get;list;watch
 // +kubebuilder:rbac:groups="",resources=secrets;,verbs=get;list;watch
 
 // Reconcile handles DockerMachine events.
@@ -73,6 +74,13 @@ func (r *DockerMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		return ctrl.Result{}, err
 	}
 
+	// AddOwners adds the owners of DockerMachine as k/v pairs to the logger.
+	// Specifically, it will add KubeadmControlPlane, MachineSet and MachineDeployment.
+	ctx, log, err := clog.AddOwners(ctx, r.Client, dockerMachine)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+
 	// Fetch the Machine.
 	machine, err := util.GetOwnerMachine(ctx, r.Client, dockerMachine.ObjectMeta)
 	if err != nil {
diff --git a/util/log/log_test.go b/util/log/log_test.go
new file mode 100644
index 000000000..6944d5782
--- /dev/null
+++ b/util/log/log_test.go
@@ -0,0 +1,216 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package log
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-logr/logr"
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+)
+
+func Test_AddObjectHierarchy(t *testing.T) {
+	g := NewWithT(t)
+
+	scheme := runtime.NewScheme()
+	g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
+
+	md := &clusterv1.MachineSet{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: clusterv1.GroupVersion.String(),
+			Kind:       "MachineDeployment",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: metav1.NamespaceDefault,
+			Name:      "development-3961-md-0-l4zn6",
+		},
+	}
+	mdOwnerRef := metav1.OwnerReference{
+		APIVersion: md.APIVersion,
+		Kind:       md.Kind,
+		Name:       md.Name,
+	}
+
+	ms := &clusterv1.MachineSet{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: clusterv1.GroupVersion.String(),
+			Kind:       "MachineSet",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace:       metav1.NamespaceDefault,
+			Name:            "development-3961-md-0-l4zn6-758c9b7677",
+			OwnerReferences: []metav1.OwnerReference{mdOwnerRef},
+		},
+	}
+	msOwnerRef := metav1.OwnerReference{
+		APIVersion: ms.APIVersion,
+		Kind:       ms.Kind,
+		Name:       ms.Name,
+	}
+
+	tests := []struct {
+		name                  string
+		obj                   metav1.Object
+		objects               []client.Object
+		expectedKeysAndValues []interface{}
+	}{
+		{
+			name: "MachineSet owning Machine is added",
+			// MachineSet does not exist in Kubernetes so only MachineSet is added
+			// and MachineDeployment is not.
+			obj: &clusterv1.Machine{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{msOwnerRef},
+					Namespace:       metav1.NamespaceDefault,
+				},
+			},
+			expectedKeysAndValues: []interface{}{
+				"MachineSet",
+				klog.ObjectRef{Namespace: ms.Namespace, Name: ms.Name},
+			},
+		},
+		{
+			name: "MachineDeployment and MachineSet owning Machine is added",
+			obj: &clusterv1.Machine{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{msOwnerRef},
+					Namespace:       metav1.NamespaceDefault,
+				},
+			},
+			objects: []client.Object{ms},
+			expectedKeysAndValues: []interface{}{
+				"MachineSet",
+				klog.ObjectRef{Namespace: ms.Namespace, Name: ms.Name},
+				"MachineDeployment",
+				klog.ObjectRef{Namespace: md.Namespace, Name: md.Name},
+			},
+		},
+		{
+			name: "MachineDeployment owning MachineSet is added",
+			obj: &clusterv1.MachineSet{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{mdOwnerRef},
+					Namespace:       metav1.NamespaceDefault,
+				},
+			},
+			expectedKeysAndValues: []interface{}{
+				"MachineDeployment",
+				klog.ObjectRef{Namespace: md.Namespace, Name: md.Name},
+			},
+		},
+		{
+			name: "KubeadmControlPlane and Machine owning DockerMachine are added",
+			obj: &unstructured.Unstructured{
+				Object: map[string]interface{}{
+					"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
+					"kind":       "DockerMachine",
+					"metadata": map[string]interface{}{
+						"ownerReferences": []interface{}{
+							map[string]interface{}{
+								"apiVersion": clusterv1.GroupVersion.String(),
+								"kind":       "Machine",
+								"name":       "development-3961-4flkb-gzxnb",
+							},
+							map[string]interface{}{
+								"apiVersion": clusterv1.GroupVersion.String(),
+								"kind":       "KubeadmControlPlane",
+								"name":       "development-3961-4flkb",
+							},
+						},
+						"namespace": metav1.NamespaceDefault,
+					},
+				},
+			},
+			expectedKeysAndValues: []interface{}{
+				"Machine",
+				klog.ObjectRef{Namespace: metav1.NamespaceDefault, Name: "development-3961-4flkb-gzxnb"},
+				"KubeadmControlPlane",
+				klog.ObjectRef{Namespace: metav1.NamespaceDefault, Name: "development-3961-4flkb"},
+			},
+		},
+		{
+			name: "Duplicate Cluster ownerRef should be deduplicated",
+			obj: &unstructured.Unstructured{
+				Object: map[string]interface{}{
+					"apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1",
+					"kind":       "DockerCluster",
+					"metadata": map[string]interface{}{
+						"ownerReferences": []interface{}{
+							map[string]interface{}{
+								"apiVersion": clusterv1.GroupVersion.String(),
+								"kind":       "Cluster",
+								"name":       "development-3961",
+							},
+							map[string]interface{}{
+								"apiVersion": clusterv1.GroupVersion.String(),
+								"kind":       "Cluster",
+								"name":       "development-3961",
+							},
+						},
+						"namespace": metav1.NamespaceDefault,
+					},
+				},
+			},
+			expectedKeysAndValues: []interface{}{
+				"Cluster",
+				klog.ObjectRef{Namespace: metav1.NamespaceDefault, Name: "development-3961"},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			c := fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.objects...).
+				Build()
+
+			// Create fake log sink so we can later verify the added k/v pairs.
+			ctx := ctrl.LoggerInto(context.Background(), logr.New(&fakeLogSink{}))
+
+			_, logger, err := AddOwners(ctx, c, tt.obj)
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(logger.GetSink().(fakeLogSink).keysAndValues).To(Equal(tt.expectedKeysAndValues))
+		})
+	}
+}
+
+type fakeLogSink struct {
+	// Embedding NullLogSink so we don't have to implement all funcs
+	// of the LogSink interface.
+	log.NullLogSink
+	keysAndValues []interface{}
+}
+
+// WithValues stores keysAndValues so we can later check if the
+// right keysAndValues have been added.
+func (f fakeLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink {
+	f.keysAndValues = keysAndValues
+	return f
+}