Repository: kubernetes-sigs/cluster-api
Pull request #9719 (2023-11-14 17:23:17)

🌱  Follow-up fixes: Add MachinePools to Runtime SDK and Rollout tests
<!-- Thanks for sending a pull request! Here are some tips for you:
    1. If this is your first time, please read our contributor guidelines: https://github.com/kubernetes-sigs/cluster-api/blob/main/CONTRIBUTING.md#contributing-a-patch and developer guide https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/book/src/developer/guide.md

    2. Please add an icon to the title of this PR (see https://sigs.k8s.io/cluster-api/CONTRIBUTING.md#contributing-a-patch), and delete this line and similar ones
    the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, feature additions), 🐛 (:bug:, patch and bugfixes), 📖 (:book:, documentation or proposals), or 🌱 (:seedling:, minor or other) 
-->

**What this PR does / why we need it**:
This PR follows up #9703 with fixes listed here: https://github.com/kubernetes-sigs/cluster-api/pull/9703#pullrequestreview-1729685341

**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 #

<!-- 
Please label this pull request according to what area(s) you are addressing. For reference on PR/issue labels, see: https://github.com/kubernetes-sigs/cluster-api/labels?q=area+

Area example:
/area runtime-sdk
-->
/area testing

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

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

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

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

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

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | ce8bf21090a8dc3fd3b26b3fea4d943d9caa8e59 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1724506707524063232) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-main | 957c0823331aa8154a778fba778eabf47eeb2faf | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-main/1724532682685157376) | true | `/test pull-cluster-api-e2e-main`
pull-cluster-api-e2e-full-main | 957c0823331aa8154a778fba778eabf47eeb2faf | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1724532574476308480) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | 957c0823331aa8154a778fba778eabf47eeb2faf | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1724608702041821184) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | 957c0823331aa8154a778fba778eabf47eeb2faf | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1724608702041821184) | true | `/test pull-cluster-api-e2e-full-main`
pull-cluster-api-test-main | 4b7a33e437bcca7ac9b9df77cdcf6decb1339e31 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-test-main/1724858074864291840) | true | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-test-main | 4b7a33e437bcca7ac9b9df77cdcf6decb1339e31 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-test-main/1724858074864291840) | true | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-full-main | 4b7a33e437bcca7ac9b9df77cdcf6decb1339e31 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1724858018031472640) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | 8e088157910f5f21c02f521084296f0dbede9541 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1725292905267990528) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | 8e088157910f5f21c02f521084296f0dbede9541 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1725324236928585728) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-cluster-api-e2e-full-main | 40680e868d881e182d2052e914ff669d8d248fff | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9719/pull-cluster-api-e2e-full-main/1725583622615863296) | true | `/test pull-cluster-api-e2e-full-main`

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

<details>

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

k8s-ci-robot: LGTM label has been added.  <details>Git tree hash: 8891fe8ddc2ad9275aa93139edbdd017482795ae</details>
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from [sbueringer](https://github.com/sbueringer). 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"]} -->
k8s-ci-robot: New changes are detected. LGTM label has been removed.

Patch:
diff --git a/.github/workflows/pr-gh-workflow-approve.yaml b/.github/workflows/pr-gh-workflow-approve.yaml
index d91cd21b8..d6c843b82 100644
--- a/.github/workflows/pr-gh-workflow-approve.yaml
+++ b/.github/workflows/pr-gh-workflow-approve.yaml
@@ -17,7 +17,7 @@ jobs:
       actions: write
     steps:
       - name: Update PR
-        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
+        uses: actions/github-script@e69ef5462fd455e02edcaf4dd7708eda96b9eda0 # v7.0.0
         continue-on-error: true
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.golangci.yml b/.golangci.yml
index 122542783..82057f946 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -139,9 +139,6 @@ linters-settings:
         alias: addonsv1alpha4
       - pkg: sigs.k8s.io/cluster-api/exp/addons/api/v1beta1
         alias: addonsv1
-      # CAPI exp IPAM
-      - pkg: sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1
-        alias: ipamv1
       # CAPI exp runtime
       - pkg: sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1
         alias: runtimev1
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 0ec44affb..ecec269c0 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -127,7 +127,7 @@ aliases:
   cluster-api-release-team:
   # members added in commented lines have a pending membership
   # and will be added back once it is acquired.
-  - adilGhaffarDev
+  # - adilGhaffarDev
   - cahillsf
   - chiukapoor
   - furkatgofurov7 # release lead
diff --git a/Tiltfile b/Tiltfile
index 1ee422acd..2ddd3140c 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -382,8 +382,7 @@ def enable_provider(name, debug):
 
 def find_object_name(objs, kind):
     for o in objs:
-        # Ignore objects that are not part of the provider, e.g. the ASO Deployment in CAPZ.
-        if o["kind"] == kind and "cluster.x-k8s.io/provider" in o["metadata"]["labels"]:
+        if o["kind"] == kind:
             return o["metadata"]["name"]
     return ""
 
diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go
index 454ec27a8..360ca8999 100644
--- a/cmd/clusterctl/client/cluster/objectgraph.go
+++ b/cmd/clusterctl/client/cluster/objectgraph.go
@@ -439,7 +439,7 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
 		}
 
 		// if we are discovering Secrets, also secrets from the providers namespace should be included.
-		if discoveryType.typeMeta.GetObjectKind().GroupVersionKind().GroupKind() == corev1.SchemeGroupVersion.WithKind("Secret").GroupKind() {
+		if discoveryType.typeMeta.GetObjectKind().GroupVersionKind().GroupKind() == corev1.SchemeGroupVersion.WithKind("SecretList").GroupKind() {
 			providers, err := o.providerInventory.List(ctx)
 			if err != nil {
 				return err
diff --git a/cmd/clusterctl/client/config/providers_client.go b/cmd/clusterctl/client/config/providers_client.go
index f74a58e4f..0c72e5b14 100644
--- a/cmd/clusterctl/client/config/providers_client.go
+++ b/cmd/clusterctl/client/config/providers_client.go
@@ -72,7 +72,6 @@ const (
 	MicroK8sBootstrapProviderName          = "microk8s"
 	OracleCloudNativeBootstrapProviderName = "ocne"
 	KubeKeyK3sBootstrapProviderName        = "kubekey-k3s"
-	RKE2BootstrapProviderName              = "rke2"
 )
 
 // ControlPlane providers.
@@ -84,7 +83,6 @@ const (
 	OracleCloudNativeControlPlaneProviderName = "ocne"
 	KubeKeyK3sControlPlaneProviderName        = "kubekey-k3s"
 	KamajiControlPlaneProviderName            = "kamaji"
-	RKE2ControlPlaneProviderName              = "rke2"
 )
 
 // Add-on providers.
@@ -297,11 +295,6 @@ func (p *providersClient) defaults() []Provider {
 			url:          "https://github.com/verrazzano/cluster-api-provider-ocne/releases/latest/bootstrap-components.yaml",
 			providerType: clusterctlv1.BootstrapProviderType,
 		},
-		&provider{
-			name:         RKE2BootstrapProviderName,
-			url:          "https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/bootstrap-components.yaml",
-			providerType: clusterctlv1.BootstrapProviderType,
-		},
 
 		// ControlPlane providers
 		&provider{
@@ -339,11 +332,6 @@ func (p *providersClient) defaults() []Provider {
 			url:          "https://github.com/clastix/cluster-api-control-plane-provider-kamaji/releases/latest/control-plane-components.yaml",
 			providerType: clusterctlv1.ControlPlaneProviderType,
 		},
-		&provider{
-			name:         RKE2ControlPlaneProviderName,
-			url:          "https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/control-plane-components.yaml",
-			providerType: clusterctlv1.ControlPlaneProviderType,
-		},
 
 		// Add-on providers
 		&provider{
diff --git a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
index 0e2fa201d..b0b82fe56 100644
--- a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
+++ b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddressclaims.yaml
@@ -146,10 +146,6 @@ spec:
       jsonPath: .spec.poolRef.kind
       name: Pool Kind
       type: string
-    - description: Time duration since creation of IPAdressClaim
-      jsonPath: .metadata.creationTimestamp
-      name: Age
-      type: date
     name: v1beta1
     schema:
       openAPIV3Schema:
diff --git a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
index 68802c550..6496c8d75 100644
--- a/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
+++ b/config/crd/bases/ipam.cluster.x-k8s.io_ipaddresses.yaml
@@ -117,10 +117,6 @@ spec:
       jsonPath: .spec.poolRef.kind
       name: Pool Kind
       type: string
-    - description: Time duration since creation of IPAdress
-      jsonPath: .metadata.creationTimestamp
-      name: Age
-      type: date
     name: v1beta1
     schema:
       openAPIV3Schema:
diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md
index a719aabae..1f4c59480 100644
--- a/docs/book/src/SUMMARY.md
+++ b/docs/book/src/SUMMARY.md
@@ -2,7 +2,6 @@
 
 [Introduction](./introduction.md)
 [Quick Start](./user/quick-start.md)
-[Quick Start Operator](./user/quick-start-operator.md)
 [Concepts](./user/concepts.md)
 [Personas](./user/personas.md)
 - [Tasks](./tasks/index.md)
diff --git a/docs/book/src/user/quick-start-operator.md b/docs/book/src/user/quick-start-operator.md
deleted file mode 100644
index 533462f40..000000000
--- a/docs/book/src/user/quick-start-operator.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Cluster API Operator Quickstart
-
-This section provides a quickstart guide for using the Cluster API Operator to create a Kubernetes cluster.
-To use the `clusterctl` quickstart path, visit [this quickstart guide](./quick-start.md).
-
-{{#embed-github repo:"kubernetes-sigs/cluster-api-operator" path:"docs/quickstart.md" }}
\ No newline at end of file
diff --git a/docs/book/src/user/quick-start.md b/docs/book/src/user/quick-start.md
index 56d519e68..b2303a356 100644
--- a/docs/book/src/user/quick-start.md
+++ b/docs/book/src/user/quick-start.md
@@ -12,16 +12,6 @@ If using a [provider] that does not support v1beta1 or v1alpha4 yet, please foll
 
 ## Installation
 
-There are two major quickstart paths:  Using clusterctl or the Cluster API Operator.  
- 
- This article describes a path that uses the `clusterctl` CLI tool to handle the lifecycle of a Cluster API [management cluster](https://cluster-api.sigs.k8s.io/reference/glossary#management-cluster).
-
-The clusterctl command line interface is specifically designed for providing a simple “day 1 experience” and a quick start with Cluster API. It automates fetching the YAML files defining [provider components](https://cluster-api.sigs.k8s.io/reference/glossary#provider-components) and installing them.
-
-Additionally it encodes a set of best practices in managing providers, that helps the user in avoiding mis-configurations or in managing day 2 operations such as upgrades.
-
-The Cluster API Operator is a Kubernetes Operator built on top of clusterctl and designed to empower cluster administrators to handle the lifecycle of Cluster API providers within a management cluster using a declarative approach. It aims to improve user experience in deploying and managing Cluster API, making it easier to handle day-to-day tasks and automate workflows with GitOps. Visit the [CAPI Operator quickstart] if you want to experiment with this tool.
-
 ### Common Prerequisites
 
 - Install and setup [kubectl] in your local environment
@@ -1623,4 +1613,3 @@ kind delete cluster
 [provider components]: ../reference/glossary.md#provider-components
 [vSphere getting started guide]: https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/blob/master/docs/getting_started.md
 [workload cluster]: ../reference/glossary.md#workload-cluster
-[CAPI Operator quickstart]: ./quick-start-operator.md
\ No newline at end of file
diff --git a/exp/internal/controllers/machinepool_controller_phases.go b/exp/internal/controllers/machinepool_controller_phases.go
index cbd9c8a35..0eed26251 100644
--- a/exp/internal/controllers/machinepool_controller_phases.go
+++ b/exp/internal/controllers/machinepool_controller_phases.go
@@ -47,7 +47,6 @@ import (
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
-	"sigs.k8s.io/cluster-api/util/labels"
 	"sigs.k8s.io/cluster-api/util/labels/format"
 	"sigs.k8s.io/cluster-api/util/patch"
 )
@@ -486,10 +485,12 @@ func computeDesiredMachine(mp *expv1.MachinePool, infraMachine *unstructured.Uns
 func (r *MachinePoolReconciler) infraMachineToMachinePoolMapper(ctx context.Context, o client.Object) []ctrl.Request {
 	log := ctrl.LoggerFrom(ctx)
 
-	if labels.IsMachinePoolOwned(o) {
-		machinePool, err := utilexp.GetMachinePoolByLabels(ctx, r.Client, o.GetNamespace(), o.GetLabels())
+	labels := o.GetLabels()
+	_, machinePoolOwned := labels[clusterv1.MachinePoolNameLabel]
+	if machinePoolOwned {
+		machinePool, err := utilexp.GetMachinePoolByLabels(ctx, r.Client, o.GetNamespace(), labels)
 		if err != nil {
-			log.Error(err, "failed to get MachinePool for InfraMachine", "infraMachine", klog.KObj(o), "labels", o.GetLabels())
+			log.Error(err, "failed to get MachinePool for InfraMachine", "infraMachine", klog.KObj(o), "labels", labels)
 			return nil
 		}
 		if machinePool != nil {
diff --git a/exp/ipam/api/v1alpha1/conversion.go b/exp/ipam/api/v1alpha1/conversion.go
index 72df082a5..3cc2a0d3a 100644
--- a/exp/ipam/api/v1alpha1/conversion.go
+++ b/exp/ipam/api/v1alpha1/conversion.go
@@ -19,53 +19,53 @@ package v1alpha1
 import (
 	"sigs.k8s.io/controller-runtime/pkg/conversion"
 
-	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
+	ipamv1beta1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
 )
 
 func (src *IPAddress) ConvertTo(dstRaw conversion.Hub) error {
-	dst := dstRaw.(*ipamv1.IPAddress)
+	dst := dstRaw.(*ipamv1beta1.IPAddress)
 
 	return Convert_v1alpha1_IPAddress_To_v1beta1_IPAddress(src, dst, nil)
 }
 
 func (dst *IPAddress) ConvertFrom(srcRaw conversion.Hub) error {
-	src := srcRaw.(*ipamv1.IPAddress)
+	src := srcRaw.(*ipamv1beta1.IPAddress)
 
 	return Convert_v1beta1_IPAddress_To_v1alpha1_IPAddress(src, dst, nil)
 }
 
 func (src *IPAddressList) ConvertTo(dstRaw conversion.Hub) error {
-	dst := dstRaw.(*ipamv1.IPAddressList)
+	dst := dstRaw.(*ipamv1beta1.IPAddressList)
 
 	return Convert_v1alpha1_IPAddressList_To_v1beta1_IPAddressList(src, dst, nil)
 }
 
 func (dst *IPAddressList) ConvertFrom(srcRaw conversion.Hub) error {
-	src := srcRaw.(*ipamv1.IPAddressList)
+	src := srcRaw.(*ipamv1beta1.IPAddressList)
 
 	return Convert_v1beta1_IPAddressList_To_v1alpha1_IPAddressList(src, dst, nil)
 }
 
 func (src *IPAddressClaim) ConvertTo(dstRaw conversion.Hub) error {
-	dst := dstRaw.(*ipamv1.IPAddressClaim)
+	dst := dstRaw.(*ipamv1beta1.IPAddressClaim)
 
 	return Convert_v1alpha1_IPAddressClaim_To_v1beta1_IPAddressClaim(src, dst, nil)
 }
 
 func (dst *IPAddressClaim) ConvertFrom(srcRaw conversion.Hub) error {
-	src := srcRaw.(*ipamv1.IPAddressClaim)
+	src := srcRaw.(*ipamv1beta1.IPAddressClaim)
 
 	return Convert_v1beta1_IPAddressClaim_To_v1alpha1_IPAddressClaim(src, dst, nil)
 }
 
 func (src *IPAddressClaimList) ConvertTo(dstRaw conversion.Hub) error {
-	dst := dstRaw.(*ipamv1.IPAddressClaimList)
+	dst := dstRaw.(*ipamv1beta1.IPAddressClaimList)
 
 	return Convert_v1alpha1_IPAddressClaimList_To_v1beta1_IPAddressClaimList(src, dst, nil)
 }
 
 func (dst *IPAddressClaimList) ConvertFrom(srcRaw conversion.Hub) error {
-	src := srcRaw.(*ipamv1.IPAddressClaimList)
+	src := srcRaw.(*ipamv1beta1.IPAddressClaimList)
 
 	return Convert_v1beta1_IPAddressClaimList_To_v1alpha1_IPAddressClaimList(src, dst, nil)
 }
diff --git a/exp/ipam/api/v1beta1/ipaddress_types.go b/exp/ipam/api/v1beta1/ipaddress_types.go
index 4f37aad81..c14860cdc 100644
--- a/exp/ipam/api/v1beta1/ipaddress_types.go
+++ b/exp/ipam/api/v1beta1/ipaddress_types.go
@@ -46,7 +46,6 @@ type IPAddressSpec struct {
 // +kubebuilder:printcolumn:name="Address",type="string",JSONPath=".spec.address",description="Address"
 // +kubebuilder:printcolumn:name="Pool Name",type="string",JSONPath=".spec.poolRef.name",description="Name of the pool the address is from"
 // +kubebuilder:printcolumn:name="Pool Kind",type="string",JSONPath=".spec.poolRef.kind",description="Kind of the pool the address is from"
-// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of IPAdress"
 
 // IPAddress is the Schema for the ipaddress API.
 type IPAddress struct {
diff --git a/exp/ipam/api/v1beta1/ipaddressclaim_types.go b/exp/ipam/api/v1beta1/ipaddressclaim_types.go
index 8910e9118..fd8c7e424 100644
--- a/exp/ipam/api/v1beta1/ipaddressclaim_types.go
+++ b/exp/ipam/api/v1beta1/ipaddressclaim_types.go
@@ -46,7 +46,6 @@ type IPAddressClaimStatus struct {
 // +kubebuilder:storageversion
 // +kubebuilder:printcolumn:name="Pool Name",type="string",JSONPath=".spec.poolRef.name",description="Name of the pool to allocate an address from"
 // +kubebuilder:printcolumn:name="Pool Kind",type="string",JSONPath=".spec.poolRef.kind",description="Kind of the pool to allocate an address from"
-// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of IPAdressClaim"
 
 // IPAddressClaim is the Schema for the ipaddressclaim API.
 type IPAddressClaim struct {
diff --git a/go.mod b/go.mod
index 5fd1ce90d..6cca59021 100644
--- a/go.mod
+++ b/go.mod
@@ -35,16 +35,16 @@ require (
 	golang.org/x/text v0.14.0
 	gomodules.xyz/jsonpatch/v2 v2.4.0
 	google.golang.org/grpc v1.59.0
-	k8s.io/api v0.28.4
-	k8s.io/apiextensions-apiserver v0.28.4
-	k8s.io/apimachinery v0.28.4
-	k8s.io/apiserver v0.28.4
-	k8s.io/client-go v0.28.4
-	k8s.io/cluster-bootstrap v0.28.4
-	k8s.io/component-base v0.28.4
+	k8s.io/api v0.28.3
+	k8s.io/apiextensions-apiserver v0.28.3
+	k8s.io/apimachinery v0.28.3
+	k8s.io/apiserver v0.28.3
+	k8s.io/client-go v0.28.3
+	k8s.io/cluster-bootstrap v0.28.3
+	k8s.io/component-base v0.28.3
 	k8s.io/klog/v2 v2.100.1
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9
-	k8s.io/kubectl v0.28.4
+	k8s.io/kubectl v0.28.3
 	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
 	sigs.k8s.io/controller-runtime v0.16.3
 	sigs.k8s.io/yaml v1.4.0
@@ -174,10 +174,10 @@ require (
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/cli-runtime v0.28.4 // indirect
-	k8s.io/component-helpers v0.28.4 // indirect
-	k8s.io/kms v0.28.4 // indirect
-	k8s.io/metrics v0.28.4 // indirect
+	k8s.io/cli-runtime v0.28.3 // indirect
+	k8s.io/component-helpers v0.28.3 // indirect
+	k8s.io/kms v0.28.3 // indirect
+	k8s.io/metrics v0.28.3 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
diff --git a/go.sum b/go.sum
index d8012dfe8..18a9bb3b0 100644
--- a/go.sum
+++ b/go.sum
@@ -1007,34 +1007,34 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
-k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
-k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU=
-k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM=
-k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
-k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
-k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg=
-k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w=
-k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q=
-k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k=
-k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY=
-k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4=
-k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q=
-k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc=
-k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo=
-k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU=
-k8s.io/component-helpers v0.28.4 h1:+X9VXT5+jUsRdC26JyMZ8Fjfln7mSjgumafocE509C4=
-k8s.io/component-helpers v0.28.4/go.mod h1:8LzMalOQ0K10tkBJWBWq8h0HTI9HDPx4WT3QvTFn9Ro=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
+k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w=
+k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
+k8s.io/cli-runtime v0.28.3 h1:lvuJYVkwCqHEvpS6KuTZsUVwPePFjBfSGvuaLl2SxzA=
+k8s.io/cli-runtime v0.28.3/go.mod h1:jeX37ZPjIcENVuXDDTskG3+FnVuZms5D9omDXS/2Jjc=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
+k8s.io/cluster-bootstrap v0.28.3 h1:hGK3mJsmVGGvRJ61nyQcYNR9g/IYax75TbJcylTmZts=
+k8s.io/cluster-bootstrap v0.28.3/go.mod h1:s1B3FTw713b9iw67yGFiVF3zCfw5obrZXWl3EMelvdg=
+k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
+k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
+k8s.io/component-helpers v0.28.3 h1:te9ieTGzcztVktUs92X53P6BamAoP73MK0qQP0WmDqc=
+k8s.io/component-helpers v0.28.3/go.mod h1:oJR7I9ist5UAQ3y/CTdbw6CXxdMZ1Lw2Ua/EZEwnVLs=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kms v0.28.4 h1:PMgY/3CQTWP9eIKmNQiTgjLIZ0ns6O+voagzD2/4mSg=
-k8s.io/kms v0.28.4/go.mod h1:HL4/lR/bhjAJPbqycKtfhWiKh1Sp21cpHOL8P4oo87w=
+k8s.io/kms v0.28.3 h1:jYwwAe96XELNjYWv1G4kNzizcFoZ50OOElvPansbw70=
+k8s.io/kms v0.28.3/go.mod h1:kSMjU2tg7vjqqoWVVCcmPmNZ/CofPsoTbSxAipCvZuE=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
-k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ=
-k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c=
-k8s.io/metrics v0.28.4 h1:u36fom9+6c8jX2sk8z58H0hFaIUfrPWbXIxN7GT2blk=
-k8s.io/metrics v0.28.4/go.mod h1:bBqAJxH20c7wAsTQxDXOlVqxGMdce49d7WNr1WeaLac=
+k8s.io/kubectl v0.28.3 h1:H1Peu1O3EbN9zHkJCcvhiJ4NUj6lb88sGPO5wrWIM6k=
+k8s.io/kubectl v0.28.3/go.mod h1:RDAudrth/2wQ3Sg46fbKKl4/g+XImzvbsSRZdP2RiyE=
+k8s.io/metrics v0.28.3 h1:w2s3kVi7HulXqCVDFkF4hN/OsL1tXTTb4Biif995h/g=
+k8s.io/metrics v0.28.3/go.mod h1:OZZ23AHFojPzU6r3xoHGRUcV3I9pauLua+07sAUbwLc=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 9ef6f6fa0..4f096fc9a 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -7,16 +7,16 @@ replace sigs.k8s.io/cluster-api => ../../
 replace sigs.k8s.io/cluster-api/test => ../../test
 
 require (
-	cloud.google.com/go/storage v1.35.1
+	cloud.google.com/go/storage v1.34.1
 	github.com/blang/semver/v4 v4.0.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/pflag v1.0.5
 	github.com/valyala/fastjson v1.6.4
 	google.golang.org/api v0.150.0
-	k8s.io/api v0.28.4
-	k8s.io/apiextensions-apiserver v0.28.4
-	k8s.io/apimachinery v0.28.4
-	k8s.io/client-go v0.28.4
+	k8s.io/api v0.28.3
+	k8s.io/apiextensions-apiserver v0.28.3
+	k8s.io/apimachinery v0.28.3
+	k8s.io/client-go v0.28.3
 	k8s.io/klog/v2 v2.100.1
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9
 	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
@@ -132,9 +132,9 @@ require (
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiserver v0.28.4 // indirect
-	k8s.io/cluster-bootstrap v0.28.4 // indirect
-	k8s.io/component-base v0.28.4 // indirect
+	k8s.io/apiserver v0.28.3 // indirect
+	k8s.io/cluster-bootstrap v0.28.3 // indirect
+	k8s.io/component-base v0.28.3 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 5fdaed1bd..5fefb0a80 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -43,8 +43,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
-cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
+cloud.google.com/go/storage v1.34.1 h1:H2Af2dU5J0PF7A5B+ECFIce+RqxVnrVilO+cu0TS3MI=
+cloud.google.com/go/storage v1.34.1/go.mod h1:VN1ElqqvR9adg1k9xlkUJ55cMOP1/QjnNNuT5xQL6dY=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -724,20 +724,20 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
-k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
-k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU=
-k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM=
-k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
-k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
-k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg=
-k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w=
-k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY=
-k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4=
-k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q=
-k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc=
-k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo=
-k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
+k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w=
+k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
+k8s.io/cluster-bootstrap v0.28.3 h1:hGK3mJsmVGGvRJ61nyQcYNR9g/IYax75TbJcylTmZts=
+k8s.io/cluster-bootstrap v0.28.3/go.mod h1:s1B3FTw713b9iw67yGFiVF3zCfw5obrZXWl3EMelvdg=
+k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
+k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
diff --git a/hack/tools/internal/tilt-prepare/main.go b/hack/tools/internal/tilt-prepare/main.go
index 9b914ed68..52d8d41e1 100644
--- a/hack/tools/internal/tilt-prepare/main.go
+++ b/hack/tools/internal/tilt-prepare/main.go
@@ -842,10 +842,6 @@ func updateDeployment(prefix string, objs []unstructured.Unstructured, f updateD
 		if obj.GetKind() != "Deployment" {
 			continue
 		}
-		// Ignore Deployments that are not part of the provider, eg. ASO in CAPZ.
-		if _, exists := obj.GetLabels()[clusterv1.ProviderNameLabel]; !exists {
-			continue
-		}
 
 		// Convert Unstructured into a typed object
 		d := &appsv1.Deployment{}
diff --git a/internal/contract/infrastructure_machinepool_template.go b/internal/contract/infrastructure_machinepool_template.go
new file mode 100644
index 000000000..773190480
--- /dev/null
+++ b/internal/contract/infrastructure_machinepool_template.go
@@ -0,0 +1,51 @@
+/*
+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 contract
+
+import (
+	"sync"
+)
+
+// InfrastructureMachinePoolTemplateContract encodes information about the Cluster API contract for InfrastructureMachinePoolTemplate objects
+// like DockerMachinePoolTemplates, AWSMachinePoolTemplates, etc.
+type InfrastructureMachinePoolTemplateContract struct{}
+
+var infrastructureMachinePoolTemplate *InfrastructureMachinePoolTemplateContract
+var onceInfrastructureMachinePoolTemplate sync.Once
+
+// InfrastructureMachinePoolTemplate provide access to the information about the Cluster API contract for InfrastructureMachinePoolTemplate objects.
+func InfrastructureMachinePoolTemplate() *InfrastructureMachinePoolTemplateContract {
+	onceInfrastructureMachinePoolTemplate.Do(func() {
+		infrastructureMachinePoolTemplate = &InfrastructureMachinePoolTemplateContract{}
+	})
+	return infrastructureMachinePoolTemplate
+}
+
+// Template provides access to the template.
+func (c *InfrastructureMachinePoolTemplateContract) Template() *InfrastructureMachinePoolTemplateTemplate {
+	return &InfrastructureMachinePoolTemplateTemplate{}
+}
+
+// InfrastructureMachinePoolTemplateTemplate provides a helper struct for working with the template in an InfrastructureMachinePoolTemplate.
+type InfrastructureMachinePoolTemplateTemplate struct{}
+
+// Metadata provides access to the metadata of a template.
+func (c *InfrastructureMachinePoolTemplateTemplate) Metadata() *Metadata {
+	return &Metadata{
+		path: Path{"spec", "template", "metadata"},
+	}
+}
diff --git a/internal/webhooks/machine.go b/internal/webhooks/machine.go
index 58cbbfb23..f36cee1f3 100644
--- a/internal/webhooks/machine.go
+++ b/internal/webhooks/machine.go
@@ -31,7 +31,6 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
-	"sigs.k8s.io/cluster-api/util/labels"
 	"sigs.k8s.io/cluster-api/util/version"
 )
 
@@ -121,7 +120,7 @@ func (webhook *Machine) validate(oldM, newM *clusterv1.Machine) error {
 	specPath := field.NewPath("spec")
 	if newM.Spec.Bootstrap.ConfigRef == nil && newM.Spec.Bootstrap.DataSecretName == nil {
 		// MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool.
-		if !labels.IsMachinePoolOwned(newM) {
+		if !isMachinePoolMachine(newM) {
 			allErrs = append(
 				allErrs,
 				field.Required(
@@ -172,3 +171,19 @@ func (webhook *Machine) validate(oldM, newM *clusterv1.Machine) error {
 	}
 	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Machine").GroupKind(), newM.Name, allErrs)
 }
+
+func isMachinePoolMachine(m *clusterv1.Machine) bool {
+	if m.Labels != nil {
+		if _, ok := m.Labels[clusterv1.MachinePoolNameLabel]; ok {
+			return true
+		}
+	}
+
+	for _, owner := range m.OwnerReferences {
+		if owner.Kind == "MachinePool" {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/util/labels/helpers.go b/util/labels/helpers.go
index c13ee48e1..848ec37cd 100644
--- a/util/labels/helpers.go
+++ b/util/labels/helpers.go
@@ -29,21 +29,6 @@ func IsTopologyOwned(o metav1.Object) bool {
 	return ok
 }
 
-// IsMachinePoolOwned returns true if the object has the `cluster.x-k8s.io/pool-name` label or is owned by a MachinePool.
-func IsMachinePoolOwned(o metav1.Object) bool {
-	if _, ok := o.GetLabels()[clusterv1.MachinePoolNameLabel]; ok {
-		return true
-	}
-
-	for _, owner := range o.GetOwnerReferences() {
-		if owner.Kind == "MachinePool" {
-			return true
-		}
-	}
-
-	return false
-}
-
 // HasWatchLabel returns true if the object has a label with the WatchLabel key matching the given value.
 func HasWatchLabel(o metav1.Object, labelValue string) bool {
 	val, ok := o.GetLabels()[clusterv1.WatchLabel]


Test patch:
diff --git a/cmd/clusterctl/client/cluster/mover_test.go b/cmd/clusterctl/client/cluster/mover_test.go
index b4100b007..08481760a 100644
--- a/cmd/clusterctl/client/cluster/mover_test.go
+++ b/cmd/clusterctl/client/cluster/mover_test.go
@@ -735,7 +735,7 @@ func Test_objectMover_backupTargetObject(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -826,7 +826,7 @@ func Test_objectMover_restoreTargetObject(t *testing.T) {
 			graph := getObjectGraph()
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -934,7 +934,7 @@ func Test_objectMover_toDirectory(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1077,7 +1077,7 @@ func Test_objectMover_fromDirectory(t *testing.T) {
 			graph := getObjectGraph()
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// gets a fakeProxy to an empty cluster with all the required CRDs
 			toProxy := getFakeProxyWithCRDs()
@@ -1155,7 +1155,7 @@ func Test_getMoveSequence(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1188,7 +1188,7 @@ func Test_objectMover_move_dryRun(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1263,7 +1263,7 @@ func Test_objectMover_move(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1375,7 +1375,7 @@ func Test_objectMover_move_with_Mutator(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1660,7 +1660,7 @@ func Test_objectMover_checkProvisioningCompleted(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -1906,7 +1906,7 @@ func Test_objectMoverService_ensureNamespaces(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.fields.objs)
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// Trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
@@ -2385,7 +2385,7 @@ func TestWaitReadyForMove(t *testing.T) {
 			}
 
 			// Get all the types to be considered for discovery
-			g.Expect(graph.getDiscoveryTypes(ctx)).To(Succeed())
+			g.Expect(getFakeDiscoveryTypes(ctx, graph)).To(Succeed())
 
 			// trigger discovery the content of the source cluster
 			g.Expect(graph.Discovery(ctx, "")).To(Succeed())
diff --git a/cmd/clusterctl/client/cluster/objectgraph_test.go b/cmd/clusterctl/client/cluster/objectgraph_test.go
index 258acda9d..be2486913 100644
--- a/cmd/clusterctl/client/cluster/objectgraph_test.go
+++ b/cmd/clusterctl/client/cluster/objectgraph_test.go
@@ -1774,6 +1774,18 @@ func getFakeProxyWithCRDs() *test.FakeProxy {
 	return proxy
 }
 
+func getFakeDiscoveryTypes(ctx context.Context, graph *objectGraph) error {
+	if err := graph.getDiscoveryTypes(ctx); err != nil {
+		return err
+	}
+
+	// Given that the Fake client behaves in a different way than real client, for this test we are required to add the List suffix to all the types.
+	for _, discoveryType := range graph.types {
+		discoveryType.typeMeta.Kind = fmt.Sprintf("%sList", discoveryType.typeMeta.Kind)
+	}
+	return nil
+}
+
 func TestObjectGraph_Discovery(t *testing.T) {
 	// NB. we are testing the graph is properly built starting from objects (TestGraphBuilder_addObj_WithFakeObjects) or from the same objects read from the cluster (this test).
 	for _, tt := range objectGraphsTests {
@@ -1786,7 +1798,7 @@ func TestObjectGraph_Discovery(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.args.objs)
 
 			// Get all the types to be considered for discovery
-			err := graph.getDiscoveryTypes(ctx)
+			err := getFakeDiscoveryTypes(ctx, graph)
 			g.Expect(err).ToNot(HaveOccurred())
 
 			// finally test discovery
@@ -1944,7 +1956,7 @@ func TestObjectGraph_DiscoveryByNamespace(t *testing.T) {
 			graph := getObjectGraphWithObjs(tt.args.objs)
 
 			// Get all the types to be considered for discovery
-			err := graph.getDiscoveryTypes(ctx)
+			err := getFakeDiscoveryTypes(ctx, graph)
 			g.Expect(err).ToNot(HaveOccurred())
 
 			// finally test discovery
diff --git a/cmd/clusterctl/client/config_test.go b/cmd/clusterctl/client/config_test.go
index f8beeee2b..947bd18bb 100644
--- a/cmd/clusterctl/client/config_test.go
+++ b/cmd/clusterctl/client/config_test.go
@@ -61,7 +61,6 @@ func Test_clusterctlClient_GetProvidersConfig(t *testing.T) {
 				config.KubeKeyK3sBootstrapProviderName,
 				config.MicroK8sBootstrapProviderName,
 				config.OracleCloudNativeBootstrapProviderName,
-				config.RKE2BootstrapProviderName,
 				config.TalosBootstrapProviderName,
 				config.KamajiControlPlaneProviderName,
 				config.KubeadmControlPlaneProviderName,
@@ -69,7 +68,6 @@ func Test_clusterctlClient_GetProvidersConfig(t *testing.T) {
 				config.MicroK8sControlPlaneProviderName,
 				config.NestedControlPlaneProviderName,
 				config.OracleCloudNativeControlPlaneProviderName,
-				config.RKE2ControlPlaneProviderName,
 				config.TalosControlPlaneProviderName,
 				config.AWSProviderName,
 				config.AzureProviderName,
@@ -114,7 +112,6 @@ func Test_clusterctlClient_GetProvidersConfig(t *testing.T) {
 				config.KubeKeyK3sBootstrapProviderName,
 				config.MicroK8sBootstrapProviderName,
 				config.OracleCloudNativeBootstrapProviderName,
-				config.RKE2BootstrapProviderName,
 				config.TalosBootstrapProviderName,
 				config.KamajiControlPlaneProviderName,
 				config.KubeadmControlPlaneProviderName,
@@ -122,7 +119,6 @@ func Test_clusterctlClient_GetProvidersConfig(t *testing.T) {
 				config.MicroK8sControlPlaneProviderName,
 				config.NestedControlPlaneProviderName,
 				config.OracleCloudNativeControlPlaneProviderName,
-				config.RKE2ControlPlaneProviderName,
 				config.TalosControlPlaneProviderName,
 				config.AWSProviderName,
 				config.AzureProviderName,
diff --git a/cmd/clusterctl/cmd/config_repositories_test.go b/cmd/clusterctl/cmd/config_repositories_test.go
index 2b419abc8..7ac413e19 100644
--- a/cmd/clusterctl/cmd/config_repositories_test.go
+++ b/cmd/clusterctl/cmd/config_repositories_test.go
@@ -109,7 +109,6 @@ kubeadm             BootstrapProvider        https://github.com/kubernetes-sigs/
 kubekey-k3s         BootstrapProvider        https://github.com/kubesphere/kubekey/releases/latest/                                      bootstrap-components.yaml
 microk8s            BootstrapProvider        https://github.com/canonical/cluster-api-bootstrap-provider-microk8s/releases/latest/       bootstrap-components.yaml
 ocne                BootstrapProvider        https://github.com/verrazzano/cluster-api-provider-ocne/releases/latest/                    bootstrap-components.yaml
-rke2                BootstrapProvider        https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/               bootstrap-components.yaml
 talos               BootstrapProvider        https://github.com/siderolabs/cluster-api-bootstrap-provider-talos/releases/latest/         bootstrap-components.yaml
 kamaji              ControlPlaneProvider     https://github.com/clastix/cluster-api-control-plane-provider-kamaji/releases/latest/       control-plane-components.yaml
 kubeadm             ControlPlaneProvider     https://github.com/kubernetes-sigs/cluster-api/releases/latest/                             control-plane-components.yaml
@@ -117,7 +116,6 @@ kubekey-k3s         ControlPlaneProvider     https://github.com/kubesphere/kubek
 microk8s            ControlPlaneProvider     https://github.com/canonical/cluster-api-control-plane-provider-microk8s/releases/latest/   control-plane-components.yaml
 nested              ControlPlaneProvider     https://github.com/kubernetes-sigs/cluster-api-provider-nested/releases/latest/             control-plane-components.yaml
 ocne                ControlPlaneProvider     https://github.com/verrazzano/cluster-api-provider-ocne/releases/latest/                    control-plane-components.yaml
-rke2                ControlPlaneProvider     https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/               control-plane-components.yaml
 talos               ControlPlaneProvider     https://github.com/siderolabs/cluster-api-control-plane-provider-talos/releases/latest/     control-plane-components.yaml
 aws                 InfrastructureProvider                                                                                               my-aws-infrastructure-components.yaml
 azure               InfrastructureProvider   https://github.com/kubernetes-sigs/cluster-api-provider-azure/releases/latest/              infrastructure-components.yaml
@@ -173,10 +171,6 @@ var expectedOutputYaml = `- File: core_components.yaml
   Name: ocne
   ProviderType: BootstrapProvider
   URL: https://github.com/verrazzano/cluster-api-provider-ocne/releases/latest/
-- File: bootstrap-components.yaml
-  Name: rke2
-  ProviderType: BootstrapProvider
-  URL: https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/
 - File: bootstrap-components.yaml
   Name: talos
   ProviderType: BootstrapProvider
@@ -205,10 +199,6 @@ var expectedOutputYaml = `- File: core_components.yaml
   Name: ocne
   ProviderType: ControlPlaneProvider
   URL: https://github.com/verrazzano/cluster-api-provider-ocne/releases/latest/
-- File: control-plane-components.yaml
-  Name: rke2
-  ProviderType: ControlPlaneProvider
-  URL: https://github.com/rancher-sandbox/cluster-api-provider-rke2/releases/latest/
 - File: control-plane-components.yaml
   Name: talos
   ProviderType: ControlPlaneProvider
diff --git a/exp/ipam/api/v1alpha1/conversion_test.go b/exp/ipam/api/v1alpha1/conversion_test.go
index 6d298ed2a..181109b39 100644
--- a/exp/ipam/api/v1alpha1/conversion_test.go
+++ b/exp/ipam/api/v1alpha1/conversion_test.go
@@ -21,18 +21,18 @@ import (
 
 	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
 
-	ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
+	ipamv1beta1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 )
 
 func TestFuzzyConversion(t *testing.T) {
 	t.Run("for IPAddress", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{
-		Hub:         &ipamv1.IPAddress{},
+		Hub:         &ipamv1beta1.IPAddress{},
 		Spoke:       &IPAddress{},
 		FuzzerFuncs: []fuzzer.FuzzerFuncs{},
 	}))
 	t.Run("for IPAddressClaim", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{
-		Hub:         &ipamv1.IPAddressClaim{},
+		Hub:         &ipamv1beta1.IPAddressClaim{},
 		Spoke:       &IPAddressClaim{},
 		FuzzerFuncs: []fuzzer.FuzzerFuncs{},
 	}))
diff --git a/internal/topology/variables/cluster_variable_defaulting_test.go b/internal/topology/variables/cluster_variable_defaulting_test.go
index b6a7e72e5..f441264e6 100644
--- a/internal/topology/variables/cluster_variable_defaulting_test.go
+++ b/internal/topology/variables/cluster_variable_defaulting_test.go
@@ -929,6 +929,7 @@ func Test_DefaultClusterVariable(t *testing.T) {
 					Schema: clusterv1.VariableSchema{
 						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
 							Type: "object",
+
 							Properties: map[string]clusterv1.JSONSchemaProps{
 								"enabled": {
 									Type:    "boolean",
@@ -948,76 +949,6 @@ func Test_DefaultClusterVariable(t *testing.T) {
 			createVariable: true,
 			want:           nil,
 		},
-		{
-			name: "Default new object variable if there is a top-level default",
-			clusterClassVariable: &statusVariableDefinition{
-				Name: "httpProxy",
-				ClusterClassStatusVariableDefinition: &clusterv1.ClusterClassStatusVariableDefinition{
-					Required: true,
-					Schema: clusterv1.VariableSchema{
-						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
-							Type:    "object",
-							Default: &apiextensionsv1.JSON{Raw: []byte(`{"url":"test-url"}`)},
-							Properties: map[string]clusterv1.JSONSchemaProps{
-								"enabled": {
-									Type:    "boolean",
-									Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
-								},
-								"url": {
-									Type: "string",
-								},
-								"noProxy": {
-									Type: "string",
-								},
-							},
-						},
-					},
-				},
-			},
-			createVariable: true,
-			want: &clusterv1.ClusterVariable{
-				Name: "httpProxy",
-				Value: apiextensionsv1.JSON{
-					// Defaulting first adds the top-level object and then the enabled field.
-					Raw: []byte(`{"enabled":false,"url":"test-url"}`),
-				},
-			},
-		},
-		{
-			name: "Default new object variable if there is a top-level default (which is an empty object)",
-			clusterClassVariable: &statusVariableDefinition{
-				Name: "httpProxy",
-				ClusterClassStatusVariableDefinition: &clusterv1.ClusterClassStatusVariableDefinition{
-					Required: true,
-					Schema: clusterv1.VariableSchema{
-						OpenAPIV3Schema: clusterv1.JSONSchemaProps{
-							Type:    "object",
-							Default: &apiextensionsv1.JSON{Raw: []byte(`{}`)},
-							Properties: map[string]clusterv1.JSONSchemaProps{
-								"enabled": {
-									Type:    "boolean",
-									Default: &apiextensionsv1.JSON{Raw: []byte(`false`)},
-								},
-								"url": {
-									Type: "string",
-								},
-								"noProxy": {
-									Type: "string",
-								},
-							},
-						},
-					},
-				},
-			},
-			createVariable: true,
-			want: &clusterv1.ClusterVariable{
-				Name: "httpProxy",
-				Value: apiextensionsv1.JSON{
-					// Defaulting first adds the top-level empty object and then the enabled field.
-					Raw: []byte(`{"enabled":false}`),
-				},
-			},
-		},
 		{
 			name: "Don't default existing object variable",
 			clusterClassVariable: &statusVariableDefinition{
diff --git a/internal/webhooks/machine_test.go b/internal/webhooks/machine_test.go
index 3568250cb..9e4c2a87a 100644
--- a/internal/webhooks/machine_test.go
+++ b/internal/webhooks/machine_test.go
@@ -220,6 +220,60 @@ func TestMachineClusterNameImmutable(t *testing.T) {
 	}
 }
 
+func TestIsMachinePoolMachine(t *testing.T) {
+	tests := []struct {
+		name    string
+		machine clusterv1.Machine
+		isMPM   bool
+	}{
+		{
+			name: "machine is a MachinePoolMachine",
+			machine: clusterv1.Machine{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{
+						{
+							Kind: "MachinePool",
+						},
+					},
+				},
+			},
+			isMPM: true,
+		},
+		{
+			name: "machine is not a MachinePoolMachine",
+			machine: clusterv1.Machine{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: []metav1.OwnerReference{
+						{
+							Kind: "NotMachinePool",
+						},
+					},
+				},
+			},
+			isMPM: false,
+		},
+		{
+			name: "machine is not a MachinePoolMachine, no owner references",
+			machine: clusterv1.Machine{
+				ObjectMeta: metav1.ObjectMeta{
+					OwnerReferences: nil,
+				},
+			},
+			isMPM: false,
+		},
+	}
+
+	for i := range tests {
+		tt := tests[i]
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			result := isMachinePoolMachine(&tt.machine)
+			g.Expect(result).To(Equal(tt.isMPM))
+		})
+	}
+}
+
 func TestMachineVersionValidation(t *testing.T) {
 	tests := []struct {
 		name      string
diff --git a/test/e2e/cluster_upgrade.go b/test/e2e/cluster_upgrade.go
index 8e1c2827d..4b3b59216 100644
--- a/test/e2e/cluster_upgrade.go
+++ b/test/e2e/cluster_upgrade.go
@@ -155,19 +155,18 @@ func ClusterUpgradeConformanceSpec(ctx context.Context, inputGetter func() Clust
 			// Cluster is using ClusterClass, upgrade via topology.
 			By("Upgrading the Cluster topology")
 			framework.UpgradeClusterTopologyAndWaitForUpgrade(ctx, framework.UpgradeClusterTopologyAndWaitForUpgradeInput{
-				ClusterProxy:                   input.BootstrapClusterProxy,
-				Cluster:                        clusterResources.Cluster,
-				ControlPlane:                   clusterResources.ControlPlane,
-				EtcdImageTag:                   input.E2EConfig.GetVariable(EtcdVersionUpgradeTo),
-				DNSImageTag:                    input.E2EConfig.GetVariable(CoreDNSVersionUpgradeTo),
-				MachineDeployments:             clusterResources.MachineDeployments,
-				MachinePools:                   clusterResources.MachinePools,
-				KubernetesUpgradeVersion:       input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
-				WaitForMachinesToBeUpgraded:    input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-				WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
-				WaitForKubeProxyUpgrade:        input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-				WaitForDNSUpgrade:              input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-				WaitForEtcdUpgrade:             input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+				ClusterProxy:                input.BootstrapClusterProxy,
+				Cluster:                     clusterResources.Cluster,
+				ControlPlane:                clusterResources.ControlPlane,
+				EtcdImageTag:                input.E2EConfig.GetVariable(EtcdVersionUpgradeTo),
+				DNSImageTag:                 input.E2EConfig.GetVariable(CoreDNSVersionUpgradeTo),
+				MachineDeployments:          clusterResources.MachineDeployments,
+				MachinePools:                clusterResources.MachinePools,
+				KubernetesUpgradeVersion:    input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
+				WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+				WaitForKubeProxyUpgrade:     input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+				WaitForDNSUpgrade:           input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+				WaitForEtcdUpgrade:          input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
 			})
 		} else {
 			// Cluster is not using ClusterClass, upgrade via individual resources.
diff --git a/test/e2e/cluster_upgrade_runtimesdk.go b/test/e2e/cluster_upgrade_runtimesdk.go
index bd82b0f5c..e2d62e5d4 100644
--- a/test/e2e/cluster_upgrade_runtimesdk.go
+++ b/test/e2e/cluster_upgrade_runtimesdk.go
@@ -217,17 +217,16 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
 		// Upgrade the Cluster topology to run through an entire cluster lifecycle to test the lifecycle hooks.
 		By("Upgrading the Cluster topology; creation waits for BeforeClusterUpgradeHook and AfterControlPlaneUpgradeHook to gate the operation")
 		framework.UpgradeClusterTopologyAndWaitForUpgrade(ctx, framework.UpgradeClusterTopologyAndWaitForUpgradeInput{
-			ClusterProxy:                   input.BootstrapClusterProxy,
-			Cluster:                        clusterResources.Cluster,
-			ControlPlane:                   clusterResources.ControlPlane,
-			MachineDeployments:             clusterResources.MachineDeployments,
-			MachinePools:                   clusterResources.MachinePools,
-			KubernetesUpgradeVersion:       input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
-			WaitForMachinesToBeUpgraded:    input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-			WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
-			WaitForKubeProxyUpgrade:        input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-			WaitForDNSUpgrade:              input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-			WaitForEtcdUpgrade:             input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+			ClusterProxy:                input.BootstrapClusterProxy,
+			Cluster:                     clusterResources.Cluster,
+			ControlPlane:                clusterResources.ControlPlane,
+			MachineDeployments:          clusterResources.MachineDeployments,
+			MachinePools:                clusterResources.MachinePools,
+			KubernetesUpgradeVersion:    input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
+			WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+			WaitForKubeProxyUpgrade:     input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+			WaitForDNSUpgrade:           input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
+			WaitForEtcdUpgrade:          input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
 			PreWaitForControlPlaneToBeUpgraded: func() {
 				beforeClusterUpgradeTestHandler(ctx,
 					input.BootstrapClusterProxy.GetClient(),
diff --git a/test/e2e/clusterclass_changes.go b/test/e2e/clusterclass_changes.go
index 3f2e9cd5e..99f0d6c86 100644
--- a/test/e2e/clusterclass_changes.go
+++ b/test/e2e/clusterclass_changes.go
@@ -88,6 +88,24 @@ type ClusterClassChangesSpecInput struct {
 	//   "spec.template.spec.path.to.field": <value>,
 	// }
 	ModifyMachineDeploymentInfrastructureMachineTemplateFields map[string]interface{}
+
+	// ModifyMachinePoolBootstrapConfigTemplateFields are the fields which will be set on the
+	// BootstrapConfigTemplate of all MachinePoolClasses of the ClusterClass after the initial Cluster creation.
+	// The test verifies that these fields are rolled out to the MachinePools.
+	// NOTE: The fields are configured in the following format:
+	// map[string]interface{}{
+	//   "spec.template.spec.path.to.field": <value>,
+	// }
+	ModifyMachinePoolBootstrapConfigTemplateFields map[string]interface{}
+
+	// ModifyMachinePoolInfrastructureMachinePoolTemplateFields are the fields which will be set on the
+	// InfrastructureMachinePoolTemplate of all MachinePoolClasses of the ClusterClass after the initial Cluster creation.
+	// The test verifies that these fields are rolled out to the MachinePools.
+	// NOTE: The fields are configured in the following format:
+	// map[string]interface{}{
+	//   "spec.template.spec.path.to.field": <value>,
+	// }
+	ModifyMachinePoolInfrastructureMachinePoolTemplateFields map[string]interface{}
 }
 
 // ClusterClassChangesSpec implements a test that verifies that ClusterClass changes are rolled out successfully.
@@ -181,6 +199,16 @@ func ClusterClassChangesSpec(ctx context.Context, inputGetter func() ClusterClas
 			WaitForMachineDeployments:                 input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
 		})
 
+		By("Modifying the MachinePool configuration in ClusterClass and wait for changes to be applied to the MachinePool objects")
+		modifyMachinePoolViaClusterClassAndWait(ctx, modifyMachinePoolViaClusterClassAndWaitInput{
+			ClusterProxy:                        input.BootstrapClusterProxy,
+			ClusterClass:                        clusterResources.ClusterClass,
+			Cluster:                             clusterResources.Cluster,
+			ModifyBootstrapConfigTemplateFields: input.ModifyMachinePoolBootstrapConfigTemplateFields,
+			ModifyInfrastructureMachinePoolTemplateFields: input.ModifyMachinePoolInfrastructureMachinePoolTemplateFields,
+			WaitForMachinePools:                           input.E2EConfig.GetIntervals(specName, "wait-machine-pool-nodes"),
+		})
+
 		By("Rebasing the Cluster to a ClusterClass with a modified label for MachineDeployments and wait for changes to be applied to the MachineDeployment objects")
 		rebaseClusterClassAndWait(ctx, rebaseClusterClassAndWaitInput{
 			ClusterProxy:              input.BootstrapClusterProxy,
@@ -535,13 +563,14 @@ func modifyMachinePoolViaClusterClassAndWait(ctx context.Context, input modifyMa
 				}
 
 				// Get the corresponding InfrastructureMachinePoolTemplate.
-				infrastructureMachinePoolTemplateRef := mp.Spec.Template.Spec.InfrastructureRef
-				infrastructureMachinePoolTemplate, err := external.Get(ctx, mgmtClient, &infrastructureMachinePoolTemplateRef, input.Cluster.Namespace)
+				infrastructureMachinePoolRef := mp.Spec.Template.Spec.InfrastructureRef
+				infrastructureMachinePool, err := external.Get(ctx, mgmtClient, &infrastructureMachinePoolRef, input.Cluster.Namespace)
 				g.Expect(err).ToNot(HaveOccurred())
 
 				// Verify that ModifyInfrastructureMachinePoolTemplateFields have been set.
 				for fieldPath, expectedValue := range input.ModifyInfrastructureMachinePoolTemplateFields {
-					currentValue, ok, err := unstructured.NestedFieldNoCopy(infrastructureMachinePoolTemplate.Object, strings.Split(fieldPath, ".")...)
+					fieldPath = strings.TrimPrefix(fieldPath, "spec.template.")
+					currentValue, ok, err := unstructured.NestedFieldNoCopy(infrastructureMachinePool.Object, strings.Split(fieldPath, ".")...)
 					g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get field %q", fieldPath))
 					g.Expect(ok).To(BeTrue(), fmt.Sprintf("failed to get field %q", fieldPath))
 					g.Expect(currentValue).To(Equal(expectedValue), fmt.Sprintf("field %q should be equal", fieldPath))
@@ -619,8 +648,9 @@ func assertMachinePoolTopologyFields(g Gomega, mp expv1.MachinePool, mpTopology
 		g.Expect(mp.Spec.MinReadySeconds).To(Equal(mpTopology.MinReadySeconds))
 	}
 
-	if mpTopology.FailureDomains != nil && mp.Spec.Template.Spec.FailureDomain != nil {
-		g.Expect(mpTopology.FailureDomains).To(ContainElement(mp.Spec.Template.Spec.FailureDomain))
+	if mpTopology.FailureDomains != nil {
+		g.Expect(mp.Spec.FailureDomains).NotTo(BeNil())
+		g.Expect(mpTopology.FailureDomains).To(Equal(mp.Spec.FailureDomains))
 	}
 }
 
diff --git a/test/e2e/clusterclass_changes_test.go b/test/e2e/clusterclass_changes_test.go
index 5d8fbab9a..d4a33c243 100644
--- a/test/e2e/clusterclass_changes_test.go
+++ b/test/e2e/clusterclass_changes_test.go
@@ -63,6 +63,29 @@ var _ = Describe("When testing ClusterClass changes [ClusterClass]", func() {
 					},
 				},
 			},
+			// ModifyMachinePoolBootstrapConfigTemplateFields are the fields which will be set on the
+			// BootstrapConfigTemplate of all MachinePoolClasses of the ClusterClass after the initial Cluster creation.
+			// The test verifies that these fields are rolled out to the MachinePools.
+			ModifyMachinePoolBootstrapConfigTemplateFields: map[string]interface{}{
+				"spec.template.spec.verbosity": int64(4),
+			},
+			// ModifyMachinePoolInfrastructureMachineTemplateFields are the fields which will be set on the
+			// InfrastructureMachinePoolTemplate of all MachinePoolClasses of the ClusterClass after the initial Cluster creation.
+			// The test verifies that these fields are rolled out to the MachinePools.
+			ModifyMachinePoolInfrastructureMachinePoolTemplateFields: map[string]interface{}{
+				"spec.template.spec.template.extraMounts": []interface{}{
+					map[string]interface{}{
+						"containerPath": "/var/run/docker.sock",
+						"hostPath":      "/var/run/docker.sock",
+					},
+					map[string]interface{}{
+						// /tmp cannot be used as containerPath as
+						// it already exists.
+						"containerPath": "/test",
+						"hostPath":      "/tmp",
+					},
+				},
+			},
 		}
 	})
 })
diff --git a/test/e2e/clusterclass_rollout.go b/test/e2e/clusterclass_rollout.go
index 11be32180..f973e6ed9 100644
--- a/test/e2e/clusterclass_rollout.go
+++ b/test/e2e/clusterclass_rollout.go
@@ -47,7 +47,6 @@ import (
 	"sigs.k8s.io/cluster-api/test/framework"
 	"sigs.k8s.io/cluster-api/test/framework/clusterctl"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/labels"
 	"sigs.k8s.io/cluster-api/util/patch"
 )
 
@@ -187,7 +186,7 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 			},
 			WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
 		})
-		By("Modifying the MachinePool configuration via Cluster topology and wait for changes to be applied to the MachinePool (in-place)")
+		By("Modifying the MachinePool configuration via Cluster topology and wait for changes to be applied to the MachinePools (in-place)")
 		modifyMachinePoolViaClusterAndWait(ctx, modifyMachinePoolViaClusterAndWaitInput{
 			ClusterProxy: input.BootstrapClusterProxy,
 			Cluster:      clusterResources.Cluster,
@@ -204,7 +203,7 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 				topology.NodeVolumeDetachTimeout = &metav1.Duration{Duration: time.Duration(rand.Intn(20)) * time.Second} //nolint:gosec
 				topology.MinReadySeconds = pointer.Int32(rand.Int31n(20))                                                 //nolint:gosec
 			},
-			WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
+			WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-nodes"),
 		})
 		By("Verifying there are no unexpected rollouts through in-place rollout")
 		Consistently(func(g Gomega) {
@@ -243,12 +242,10 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 			ModifyBootstrapConfigTemplateFields: map[string]interface{}{
 				"spec.template.spec.verbosity": int64(4),
 			},
-			WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
+			WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-nodes"),
 		})
 		By("Verifying all Machines are replaced through rollout")
 		Eventually(func(g Gomega) {
-			// Note: This excludes MachinePool Machines as they are not replaced by rollout yet.
-			// This is tracked by https://github.com/kubernetes-sigs/cluster-api/issues/8858.
 			machinesAfterUpgrade := getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster)
 			g.Expect(machinesAfterUpgrade.HasAny(machinesBeforeUpgrade.UnsortedList()...)).To(BeFalse(), "All Machines must be replaced through rollout")
 		}, input.E2EConfig.GetIntervals(specName, "wait-control-plane")...).Should(Succeed())
@@ -647,7 +644,7 @@ func assertMachinePools(g Gomega, clusterClassObjects clusterClassObjects, clust
 		g.Expect(
 			union(
 				machinePool.Annotations,
-			).without(g, clusterv1.RevisionAnnotation),
+			),
 		).To(BeEquivalentTo(
 			union(
 				mpTopology.Metadata.Annotations,
@@ -674,68 +671,52 @@ func assertMachinePools(g Gomega, clusterClassObjects clusterClassObjects, clust
 			),
 		))
 
-		// MachinePool InfrastructureMachineTemplate.metadata
-		ccInfrastructureMachineTemplate := clusterClassObjects.InfrastructureMachinePoolTemplateByMachinePoolClass[mpClass.Class]
-		ccInfrastructureMachineTemplateTemplateMetadata := mustMetadata(contract.InfrastructureMachineTemplate().Template().Metadata().Get(ccInfrastructureMachineTemplate))
-		infrastructureMachineTemplate := clusterObjects.InfrastructureMachinePoolTemplateByMachinePool[machinePool.Name]
-		infrastructureMachineTemplateTemplateMetadata := mustMetadata(contract.InfrastructureMachineTemplate().Template().Metadata().Get(infrastructureMachineTemplate))
-		g.Expect(infrastructureMachineTemplate.GetLabels()).To(BeEquivalentTo(
+		// MachinePool InfrastructureMachinePool.metadata
+		ccInfrastructureMachinePoolTemplate := clusterClassObjects.InfrastructureMachinePoolTemplateByMachinePoolClass[mpClass.Class]
+		ccInfrastructureMachinePoolTemplateTemplateMetadata := mustMetadata(contract.InfrastructureMachinePoolTemplate().Template().Metadata().Get(ccInfrastructureMachinePoolTemplate))
+		infrastructureMachinePool := clusterObjects.InfrastructureMachinePoolByMachinePool[machinePool.Name]
+		g.Expect(infrastructureMachinePool.GetLabels()).To(BeEquivalentTo(
 			union(
 				map[string]string{
 					clusterv1.ClusterNameLabel:                    cluster.Name,
 					clusterv1.ClusterTopologyOwnedLabel:           "",
 					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
 				},
-				ccInfrastructureMachineTemplate.GetLabels(),
+				ccInfrastructureMachinePoolTemplateTemplateMetadata.Labels,
 			),
 		))
-		g.Expect(infrastructureMachineTemplate.GetAnnotations()).To(BeEquivalentTo(
+		g.Expect(infrastructureMachinePool.GetAnnotations()).To(BeEquivalentTo(
 			union(
 				map[string]string{
 					clusterv1.TemplateClonedFromGroupKindAnnotation: groupKind(mpClass.Template.Infrastructure.Ref),
 					clusterv1.TemplateClonedFromNameAnnotation:      mpClass.Template.Infrastructure.Ref.Name,
 				},
-				ccInfrastructureMachineTemplate.GetAnnotations(),
-			).without(g, corev1.LastAppliedConfigAnnotation),
-		))
-		// MachinePool InfrastructureMachineTemplate.spec.template.metadata
-		g.Expect(infrastructureMachineTemplateTemplateMetadata.Labels).To(BeEquivalentTo(
-			ccInfrastructureMachineTemplateTemplateMetadata.Labels,
-		))
-		g.Expect(infrastructureMachineTemplateTemplateMetadata.Annotations).To(BeEquivalentTo(
-			ccInfrastructureMachineTemplateTemplateMetadata.Annotations,
+				ccInfrastructureMachinePoolTemplateTemplateMetadata.Annotations,
+			),
 		))
 
-		// MachinePool BootstrapConfigTemplate.metadata
+		// MachinePool BootstrapConfig.metadata
 		ccBootstrapConfigTemplate := clusterClassObjects.BootstrapConfigTemplateByMachinePoolClass[mpClass.Class]
 		ccBootstrapConfigTemplateTemplateMetadata := mustMetadata(contract.BootstrapConfigTemplate().Template().Metadata().Get(ccBootstrapConfigTemplate))
-		bootstrapConfigTemplate := clusterObjects.BootstrapConfigTemplateByMachinePool[machinePool.Name]
-		bootstrapConfigTemplateTemplateMetadata := mustMetadata(contract.BootstrapConfigTemplate().Template().Metadata().Get(bootstrapConfigTemplate))
-		g.Expect(bootstrapConfigTemplate.GetLabels()).To(BeEquivalentTo(
+		bootstrapConfig := clusterObjects.BootstrapConfigByMachinePool[machinePool.Name]
+		g.Expect(bootstrapConfig.GetLabels()).To(BeEquivalentTo(
 			union(
 				map[string]string{
 					clusterv1.ClusterNameLabel:                    cluster.Name,
 					clusterv1.ClusterTopologyOwnedLabel:           "",
 					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
 				},
-				ccBootstrapConfigTemplate.GetLabels(),
+				ccBootstrapConfigTemplateTemplateMetadata.Labels,
 			),
 		))
-		g.Expect(bootstrapConfigTemplate.GetAnnotations()).To(BeEquivalentTo(
+		g.Expect(bootstrapConfig.GetAnnotations()).To(BeEquivalentTo(
 			union(
 				map[string]string{
 					clusterv1.TemplateClonedFromGroupKindAnnotation: groupKind(mpClass.Template.Bootstrap.Ref),
 					clusterv1.TemplateClonedFromNameAnnotation:      mpClass.Template.Bootstrap.Ref.Name,
 				},
-				ccBootstrapConfigTemplate.GetAnnotations(),
-			).without(g, corev1.LastAppliedConfigAnnotation),
-		))
-		// MachinePool BootstrapConfigTemplate.spec.template.metadata
-		g.Expect(bootstrapConfigTemplateTemplateMetadata.Labels).To(BeEquivalentTo(
-			ccBootstrapConfigTemplateTemplateMetadata.Labels,
-		))
-		g.Expect(bootstrapConfigTemplateTemplateMetadata.Annotations).To(BeEquivalentTo(
-			ccBootstrapConfigTemplateTemplateMetadata.Annotations,
+				ccBootstrapConfigTemplateTemplateMetadata.Annotations,
+			),
 		))
 	}
 }
@@ -901,7 +882,6 @@ func mustMetadata(metadata *clusterv1.ObjectMeta, err error) *clusterv1.ObjectMe
 }
 
 // getMachinesByCluster gets the Machines of a Cluster and returns them as a Set of Machine names.
-// Note: This excludes MachinePool Machines as they are not replaced by rollout yet.
 func getMachinesByCluster(ctx context.Context, client client.Client, cluster *clusterv1.Cluster) sets.Set[string] {
 	machines := sets.Set[string]{}
 	machinesByCluster := framework.GetMachinesByCluster(ctx, framework.GetMachinesByClusterInput{
@@ -909,11 +889,8 @@ func getMachinesByCluster(ctx context.Context, client client.Client, cluster *cl
 		ClusterName: cluster.Name,
 		Namespace:   cluster.Namespace,
 	})
-	for i := range machinesByCluster {
-		m := machinesByCluster[i]
-		if !labels.IsMachinePoolOwned(&m) {
-			machines.Insert(m.Name)
-		}
+	for _, m := range machinesByCluster {
+		machines.Insert(m.Name)
 	}
 	return machines
 }
@@ -942,7 +919,7 @@ func getMDTopology(cluster *clusterv1.Cluster, md *clusterv1.MachineDeployment)
 	return nil
 }
 
-// getMPClass looks up the MachinePoolClass for a MachinePool in the ClusterClass.
+// getMPClass looks up the MachinePoolClass for a md in the ClusterClass.
 func getMPClass(cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass, mp *expv1.MachinePool) *clusterv1.MachinePoolClass {
 	mpTopology := getMPTopology(cluster, mp)
 
@@ -1047,6 +1024,8 @@ func getClusterClassObjects(ctx context.Context, g Gomega, clusterProxy framewor
 	res := clusterClassObjects{
 		InfrastructureMachineTemplateByMachineDeploymentClass: map[string]*unstructured.Unstructured{},
 		BootstrapConfigTemplateByMachineDeploymentClass:       map[string]*unstructured.Unstructured{},
+		InfrastructureMachinePoolTemplateByMachinePoolClass:   map[string]*unstructured.Unstructured{},
+		BootstrapConfigTemplateByMachinePoolClass:             map[string]*unstructured.Unstructured{},
 	}
 	var err error
 
@@ -1069,6 +1048,16 @@ func getClusterClassObjects(ctx context.Context, g Gomega, clusterProxy framewor
 		res.BootstrapConfigTemplateByMachineDeploymentClass[mdClass.Class] = bootstrapConfigTemplate
 	}
 
+	for _, mpClass := range clusterClass.Spec.Workers.MachinePools {
+		infrastructureMachinePoolTemplate, err := external.Get(ctx, mgmtClient, mpClass.Template.Infrastructure.Ref, clusterClass.Namespace)
+		g.Expect(err).ToNot(HaveOccurred())
+		res.InfrastructureMachinePoolTemplateByMachinePoolClass[mpClass.Class] = infrastructureMachinePoolTemplate
+
+		bootstrapConfigTemplate, err := external.Get(ctx, mgmtClient, mpClass.Template.Bootstrap.Ref, clusterClass.Namespace)
+		g.Expect(err).ToNot(HaveOccurred())
+		res.BootstrapConfigTemplateByMachinePoolClass[mpClass.Class] = bootstrapConfigTemplate
+	}
+
 	return res
 }
 
@@ -1089,8 +1078,8 @@ type clusterObjects struct {
 	InfrastructureMachineTemplateByMachineDeployment map[string]*unstructured.Unstructured
 	BootstrapConfigTemplateByMachineDeployment       map[string]*unstructured.Unstructured
 
-	InfrastructureMachinePoolTemplateByMachinePool map[string]*unstructured.Unstructured
-	BootstrapConfigTemplateByMachinePool           map[string]*unstructured.Unstructured
+	InfrastructureMachinePoolByMachinePool map[string]*unstructured.Unstructured
+	BootstrapConfigByMachinePool           map[string]*unstructured.Unstructured
 
 	InfrastructureMachineByMachine map[string]*unstructured.Unstructured
 	BootstrapConfigByMachine       map[string]*unstructured.Unstructured
@@ -1109,6 +1098,8 @@ func getClusterObjects(ctx context.Context, g Gomega, clusterProxy framework.Clu
 		InfrastructureMachineTemplateByMachineDeployment: map[string]*unstructured.Unstructured{},
 		BootstrapConfigByMachine:                         map[string]*unstructured.Unstructured{},
 		InfrastructureMachineByMachine:                   map[string]*unstructured.Unstructured{},
+		BootstrapConfigByMachinePool:                     map[string]*unstructured.Unstructured{},
+		InfrastructureMachinePoolByMachinePool:           map[string]*unstructured.Unstructured{},
 	}
 	var err error
 
@@ -1177,6 +1168,26 @@ func getClusterObjects(ctx context.Context, g Gomega, clusterProxy framework.Clu
 		}
 	}
 
+	// MachinePools.
+	for _, mpTopology := range cluster.Spec.Topology.Workers.MachinePools {
+		// Get MachinePool for the current MachinePoolTopology.
+		mpList := &expv1.MachinePoolList{}
+		g.Expect(mgmtClient.List(ctx, mpList, client.InNamespace(cluster.Namespace), client.MatchingLabels{
+			clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
+		})).To(Succeed())
+		g.Expect(mpList.Items).To(HaveLen(1), fmt.Sprintf("expected one MachinePool for topology %q, but got %d", mpTopology.Name, len(mpList.Items)))
+		mp := mpList.Items[0]
+		res.MachinePools = append(res.MachinePools, &mp)
+
+		bootstrapConfig, err := external.Get(ctx, mgmtClient, mp.Spec.Template.Spec.Bootstrap.ConfigRef, cluster.Namespace)
+		g.Expect(err).ToNot(HaveOccurred())
+		res.BootstrapConfigByMachinePool[mp.Name] = bootstrapConfig
+
+		infrastructureMachinePool, err := external.Get(ctx, mgmtClient, &mp.Spec.Template.Spec.InfrastructureRef, cluster.Namespace)
+		g.Expect(err).ToNot(HaveOccurred())
+		res.InfrastructureMachinePoolByMachinePool[mp.Name] = infrastructureMachinePool
+	}
+
 	return res
 }
 
diff --git a/test/framework/ownerreference_helpers.go b/test/framework/ownerreference_helpers.go
index 4b6b4ae03..5b5d78861 100644
--- a/test/framework/ownerreference_helpers.go
+++ b/test/framework/ownerreference_helpers.go
@@ -39,7 +39,6 @@ import (
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
 	addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
-	infraexpv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util/patch"
 )
 
@@ -289,8 +288,6 @@ var (
 	dockerMachinePoolTemplateKind = "DockerMachinePoolTemplate"
 	dockerClusterKind             = "DockerCluster"
 	dockerClusterTemplateKind     = "DockerClusterTemplate"
-
-	dockerMachinePoolController = metav1.OwnerReference{Kind: dockerMachinePoolKind, APIVersion: infraexpv1.GroupVersion.String()}
 )
 
 // DockerInfraOwnerReferenceAssertions maps Docker Infrastructure types to functions which return an error if the passed
@@ -299,9 +296,8 @@ var (
 // That document should be updated if these references change.
 var DockerInfraOwnerReferenceAssertions = map[string]func([]metav1.OwnerReference) error{
 	dockerMachineKind: func(owners []metav1.OwnerReference) error {
-		// The DockerMachine must be owned and controlled by a Machine or a DockerMachinePool.
-		return HasOneOfExactOwners(owners, []metav1.OwnerReference{machineController}, []metav1.OwnerReference{machineController, dockerMachinePoolController})
-
+		// The DockerMachine must be owned and controlled by a Machine.
+		return HasExactOwners(owners, machineController)
 	},
 	dockerMachineTemplateKind: func(owners []metav1.OwnerReference) error {
 		// Base DockerMachineTemplates referenced in a ClusterClass must be owned by the ClusterClass.
diff --git a/test/go.mod b/test/go.mod
index 320e9d4de..582ab5095 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -23,12 +23,12 @@ require (
 	go.etcd.io/etcd/client/v3 v3.5.10
 	golang.org/x/net v0.18.0
 	google.golang.org/grpc v1.59.0
-	k8s.io/api v0.28.4
-	k8s.io/apiextensions-apiserver v0.28.4
-	k8s.io/apimachinery v0.28.4
-	k8s.io/apiserver v0.28.4
-	k8s.io/client-go v0.28.4
-	k8s.io/component-base v0.28.4
+	k8s.io/api v0.28.3
+	k8s.io/apiextensions-apiserver v0.28.3
+	k8s.io/apimachinery v0.28.3
+	k8s.io/apiserver v0.28.3
+	k8s.io/client-go v0.28.3
+	k8s.io/component-base v0.28.3
 	k8s.io/klog/v2 v2.100.1
 	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
 	sigs.k8s.io/cluster-api v0.0.0-00010101000000-000000000000
@@ -154,8 +154,8 @@ require (
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gotest.tools/v3 v3.4.0 // indirect
-	k8s.io/cluster-bootstrap v0.28.4 // indirect
-	k8s.io/kms v0.28.4 // indirect
+	k8s.io/cluster-bootstrap v0.28.3 // indirect
+	k8s.io/kms v0.28.3 // indirect
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
diff --git a/test/go.sum b/test/go.sum
index 88a2ba2a8..8d086c16a 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -838,24 +838,24 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
-k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
-k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU=
-k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM=
-k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
-k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
-k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg=
-k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w=
-k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY=
-k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4=
-k8s.io/cluster-bootstrap v0.28.4 h1:4MKNy1Qd9QY7pl47rSMGIORF+tm3CUaqC1M8U9bjn4Q=
-k8s.io/cluster-bootstrap v0.28.4/go.mod h1:/c4ro/R4yf4EtJgFgFtvnHkbDOHwubeKJXh5R1c89Bc=
-k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo=
-k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
+k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w=
+k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
+k8s.io/cluster-bootstrap v0.28.3 h1:hGK3mJsmVGGvRJ61nyQcYNR9g/IYax75TbJcylTmZts=
+k8s.io/cluster-bootstrap v0.28.3/go.mod h1:s1B3FTw713b9iw67yGFiVF3zCfw5obrZXWl3EMelvdg=
+k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
+k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kms v0.28.4 h1:PMgY/3CQTWP9eIKmNQiTgjLIZ0ns6O+voagzD2/4mSg=
-k8s.io/kms v0.28.4/go.mod h1:HL4/lR/bhjAJPbqycKtfhWiKh1Sp21cpHOL8P4oo87w=
+k8s.io/kms v0.28.3 h1:jYwwAe96XELNjYWv1G4kNzizcFoZ50OOElvPansbw70=
+k8s.io/kms v0.28.3/go.mod h1:kSMjU2tg7vjqqoWVVCcmPmNZ/CofPsoTbSxAipCvZuE=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
diff --git a/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachinepools.yaml b/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachinepools.yaml
index d1f1d93a4..9e1fc5273 100644
--- a/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachinepools.yaml
+++ b/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachinepools.yaml
@@ -322,10 +322,6 @@ spec:
                   - type
                   type: object
                 type: array
-              infrastructureMachineKind:
-                description: InfrastructureMachineKind is the kind of the infrastructure
-                  resources behind MachinePool Machines.
-                type: string
               instances:
                 description: Instances contains the status for each instance in the
                   pool
diff --git a/test/infrastructure/docker/config/rbac/role.yaml b/test/infrastructure/docker/config/rbac/role.yaml
index 546705e9a..aa34b0a59 100644
--- a/test/infrastructure/docker/config/rbac/role.yaml
+++ b/test/infrastructure/docker/config/rbac/role.yaml
@@ -51,15 +51,6 @@ rules:
   - get
   - list
   - watch
-- apiGroups:
-  - cluster.x-k8s.io
-  resources:
-  - machines
-  verbs:
-  - delete
-  - get
-  - list
-  - watch
 - apiGroups:
   - infrastructure.cluster.x-k8s.io
   resources:
diff --git a/test/infrastructure/docker/exp/api/v1alpha4/conversion.go b/test/infrastructure/docker/exp/api/v1alpha4/conversion.go
index 68b10e18b..acc200d9e 100644
--- a/test/infrastructure/docker/exp/api/v1alpha4/conversion.go
+++ b/test/infrastructure/docker/exp/api/v1alpha4/conversion.go
@@ -17,40 +17,21 @@ limitations under the License.
 package v1alpha4
 
 import (
-	apiconversion "k8s.io/apimachinery/pkg/conversion"
 	"sigs.k8s.io/controller-runtime/pkg/conversion"
 
 	infraexpv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/api/v1beta1"
-	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 )
 
 func (src *DockerMachinePool) ConvertTo(dstRaw conversion.Hub) error {
 	dst := dstRaw.(*infraexpv1.DockerMachinePool)
 
-	if err := Convert_v1alpha4_DockerMachinePool_To_v1beta1_DockerMachinePool(src, dst, nil); err != nil {
-		return err
-	}
-
-	// Manually restore data.
-	restored := &infraexpv1.DockerMachinePool{}
-	if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
-		return err
-	}
-
-	dst.Status.InfrastructureMachineKind = restored.Status.InfrastructureMachineKind
-
-	return nil
+	return Convert_v1alpha4_DockerMachinePool_To_v1beta1_DockerMachinePool(src, dst, nil)
 }
 
 func (dst *DockerMachinePool) ConvertFrom(srcRaw conversion.Hub) error {
 	src := srcRaw.(*infraexpv1.DockerMachinePool)
 
-	if err := Convert_v1beta1_DockerMachinePool_To_v1alpha4_DockerMachinePool(src, dst, nil); err != nil {
-		return err
-	}
-
-	// Preserve Hub data on down-conversion except for metadata
-	return utilconversion.MarshalData(src, dst)
+	return Convert_v1beta1_DockerMachinePool_To_v1alpha4_DockerMachinePool(src, dst, nil)
 }
 
 func (src *DockerMachinePoolList) ConvertTo(dstRaw conversion.Hub) error {
@@ -64,8 +45,3 @@ func (dst *DockerMachinePoolList) ConvertFrom(srcRaw conversion.Hub) error {
 
 	return Convert_v1beta1_DockerMachinePoolList_To_v1alpha4_DockerMachinePoolList(src, dst, nil)
 }
-
-func Convert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus(in *infraexpv1.DockerMachinePoolStatus, out *DockerMachinePoolStatus, s apiconversion.Scope) error {
-	// NOTE: custom conversion func is required because Status.InfrastructureMachineKind has been added in v1beta1.
-	return autoConvert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus(in, out, s)
-}
diff --git a/test/infrastructure/docker/exp/api/v1alpha4/zz_generated.conversion.go b/test/infrastructure/docker/exp/api/v1alpha4/zz_generated.conversion.go
index 53081f3c8..4f3020889 100644
--- a/test/infrastructure/docker/exp/api/v1alpha4/zz_generated.conversion.go
+++ b/test/infrastructure/docker/exp/api/v1alpha4/zz_generated.conversion.go
@@ -95,7 +95,7 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
-	if err := s.AddConversionFunc((*v1beta1.DockerMachinePoolStatus)(nil), (*DockerMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+	if err := s.AddGeneratedConversionFunc((*v1beta1.DockerMachinePoolStatus)(nil), (*DockerMachinePoolStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus(a.(*v1beta1.DockerMachinePoolStatus), b.(*DockerMachinePoolStatus), scope)
 	}); err != nil {
 		return err
@@ -339,6 +339,10 @@ func autoConvert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolSt
 	} else {
 		out.Conditions = nil
 	}
-	// WARNING: in.InfrastructureMachineKind requires manual conversion: does not exist in peer-type
 	return nil
 }
+
+// Convert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus is an autogenerated conversion function.
+func Convert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus(in *v1beta1.DockerMachinePoolStatus, out *DockerMachinePoolStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_DockerMachinePoolStatus_To_v1alpha4_DockerMachinePoolStatus(in, out, s)
+}
diff --git a/test/infrastructure/docker/exp/api/v1beta1/dockermachinepool_types.go b/test/infrastructure/docker/exp/api/v1beta1/dockermachinepool_types.go
index e90d7d585..134939449 100644
--- a/test/infrastructure/docker/exp/api/v1beta1/dockermachinepool_types.go
+++ b/test/infrastructure/docker/exp/api/v1beta1/dockermachinepool_types.go
@@ -82,10 +82,6 @@ type DockerMachinePoolStatus struct {
 	// Conditions defines current service state of the DockerMachinePool.
 	// +optional
 	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
-
-	// InfrastructureMachineKind is the kind of the infrastructure resources behind MachinePool Machines.
-	// +optional
-	InfrastructureMachineKind string `json:"infrastructureMachineKind,omitempty"`
 }
 
 // DockerMachinePoolInstanceStatus contains status information about a DockerMachinePool.
@@ -134,13 +130,13 @@ type DockerMachinePool struct {
 }
 
 // GetConditions returns the set of conditions for this object.
-func (d *DockerMachinePool) GetConditions() clusterv1.Conditions {
-	return d.Status.Conditions
+func (c *DockerMachinePool) GetConditions() clusterv1.Conditions {
+	return c.Status.Conditions
 }
 
 // SetConditions sets the conditions on this object.
-func (d *DockerMachinePool) SetConditions(conditions clusterv1.Conditions) {
-	d.Status.Conditions = conditions
+func (c *DockerMachinePool) SetConditions(conditions clusterv1.Conditions) {
+	c.Status.Conditions = conditions
 }
 
 // +kubebuilder:object:root=true
diff --git a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go b/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
index e23f46b44..67f997ea8 100644
--- a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
+++ b/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller.go
@@ -25,10 +25,6 @@ import (
 	"github.com/pkg/errors"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
-	kerrors "k8s.io/apimachinery/pkg/util/errors"
-	"k8s.io/client-go/tools/record"
 	"k8s.io/klog/v2"
 	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -39,28 +35,17 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
-	"sigs.k8s.io/cluster-api/controllers/external"
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	utilexp "sigs.k8s.io/cluster-api/exp/util"
 	"sigs.k8s.io/cluster-api/test/infrastructure/container"
-	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
 	infraexpv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/api/v1beta1"
-	"sigs.k8s.io/cluster-api/test/infrastructure/docker/internal/docker"
+	"sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/internal/docker"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
 
-const (
-	// dockerMachinePoolLabel is the label used to identify the DockerMachinePool that is responsible for a Docker container.
-	dockerMachinePoolLabel = "docker.cluster.x-k8s.io/machine-pool"
-
-	// requeueAfter is how long to wait before checking again to see if the DockerMachines are still provisioning or deleting.
-	requeueAfter = 10 * time.Second
-)
-
 // DockerMachinePoolReconciler reconciles a DockerMachinePool object.
 type DockerMachinePoolReconciler struct {
 	Client           client.Client
@@ -70,15 +55,11 @@ type DockerMachinePoolReconciler struct {
 
 	// WatchFilterValue is the label value used to filter events prior to reconciliation.
 	WatchFilterValue string
-
-	recorder        record.EventRecorder
-	externalTracker external.ObjectTracker
 }
 
 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=dockermachinepools,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=dockermachinepools/status;dockermachinepools/finalizers,verbs=get;update;patch
 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools;machinepools/status,verbs=get;list;watch
-// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines,verbs=get;list;watch;delete
 // +kubebuilder:rbac:groups="",resources=secrets;,verbs=get;list;watch
 
 func (r *DockerMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, rerr error) {
@@ -140,15 +121,13 @@ func (r *DockerMachinePoolReconciler) Reconcile(ctx context.Context, req ctrl.Re
 
 	// Handle deleted machines
 	if !dockerMachinePool.ObjectMeta.DeletionTimestamp.IsZero() {
-		return r.reconcileDelete(ctx, cluster, machinePool, dockerMachinePool)
+		return ctrl.Result{}, r.reconcileDelete(ctx, cluster, machinePool, dockerMachinePool)
 	}
 
-	// Add finalizer and the InfrastructureMachineKind if they aren't already present, and requeue if either were added.
-	// We want to add the finalizer here to avoid the race condition between init and delete.
+	// Add finalizer first if not set to avoid the race condition between init and delete.
 	// Note: Finalizers in general can only be added when the deletionTimestamp is not set.
-	needsPatch := controllerutil.AddFinalizer(dockerMachinePool, infraexpv1.MachinePoolFinalizer)
-	needsPatch = setInfrastructureMachineKind(dockerMachinePool) || needsPatch
-	if needsPatch {
+	if !controllerutil.ContainsFinalizer(dockerMachinePool, infraexpv1.MachinePoolFinalizer) {
+		controllerutil.AddFinalizer(dockerMachinePool, infraexpv1.MachinePoolFinalizer)
 		return ctrl.Result{}, nil
 	}
 
@@ -170,7 +149,7 @@ func (r *DockerMachinePoolReconciler) SetupWithManager(ctx context.Context, mgr
 		return err
 	}
 
-	c, err := ctrl.NewControllerManagedBy(mgr).
+	err = ctrl.NewControllerManagedBy(mgr).
 		For(&infraexpv1.DockerMachinePool{}).
 		WithOptions(options).
 		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
@@ -179,85 +158,31 @@ func (r *DockerMachinePoolReconciler) SetupWithManager(ctx context.Context, mgr
 			handler.EnqueueRequestsFromMapFunc(utilexp.MachinePoolToInfrastructureMapFunc(
 				infraexpv1.GroupVersion.WithKind("DockerMachinePool"), ctrl.LoggerFrom(ctx))),
 		).
-		Watches(
-			&infrav1.DockerMachine{},
-			handler.EnqueueRequestsFromMapFunc(dockerMachineToDockerMachinePool),
-		).
 		Watches(
 			&clusterv1.Cluster{},
 			handler.EnqueueRequestsFromMapFunc(clusterToDockerMachinePools),
 			builder.WithPredicates(
 				predicates.ClusterUnpausedAndInfrastructureReady(ctrl.LoggerFrom(ctx)),
 			),
-		).Build(r)
+		).Complete(r)
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
-
-	r.recorder = mgr.GetEventRecorderFor("dockermachinepool-controller")
-	r.externalTracker = external.ObjectTracker{
-		Controller: c,
-		Cache:      mgr.GetCache(),
-	}
-
 	return nil
 }
 
-func (r *DockerMachinePoolReconciler) reconcileDelete(ctx context.Context, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
-
-	dockerMachineList, err := getDockerMachines(ctx, r.Client, *cluster, *machinePool, *dockerMachinePool)
-	if err != nil {
-		return ctrl.Result{}, err
-	}
-
-	if len(dockerMachineList.Items) > 0 {
-		log.Info("DockerMachinePool still has dependent DockerMachines, deleting them first and requeuing", "count", len(dockerMachineList.Items))
-
-		var errs []error
-
-		for _, dockerMachine := range dockerMachineList.Items {
-			if !dockerMachine.GetDeletionTimestamp().IsZero() {
-				// Don't handle deleted child
-				continue
-			}
-
-			if err := r.deleteMachinePoolMachine(ctx, dockerMachine); err != nil {
-				err = errors.Wrapf(err, "error deleting DockerMachinePool %s/%s: failed to delete %s %s", dockerMachinePool.Namespace, dockerMachinePool.Name, dockerMachine.Namespace, dockerMachine.Name)
-				errs = append(errs, err)
-			}
-		}
-
-		if len(errs) > 0 {
-			return ctrl.Result{}, kerrors.NewAggregate(errs)
-		}
-
-		return ctrl.Result{RequeueAfter: requeueAfter}, nil
-	}
-
-	// Once there are no DockerMachines left, ensure there are no Docker containers left behind.
-	// This can occur if deletion began after containers were created but before the DockerMachines were created, or if creation of a DockerMachine failed.
-	log.Info("DockerMachines have been deleted, deleting any remaining Docker containers")
-
-	labelFilters := map[string]string{dockerMachinePoolLabel: dockerMachinePool.Name}
-	// List Docker containers, i.e. external machines in the cluster.
-	externalMachines, err := docker.ListMachinesByCluster(ctx, cluster, labelFilters)
+func (r *DockerMachinePoolReconciler) reconcileDelete(ctx context.Context, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) error {
+	pool, err := docker.NewNodePool(ctx, r.Client, cluster, machinePool, dockerMachinePool)
 	if err != nil {
-		return ctrl.Result{}, errors.Wrapf(err, "failed to list all machines in the cluster with label \"%s:%s\"", dockerMachinePoolLabel, dockerMachinePool.Name)
+		return errors.Wrap(err, "failed to build new node pool")
 	}
 
-	// Providers should similarly ensure that all infrastructure instances are deleted even if the InfraMachine has not been created yet.
-	for _, externalMachine := range externalMachines {
-		log.Info("Deleting Docker container", "container", externalMachine.Name())
-		if err := externalMachine.Delete(ctx); err != nil {
-			return ctrl.Result{}, errors.Wrapf(err, "failed to delete machine %s", externalMachine.Name())
-		}
+	if err := pool.Delete(ctx); err != nil {
+		return errors.Wrap(err, "failed to delete all machines in the node pool")
 	}
 
-	// Once all DockerMachines and Docker containers are deleted, remove the finalizer.
 	controllerutil.RemoveFinalizer(dockerMachinePool, infraexpv1.MachinePoolFinalizer)
-
-	return ctrl.Result{}, nil
+	return nil
 }
 
 func (r *DockerMachinePoolReconciler) reconcileNormal(ctx context.Context, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) (ctrl.Result, error) {
@@ -273,36 +198,30 @@ func (r *DockerMachinePoolReconciler) reconcileNormal(ctx context.Context, clust
 		machinePool.Spec.Replicas = pointer.Int32(1)
 	}
 
-	// First, reconcile the Docker containers, but do not delete any as we need to delete the Machine to ensure node cordon/drain.
-	// Similarly, providers implementing MachinePool Machines will need to reconcile their analogous infrastructure instances (aside
-	// from deletion) before reconciling InfraMachinePoolMachines.
-	if err := r.reconcileDockerContainers(ctx, cluster, machinePool, dockerMachinePool); err != nil {
-		return ctrl.Result{}, err
+	pool, err := docker.NewNodePool(ctx, r.Client, cluster, machinePool, dockerMachinePool)
+	if err != nil {
+		return ctrl.Result{}, errors.Wrap(err, "failed to build new node pool")
 	}
 
-	// Second, once the Docker containers are created, reconcile the DockerMachines. This function creates a DockerMachine for each newly created Docker
-	// container, and handles container deletion. Instead of deleting an infrastructure instance directly, we want to delete the owner Machine. This will
-	// trigger a cordon and drain of the node, as well as trigger the deletion of the DockerMachine, which in turn causes the Docker container to be deleted.
-	// Similarly, providers will need to create InfraMachines for each instance, and instead of deleting instances directly, delete the owner Machine.
-	if err := r.reconcileDockerMachines(ctx, cluster, machinePool, dockerMachinePool); err != nil {
-		return ctrl.Result{}, err
+	// Reconcile machines and updates Status.Instances
+	remoteClient, err := r.Tracker.GetClient(ctx, client.ObjectKeyFromObject(cluster))
+	if err != nil {
+		return ctrl.Result{}, errors.Wrap(err, "failed to generate workload cluster client")
 	}
-
-	// Fetch the list of DockerMachines to ensure the provider IDs are up to date.
-	dockerMachineList, err := getDockerMachines(ctx, r.Client, *cluster, *machinePool, *dockerMachinePool)
+	res, err := pool.ReconcileMachines(ctx, remoteClient)
 	if err != nil {
-		return ctrl.Result{}, err
+		return res, err
 	}
 
-	// Derive providerIDList from the provider ID on each DockerMachine if it exists. The providerID is set by the DockerMachine controller.
+	// Derive info from Status.Instances
 	dockerMachinePool.Spec.ProviderIDList = []string{}
-	for _, dockerMachine := range dockerMachineList.Items {
-		if dockerMachine.Spec.ProviderID != nil {
-			dockerMachinePool.Spec.ProviderIDList = append(dockerMachinePool.Spec.ProviderIDList, *dockerMachine.Spec.ProviderID)
+	for _, instance := range dockerMachinePool.Status.Instances {
+		if instance.ProviderID != nil && instance.Ready {
+			dockerMachinePool.Spec.ProviderIDList = append(dockerMachinePool.Spec.ProviderIDList, *instance.ProviderID)
 		}
 	}
 
-	dockerMachinePool.Status.Replicas = int32(len(dockerMachineList.Items))
+	dockerMachinePool.Status.Replicas = int32(len(dockerMachinePool.Status.Instances))
 
 	if dockerMachinePool.Spec.ProviderID == "" {
 		// This is a fake provider ID which does not tie back to any docker infrastructure. In cloud providers,
@@ -311,89 +230,25 @@ func (r *DockerMachinePoolReconciler) reconcileNormal(ctx context.Context, clust
 		dockerMachinePool.Spec.ProviderID = getDockerMachinePoolProviderID(cluster.Name, dockerMachinePool.Name)
 	}
 
-	if len(dockerMachinePool.Spec.ProviderIDList) == int(*machinePool.Spec.Replicas) && len(dockerMachineList.Items) == int(*machinePool.Spec.Replicas) {
-		dockerMachinePool.Status.Ready = true
-		conditions.MarkTrue(dockerMachinePool, expv1.ReplicasReadyCondition)
-
-		return ctrl.Result{}, nil
-	}
-
-	dockerMachinePool.Status.Ready = false
-	conditions.MarkFalse(dockerMachinePool, expv1.ReplicasReadyCondition, expv1.WaitingForReplicasReadyReason, clusterv1.ConditionSeverityInfo, "")
+	dockerMachinePool.Status.Ready = len(dockerMachinePool.Spec.ProviderIDList) == int(*machinePool.Spec.Replicas)
 
 	// if some machine is still provisioning, force reconcile in few seconds to check again infrastructure.
-	return ctrl.Result{RequeueAfter: requeueAfter}, nil
-}
-
-func getDockerMachines(ctx context.Context, c client.Client, cluster clusterv1.Cluster, machinePool expv1.MachinePool, dockerMachinePool infraexpv1.DockerMachinePool) (*infrav1.DockerMachineList, error) {
-	dockerMachineList := &infrav1.DockerMachineList{}
-	labels := map[string]string{
-		clusterv1.ClusterNameLabel:     cluster.Name,
-		clusterv1.MachinePoolNameLabel: machinePool.Name,
+	if !dockerMachinePool.Status.Ready && res.IsZero() {
+		return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
 	}
-	if err := c.List(ctx, dockerMachineList, client.InNamespace(dockerMachinePool.Namespace), client.MatchingLabels(labels)); err != nil {
-		return nil, err
-	}
-
-	return dockerMachineList, nil
+	return res, nil
 }
 
 func getDockerMachinePoolProviderID(clusterName, dockerMachinePoolName string) string {
 	return fmt.Sprintf("docker:////%s-dmp-%s", clusterName, dockerMachinePoolName)
 }
 
-// setInfrastructureMachineKind sets the infrastructure machine kind in the status if it is not set already to support
-// MachinePool Machines and returns a boolean indicating if the status was updated.
-func setInfrastructureMachineKind(dockerMachinePool *infraexpv1.DockerMachinePool) bool {
-	if dockerMachinePool != nil && dockerMachinePool.Status.InfrastructureMachineKind != "DockerMachine" {
-		dockerMachinePool.Status.InfrastructureMachineKind = "DockerMachine"
-		return true
-	}
-
-	return false
-}
-
-// dockerMachineToDockerMachinePool creates a mapping handler to transform DockerMachine to DockerMachinePools.
-func dockerMachineToDockerMachinePool(_ context.Context, o client.Object) []ctrl.Request {
-	dockerMachine, ok := o.(*infrav1.DockerMachine)
-	if !ok {
-		panic(fmt.Sprintf("Expected a DockerMachine but got a %T", o))
-	}
-
-	for _, ownerRef := range dockerMachine.GetOwnerReferences() {
-		gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
-		if err != nil {
-			return nil
-		}
-		if ownerRef.Kind == "DockerMachinePool" && gv.Group == infraexpv1.GroupVersion.Group {
-			return []ctrl.Request{
-				{
-					NamespacedName: types.NamespacedName{
-						Name:      ownerRef.Name,
-						Namespace: dockerMachine.Namespace,
-					},
-				},
-			}
-		}
-	}
-
-	return nil
-}
-
 func patchDockerMachinePool(ctx context.Context, patchHelper *patch.Helper, dockerMachinePool *infraexpv1.DockerMachinePool) error {
-	conditions.SetSummary(dockerMachinePool,
-		conditions.WithConditions(
-			expv1.ReplicasReadyCondition,
-		),
-	)
+	// TODO: add conditions
 
 	// Patch the object, ignoring conflicts on the conditions owned by this controller.
 	return patchHelper.Patch(
 		ctx,
 		dockerMachinePool,
-		patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
-			clusterv1.ReadyCondition,
-			expv1.ReplicasReadyCondition,
-		}},
 	)
 }
diff --git a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller_phases.go b/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller_phases.go
deleted file mode 100644
index 1a7201c53..000000000
--- a/test/infrastructure/docker/exp/internal/controllers/dockermachinepool_controller_phases.go
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
-Copyright 2023 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Package controllers implements controller functionality.
-package controllers
-
-import (
-	"context"
-	"fmt"
-	"math/rand"
-	"sort"
-	"strings"
-
-	"github.com/blang/semver/v4"
-	"github.com/pkg/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/klog/v2"
-	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/kind/pkg/cluster/constants"
-
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
-	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
-	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
-	infraexpv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/api/v1beta1"
-	"sigs.k8s.io/cluster-api/test/infrastructure/docker/internal/docker"
-	"sigs.k8s.io/cluster-api/test/infrastructure/kind"
-	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/cluster-api/util/labels/format"
-)
-
-// reconcileDockerContainers manages the Docker containers for a MachinePool such that it
-// - Ensures the number of up-to-date Docker containers is equal to the MachinePool's desired replica count.
-// - Does not delete any containers as that must be triggered in reconcileDockerMachines to ensure node cordon/drain.
-//
-// Providers should similarly create their infrastructure instances and reconcile any additional logic.
-func (r *DockerMachinePoolReconciler) reconcileDockerContainers(ctx context.Context, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) error {
-	log := ctrl.LoggerFrom(ctx)
-
-	log.V(2).Info("Reconciling Docker containers", "dockerMachinePool", dockerMachinePool.Name, "namespace", dockerMachinePool.Namespace)
-
-	labelFilters := map[string]string{dockerMachinePoolLabel: dockerMachinePool.Name}
-
-	machines, err := docker.ListMachinesByCluster(ctx, cluster, labelFilters)
-	if err != nil {
-		return errors.Wrapf(err, "failed to list all machines in the cluster")
-	}
-
-	matchingMachineCount := len(machinesMatchingInfrastructureSpec(ctx, machines, machinePool, dockerMachinePool))
-	numToCreate := int(*machinePool.Spec.Replicas) - matchingMachineCount
-	for i := 0; i < numToCreate; i++ {
-		log.V(2).Info("Creating a new Docker container for machinePool", "machinePool", machinePool.Name)
-		name := fmt.Sprintf("worker-%s", util.RandomString(6))
-		if err := createDockerContainer(ctx, name, cluster, machinePool, dockerMachinePool); err != nil {
-			return errors.Wrap(err, "failed to create a new docker machine")
-		}
-	}
-
-	return nil
-}
-
-// createDockerContainer creates a Docker container to serve as a replica for the MachinePool.
-func createDockerContainer(ctx context.Context, name string, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) error {
-	log := ctrl.LoggerFrom(ctx)
-	labelFilters := map[string]string{dockerMachinePoolLabel: dockerMachinePool.Name}
-	externalMachine, err := docker.NewMachine(ctx, cluster, name, labelFilters)
-	if err != nil {
-		return errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", name)
-	}
-
-	// NOTE: FailureDomains don't mean much in CAPD since it's all local, but we are setting a label on
-	// each container, so we can check placement.
-	labels := map[string]string{}
-	for k, v := range labelFilters {
-		labels[k] = v
-	}
-
-	if len(machinePool.Spec.FailureDomains) > 0 {
-		// For MachinePools placement is expected to be managed by the underlying infrastructure primitive, but
-		// given that there is no such an thing in CAPD, we are picking a random failure domain.
-		randomIndex := rand.Intn(len(machinePool.Spec.FailureDomains)) //nolint:gosec
-		for k, v := range docker.FailureDomainLabel(&machinePool.Spec.FailureDomains[randomIndex]) {
-			labels[k] = v
-		}
-	}
-
-	log.Info("Creating container for machinePool", "name", name, "machinePool", machinePool.Name)
-	if err := externalMachine.Create(ctx, dockerMachinePool.Spec.Template.CustomImage, constants.WorkerNodeRoleValue, machinePool.Spec.Template.Spec.Version, labels, dockerMachinePool.Spec.Template.ExtraMounts); err != nil {
-		return errors.Wrapf(err, "failed to create docker machine with name %s", name)
-	}
-	return nil
-}
-
-// reconcileDockerMachines creates and deletes DockerMachines to match the MachinePool's desired number of replicas and infrastructure spec.
-// It is responsible for
-// - Ensuring each Docker container has an associated DockerMachine by creating one if it doesn't already exist.
-// - Ensuring that deletion for Docker container happens by calling delete on the associated Machine so that the node is cordoned/drained and the infrastructure is cleaned up.
-// - Deleting DockerMachines referencing a container whose Kubernetes version or custom image no longer matches the spec.
-// - Deleting DockerMachines that correspond to a deleted/non-existent Docker container.
-// - Deleting DockerMachines when scaling down such that DockerMachines whose owner Machine has the clusterv1.DeleteMachineAnnotation is given priority.
-func (r *DockerMachinePoolReconciler) reconcileDockerMachines(ctx context.Context, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) error {
-	log := ctrl.LoggerFrom(ctx)
-
-	log.V(2).Info("Reconciling DockerMachines", "dockerMachinePool", dockerMachinePool.Name, "namespace", dockerMachinePool.Namespace)
-
-	dockerMachineList, err := getDockerMachines(ctx, r.Client, *cluster, *machinePool, *dockerMachinePool)
-	if err != nil {
-		return err
-	}
-
-	dockerMachineMap := make(map[string]infrav1.DockerMachine)
-	for _, dockerMachine := range dockerMachineList.Items {
-		dockerMachineMap[dockerMachine.Name] = dockerMachine
-	}
-
-	// List the Docker containers. This corresponds to a InfraMachinePool instance for providers.
-	labelFilters := map[string]string{dockerMachinePoolLabel: dockerMachinePool.Name}
-	externalMachines, err := docker.ListMachinesByCluster(ctx, cluster, labelFilters)
-	if err != nil {
-		return errors.Wrapf(err, "failed to list all machines in the cluster")
-	}
-
-	externalMachineMap := make(map[string]*docker.Machine)
-	for _, externalMachine := range externalMachines {
-		externalMachineMap[externalMachine.Name()] = externalMachine
-	}
-
-	// Step 1:
-	// Create a DockerMachine for each Docker container so we surface the information to the user. Use the same name as the Docker container for the Docker Machine for ease of lookup.
-	// Providers should iterate through their infrastructure instances and ensure that each instance has a corresponding InfraMachine.
-	for _, machine := range externalMachines {
-		if _, ok := dockerMachineMap[machine.Name()]; !ok {
-			log.V(2).Info("Creating a new DockerMachine for Docker container", "container", machine.Name())
-			dockerMachine, err := r.createDockerMachine(ctx, machine.Name(), cluster, machinePool, dockerMachinePool)
-			if err != nil {
-				return errors.Wrap(err, "failed to create a new docker machine")
-			}
-
-			dockerMachineMap[dockerMachine.Name] = *dockerMachine
-		} else {
-			log.V(4).Info("DockerMachine already exists, nothing to do", "name", machine.Name())
-		}
-	}
-
-	// Step 2:
-	// Delete any DockerMachines that correspond to a deleted Docker container.
-	// Providers should iterate through the InfraMachines to ensure each one still corresponds to an existing infrastructure instance.
-	// This allows the InfraMachine (and owner Machine) to be deleted and avoid hanging resources when a user deletes an instance out-of-band.
-	for _, dockerMachine := range dockerMachineMap {
-		if _, ok := externalMachineMap[dockerMachine.Name]; !ok {
-			log.V(2).Info("Deleting DockerMachine with no underlying infrastructure", "dockerMachine", dockerMachine.Name)
-			if err := r.deleteMachinePoolMachine(ctx, dockerMachine); err != nil {
-				return err
-			}
-
-			delete(dockerMachineMap, dockerMachine.Name)
-		}
-	}
-
-	// Step 3:
-	// This handles the scale down/excess replicas case and the case where a rolling upgrade is needed.
-	// If there are more ready DockerMachines than desired replicas, start to delete the excess DockerMachines such that
-	// - DockerMachines with an outdated Kubernetes version or custom image are deleted first (i.e. the rolling upgrade).
-	// - DockerMachines whose owner Machine contains the clusterv1.DeleteMachineAnnotation are deleted next (to support cluster autoscaler).
-	// Note: we want to ensure that there are always enough ready DockerMachines before deleting anything or scaling down.
-
-	// For each DockerMachine, fetch the owner Machine and copy the clusterv1.DeleteMachineAnnotation to the DockerMachine if it exists before sorting the DockerMachines.
-	// This is done just before sorting to guarantee we have the latest copy of the Machine annotations.
-	dockerMachinesWithAnnotation, err := r.propagateMachineDeleteAnnotation(ctx, dockerMachineMap)
-	if err != nil {
-		return err
-	}
-
-	// Sort DockerMachines with the clusterv1.DeleteMachineAnnotation to the front of each list.
-	// If providers already have a sorting order for instance deletion, i.e. oldest first or newest first, the clusterv1.DeleteMachineAnnotation must take priority.
-	// For example, if deleting by oldest, we expect the InfraMachines with clusterv1.DeleteMachineAnnotation to be deleted first followed by the oldest, and the second oldest, etc.
-	orderedDockerMachines := orderByDeleteMachineAnnotation(dockerMachinesWithAnnotation)
-
-	// Note: this includes DockerMachines that are out of date but still ready. This is to ensure we always have enough ready DockerMachines before deleting anything.
-	totalReadyMachines := 0
-	for i := range orderedDockerMachines {
-		dockerMachine := orderedDockerMachines[i]
-		if dockerMachine.Status.Ready || conditions.IsTrue(&dockerMachine, clusterv1.ReadyCondition) {
-			totalReadyMachines++
-		}
-	}
-
-	outdatedMachines, readyMachines, err := r.getDeletionCandidates(ctx, orderedDockerMachines, externalMachineMap, machinePool, dockerMachinePool)
-	if err != nil {
-		return err
-	}
-
-	desiredReplicas := int(*machinePool.Spec.Replicas)
-	overProvisionCount := totalReadyMachines - desiredReplicas
-
-	// Loop through outdated DockerMachines first and decrement the overProvisionCount until it reaches 0.
-	for _, dockerMachine := range outdatedMachines {
-		if overProvisionCount > 0 {
-			log.V(2).Info("Deleting DockerMachine because it is outdated", "dockerMachine", dockerMachine.Name, "namespace", dockerMachine.Namespace)
-			if err := r.deleteMachinePoolMachine(ctx, dockerMachine); err != nil {
-				return err
-			}
-
-			overProvisionCount--
-		}
-	}
-
-	// Then, loop through the ready DockerMachines first and decrement the overProvisionCount until it reaches 0.
-	for _, dockerMachine := range readyMachines {
-		if overProvisionCount > 0 {
-			log.V(2).Info("Deleting DockerMachine because it is an excess replica", "dockerMachine", dockerMachine.Name, "namespace", dockerMachine.Namespace)
-			if err := r.deleteMachinePoolMachine(ctx, dockerMachine); err != nil {
-				return err
-			}
-
-			overProvisionCount--
-		}
-	}
-
-	return nil
-}
-
-// createDockerMachine creates a DockerMachine to represent a Docker container in a DockerMachinePool.
-// These DockerMachines have the clusterv1.ClusterNameLabel and clusterv1.MachinePoolNameLabel to support MachinePool Machines.
-func (r *DockerMachinePoolReconciler) createDockerMachine(ctx context.Context, name string, cluster *clusterv1.Cluster, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) (*infrav1.DockerMachine, error) {
-	log := ctrl.LoggerFrom(ctx)
-
-	labels := map[string]string{
-		clusterv1.ClusterNameLabel:     cluster.Name,
-		clusterv1.MachinePoolNameLabel: format.MustFormatValue(machinePool.Name),
-	}
-	dockerMachine := &infrav1.DockerMachine{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace:   dockerMachinePool.Namespace,
-			Name:        name,
-			Labels:      labels,
-			Annotations: make(map[string]string),
-			OwnerReferences: []metav1.OwnerReference{
-				{
-					APIVersion: dockerMachinePool.APIVersion,
-					Kind:       dockerMachinePool.Kind,
-					Name:       dockerMachinePool.Name,
-					UID:        dockerMachinePool.UID,
-				},
-				// Note: Since the MachinePool controller has not created its owner Machine yet, we want to set the DockerMachinePool as the owner so it's not orphaned.
-			},
-		},
-		Spec: infrav1.DockerMachineSpec{
-			CustomImage:   dockerMachinePool.Spec.Template.CustomImage,
-			PreLoadImages: dockerMachinePool.Spec.Template.PreLoadImages,
-			ExtraMounts:   dockerMachinePool.Spec.Template.ExtraMounts,
-		},
-	}
-
-	log.V(2).Info("Creating DockerMachine", "dockerMachine", dockerMachine.Name)
-
-	if err := r.Client.Create(ctx, dockerMachine); err != nil {
-		return nil, errors.Wrap(err, "failed to create dockerMachine")
-	}
-
-	return dockerMachine, nil
-}
-
-// deleteMachinePoolMachine attempts to delete a DockerMachine and its associated owner Machine if it exists.
-func (r *DockerMachinePoolReconciler) deleteMachinePoolMachine(ctx context.Context, dockerMachine infrav1.DockerMachine) error {
-	log := ctrl.LoggerFrom(ctx)
-
-	machine, err := util.GetOwnerMachine(ctx, r.Client, dockerMachine.ObjectMeta)
-	if err != nil {
-		return errors.Wrapf(err, "error getting owner Machine for DockerMachine %s/%s", dockerMachine.Namespace, dockerMachine.Name)
-	}
-	// util.GetOwnerMachine() returns a nil Machine without error if there is no Machine kind in the ownerRefs, so we must verify that machine is not nil.
-	if machine == nil {
-		log.V(2).Info("No owner Machine exists for DockerMachine", "dockerMachine", klog.KObj(&dockerMachine))
-
-		// If the DockerMachine does not have an owner Machine, do not attempt to delete the DockerMachine as the MachinePool controller will create the
-		// Machine and we want to let it catch up. If we are too hasty to delete, that introduces a race condition where the DockerMachine could be deleted
-		// just as the Machine comes online.
-
-		// In the case where the MachinePool is being deleted and the Machine will never come online, the DockerMachine will be deleted via its ownerRef to the
-		// DockerMachinePool, so that is covered as well.
-
-		return nil
-	}
-
-	log.Info("Deleting Machine for DockerMachine", "machine", klog.KObj(machine), "dockerMachine", klog.KObj(&dockerMachine))
-
-	if err := r.Client.Delete(ctx, machine); err != nil {
-		return errors.Wrapf(err, "failed to delete Machine %s/%s", machine.Namespace, machine.Name)
-	}
-
-	return nil
-}
-
-// propagateMachineDeleteAnnotation returns the DockerMachines for a MachinePool and for each DockerMachine, it copies the owner
-// Machine's delete annotation to each DockerMachine if it's present. This is done just in time to ensure that the annotations are
-// up to date when we sort for DockerMachine deletion.
-func (r *DockerMachinePoolReconciler) propagateMachineDeleteAnnotation(ctx context.Context, dockerMachineSet map[string]infrav1.DockerMachine) ([]infrav1.DockerMachine, error) {
-	_ = ctrl.LoggerFrom(ctx)
-
-	dockerMachines := []infrav1.DockerMachine{}
-	for _, dockerMachine := range dockerMachineSet {
-		machine, err := util.GetOwnerMachine(ctx, r.Client, dockerMachine.ObjectMeta)
-		if err != nil {
-			return nil, errors.Wrapf(err, "error getting owner Machine for DockerMachine %s/%s", dockerMachine.Namespace, dockerMachine.Name)
-		}
-		if machine != nil && machine.Annotations != nil {
-			if _, hasDeleteAnnotation := machine.Annotations[clusterv1.DeleteMachineAnnotation]; hasDeleteAnnotation {
-				dockerMachine.Annotations[clusterv1.DeleteMachineAnnotation] = machine.Annotations[clusterv1.DeleteMachineAnnotation]
-			}
-		}
-
-		dockerMachines = append(dockerMachines, dockerMachine)
-	}
-
-	return dockerMachines, nil
-}
-
-// orderByDeleteMachineAnnotation will sort DockerMachines with the clusterv1.DeleteMachineAnnotation to the front of the list.
-// It will preserve the existing order of the list otherwise so that it respects the existing delete priority otherwise.
-func orderByDeleteMachineAnnotation(machines []infrav1.DockerMachine) []infrav1.DockerMachine {
-	sort.SliceStable(machines, func(i, j int) bool {
-		_, iHasAnnotation := machines[i].Annotations[clusterv1.DeleteMachineAnnotation]
-
-		return iHasAnnotation
-	})
-
-	return machines
-}
-
-// isMachineMatchingInfrastructureSpec returns true if the Docker container image matches the custom image in the DockerMachinePool spec.
-func isMachineMatchingInfrastructureSpec(_ context.Context, machine *docker.Machine, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) bool {
-	// NOTE: With the current implementation we are checking if the machine is using a kindest/node image for the expected version,
-	// but not checking if the machine has the expected extra.mounts or pre.loaded images.
-
-	semVer, err := semver.Parse(strings.TrimPrefix(*machinePool.Spec.Template.Spec.Version, "v"))
-	if err != nil {
-		// TODO: consider if to return an error
-		panic(errors.Wrap(err, "failed to parse DockerMachine version").Error())
-	}
-
-	kindMapping := kind.GetMapping(semVer, dockerMachinePool.Spec.Template.CustomImage)
-
-	return machine.ContainerImage() == kindMapping.Image
-}
-
-// machinesMatchingInfrastructureSpec returns the  Docker containers matching the custom image in the DockerMachinePool spec.
-func machinesMatchingInfrastructureSpec(ctx context.Context, machines []*docker.Machine, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) []*docker.Machine {
-	var matchingMachines []*docker.Machine
-	for _, machine := range machines {
-		if isMachineMatchingInfrastructureSpec(ctx, machine, machinePool, dockerMachinePool) {
-			matchingMachines = append(matchingMachines, machine)
-		}
-	}
-
-	return matchingMachines
-}
-
-// getDeletionCandidates returns the DockerMachines for a MachinePool that do not match the infrastructure spec followed by any DockerMachines that are ready and up to date, i.e. matching the infrastructure spec.
-func (r *DockerMachinePoolReconciler) getDeletionCandidates(ctx context.Context, dockerMachines []infrav1.DockerMachine, externalMachineSet map[string]*docker.Machine, machinePool *expv1.MachinePool, dockerMachinePool *infraexpv1.DockerMachinePool) (outdatedMachines []infrav1.DockerMachine, readyMatchingMachines []infrav1.DockerMachine, err error) {
-	for i := range dockerMachines {
-		dockerMachine := dockerMachines[i]
-		externalMachine, ok := externalMachineSet[dockerMachine.Name]
-		if !ok {
-			// Note: Since we deleted any DockerMachines that do not have an associated Docker container earlier, we should never hit this case.
-			return nil, nil, errors.Errorf("failed to find externalMachine for DockerMachine %s/%s", dockerMachine.Namespace, dockerMachine.Name)
-		}
-
-		if !isMachineMatchingInfrastructureSpec(ctx, externalMachine, machinePool, dockerMachinePool) {
-			outdatedMachines = append(outdatedMachines, dockerMachine)
-		} else if dockerMachine.Status.Ready || conditions.IsTrue(&dockerMachine, clusterv1.ReadyCondition) {
-			readyMatchingMachines = append(readyMatchingMachines, dockerMachine)
-		}
-	}
-
-	return outdatedMachines, readyMatchingMachines, nil
-}
diff --git a/test/infrastructure/docker/exp/internal/docker/nodepool.go b/test/infrastructure/docker/exp/internal/docker/nodepool.go
new file mode 100644
index 000000000..12920be70
--- /dev/null
+++ b/test/infrastructure/docker/exp/internal/docker/nodepool.go
@@ -0,0 +1,388 @@
+/*
+Copyright 2020 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 docker implements docker functionality.
+package docker
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"math/rand"
+	"strings"
+	"time"
+
+	"github.com/blang/semver/v4"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/kind/pkg/cluster/constants"
+
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
+	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
+	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
+	infraexpv1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/exp/api/v1beta1"
+	"sigs.k8s.io/cluster-api/test/infrastructure/docker/internal/docker"
+	"sigs.k8s.io/cluster-api/test/infrastructure/kind"
+	"sigs.k8s.io/cluster-api/util"
+)
+
+const (
+	dockerMachinePoolLabel = "docker.cluster.x-k8s.io/machine-pool"
+)
+
+// NodePool is a wrapper around a collection of like machines which are owned by a DockerMachinePool. A node pool
+// provides a friendly way of managing (adding, deleting, updating) a set of docker machines. The node pool will also
+// sync the docker machine pool status Instances field with the state of the docker machines.
+type NodePool struct {
+	client            client.Client
+	cluster           *clusterv1.Cluster
+	machinePool       *expv1.MachinePool
+	dockerMachinePool *infraexpv1.DockerMachinePool
+	labelFilters      map[string]string
+	machines          []*docker.Machine
+}
+
+// NewNodePool creates a new node pool instances.
+func NewNodePool(ctx context.Context, c client.Client, cluster *clusterv1.Cluster, mp *expv1.MachinePool, dmp *infraexpv1.DockerMachinePool) (*NodePool, error) {
+	np := &NodePool{
+		client:            c,
+		cluster:           cluster,
+		machinePool:       mp,
+		dockerMachinePool: dmp,
+		labelFilters:      map[string]string{dockerMachinePoolLabel: dmp.Name},
+	}
+
+	if err := np.refresh(ctx); err != nil {
+		return np, errors.Wrapf(err, "failed to refresh the node pool")
+	}
+	return np, nil
+}
+
+// ReconcileMachines will build enough machines to satisfy the machine pool / docker machine pool spec
+// eventually delete all the machine in excess, and update the status for all the machines.
+//
+// NOTE: The goal for the current implementation is to verify MachinePool construct; accordingly,
+// currently the nodepool supports only a recreate strategy for replacing old nodes with new ones
+// (all existing machines are killed before new ones are created).
+// TODO: consider if to support a Rollout strategy (a more progressive node replacement).
+func (np *NodePool) ReconcileMachines(ctx context.Context, remoteClient client.Client) (ctrl.Result, error) {
+	desiredReplicas := int(*np.machinePool.Spec.Replicas)
+
+	// Delete all the machines in excess (outdated machines or machines exceeding desired replica count).
+	machineDeleted := false
+	totalNumberOfMachines := 0
+	for _, machine := range np.machines {
+		totalNumberOfMachines++
+		if totalNumberOfMachines > desiredReplicas || !np.isMachineMatchingInfrastructureSpec(machine) {
+			externalMachine, err := docker.NewMachine(ctx, np.cluster, machine.Name(), np.labelFilters)
+			if err != nil {
+				return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", machine.Name())
+			}
+			if err := externalMachine.Delete(ctx); err != nil {
+				return ctrl.Result{}, errors.Wrapf(err, "failed to delete machine %s", machine.Name())
+			}
+			machineDeleted = true
+			totalNumberOfMachines-- // remove deleted machine from the count
+		}
+	}
+	if machineDeleted {
+		if err := np.refresh(ctx); err != nil {
+			return ctrl.Result{}, errors.Wrapf(err, "failed to refresh the node pool")
+		}
+	}
+
+	// Add new machines if missing.
+	machineAdded := false
+	matchingMachineCount := len(np.machinesMatchingInfrastructureSpec())
+	if matchingMachineCount < desiredReplicas {
+		for i := 0; i < desiredReplicas-matchingMachineCount; i++ {
+			if err := np.addMachine(ctx); err != nil {
+				return ctrl.Result{}, errors.Wrap(err, "failed to create a new docker machine")
+			}
+			machineAdded = true
+		}
+	}
+	if machineAdded {
+		if err := np.refresh(ctx); err != nil {
+			return ctrl.Result{}, errors.Wrapf(err, "failed to refresh the node pool")
+		}
+	}
+
+	// First remove instance status for machines no longer existing, then reconcile the existing machines.
+	// NOTE: the status is the only source of truth for understanding if the machine is already bootstrapped, ready etc.
+	// so we are preserving the existing status and using it as a bases for the next reconcile machine.
+	instances := make([]infraexpv1.DockerMachinePoolInstanceStatus, 0, len(np.machines))
+	for i := range np.dockerMachinePool.Status.Instances {
+		instance := np.dockerMachinePool.Status.Instances[i]
+		for j := range np.machines {
+			if instance.InstanceName == np.machines[j].Name() {
+				instances = append(instances, instance)
+				break
+			}
+		}
+	}
+	np.dockerMachinePool.Status.Instances = instances
+
+	result := ctrl.Result{}
+	for i := range np.machines {
+		machine := np.machines[i]
+		if res, err := np.reconcileMachine(ctx, machine, remoteClient); err != nil || !res.IsZero() {
+			if err != nil {
+				return ctrl.Result{}, errors.Wrap(err, "failed to reconcile machine")
+			}
+			result = util.LowestNonZeroResult(result, res)
+		}
+	}
+	return result, nil
+}
+
+// Delete will delete all of the machines in the node pool.
+func (np *NodePool) Delete(ctx context.Context) error {
+	for _, machine := range np.machines {
+		externalMachine, err := docker.NewMachine(ctx, np.cluster, machine.Name(), np.labelFilters)
+		if err != nil {
+			return errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", machine.Name())
+		}
+
+		if err := externalMachine.Delete(ctx); err != nil {
+			return errors.Wrapf(err, "failed to delete machine %s", machine.Name())
+		}
+	}
+
+	return nil
+}
+
+func (np *NodePool) isMachineMatchingInfrastructureSpec(machine *docker.Machine) bool {
+	// NOTE: With the current implementation we are checking if the machine is using a kindest/node image for the expected version,
+	// but not checking if the machine has the expected extra.mounts or pre.loaded images.
+
+	semVer, err := semver.Parse(strings.TrimPrefix(*np.machinePool.Spec.Template.Spec.Version, "v"))
+	if err != nil {
+		// TODO: consider if to return an error
+		panic(errors.Wrap(err, "failed to parse DockerMachine version").Error())
+	}
+
+	kindMapping := kind.GetMapping(semVer, np.dockerMachinePool.Spec.Template.CustomImage)
+
+	return machine.ContainerImage() == kindMapping.Image
+}
+
+// machinesMatchingInfrastructureSpec returns all of the docker.Machines which match the machine pool / docker machine pool spec.
+func (np *NodePool) machinesMatchingInfrastructureSpec() []*docker.Machine {
+	var matchingMachines []*docker.Machine
+	for _, machine := range np.machines {
+		if np.isMachineMatchingInfrastructureSpec(machine) {
+			matchingMachines = append(matchingMachines, machine)
+		}
+	}
+
+	return matchingMachines
+}
+
+// addMachine will add a new machine to the node pool and update the docker machine pool status.
+func (np *NodePool) addMachine(ctx context.Context) error {
+	instanceName := fmt.Sprintf("worker-%s", util.RandomString(6))
+	externalMachine, err := docker.NewMachine(ctx, np.cluster, instanceName, np.labelFilters)
+	if err != nil {
+		return errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", instanceName)
+	}
+
+	// NOTE: FailureDomains don't mean much in CAPD since it's all local, but we are setting a label on
+	// each container, so we can check placement.
+	labels := map[string]string{}
+	for k, v := range np.labelFilters {
+		labels[k] = v
+	}
+
+	if len(np.machinePool.Spec.FailureDomains) > 0 {
+		// For MachinePools placement is expected to be managed by the underlying infrastructure primitive, but
+		// given that there is no such an thing in CAPD, we are picking a random failure domain.
+		randomIndex := rand.Intn(len(np.machinePool.Spec.FailureDomains)) //nolint:gosec
+		for k, v := range docker.FailureDomainLabel(&np.machinePool.Spec.FailureDomains[randomIndex]) {
+			labels[k] = v
+		}
+	}
+
+	if err := externalMachine.Create(ctx, np.dockerMachinePool.Spec.Template.CustomImage, constants.WorkerNodeRoleValue, np.machinePool.Spec.Template.Spec.Version, labels, np.dockerMachinePool.Spec.Template.ExtraMounts); err != nil {
+		return errors.Wrapf(err, "failed to create docker machine with instance name %s", instanceName)
+	}
+	return nil
+}
+
+// refresh asks docker to list all the machines matching the node pool label and updates the cached list of node pool
+// machines.
+func (np *NodePool) refresh(ctx context.Context) error {
+	machines, err := docker.ListMachinesByCluster(ctx, np.cluster, np.labelFilters)
+	if err != nil {
+		return errors.Wrapf(err, "failed to list all machines in the cluster")
+	}
+
+	np.machines = make([]*docker.Machine, 0, len(machines))
+	for i := range machines {
+		machine := machines[i]
+		// makes sure no control plane machines gets selected by chance.
+		if !machine.IsControlPlane() {
+			np.machines = append(np.machines, machine)
+		}
+	}
+	return nil
+}
+
+// reconcileMachine will build and provision a docker machine and update the docker machine pool status for that instance.
+func (np *NodePool) reconcileMachine(ctx context.Context, machine *docker.Machine, remoteClient client.Client) (ctrl.Result, error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	var machineStatus infraexpv1.DockerMachinePoolInstanceStatus
+	isFound := false
+	for _, instanceStatus := range np.dockerMachinePool.Status.Instances {
+		if instanceStatus.InstanceName == machine.Name() {
+			machineStatus = instanceStatus
+			isFound = true
+		}
+	}
+	if !isFound {
+		log.Info("Creating instance record", "instance", machine.Name())
+		machineStatus = infraexpv1.DockerMachinePoolInstanceStatus{
+			InstanceName: machine.Name(),
+			Version:      np.machinePool.Spec.Template.Spec.Version,
+		}
+		np.dockerMachinePool.Status.Instances = append(np.dockerMachinePool.Status.Instances, machineStatus)
+		// return to surface the new machine exists.
+		return ctrl.Result{Requeue: true}, nil
+	}
+
+	defer func() {
+		for i, instanceStatus := range np.dockerMachinePool.Status.Instances {
+			if instanceStatus.InstanceName == machine.Name() {
+				np.dockerMachinePool.Status.Instances[i] = machineStatus
+			}
+		}
+	}()
+
+	externalMachine, err := docker.NewMachine(ctx, np.cluster, machine.Name(), np.labelFilters)
+	if err != nil {
+		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", machine.Name())
+	}
+
+	// if the machine isn't bootstrapped, only then run bootstrap scripts
+	if !machineStatus.Bootstrapped {
+		timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
+		defer cancel()
+
+		// Check for bootstrap success
+		// We have to check here to make this reentrant for cases where the bootstrap works
+		// but bootstrapped is never set on the object. We only try to bootstrap if the machine
+		// is not already bootstrapped.
+		if err := externalMachine.CheckForBootstrapSuccess(timeoutCtx, false); err != nil {
+			log.Info("Bootstrapping instance", "instance", machine.Name())
+			if err := externalMachine.PreloadLoadImages(timeoutCtx, np.dockerMachinePool.Spec.Template.PreLoadImages); err != nil {
+				return ctrl.Result{}, errors.Wrapf(err, "failed to pre-load images into the docker machine with instance name %s", machine.Name())
+			}
+
+			bootstrapData, format, err := getBootstrapData(timeoutCtx, np.client, np.machinePool)
+			if err != nil {
+				return ctrl.Result{}, errors.Wrapf(err, "failed to get bootstrap data for instance named %s", machine.Name())
+			}
+
+			// Run the bootstrap script. Simulates cloud-init/Ignition.
+			if err := externalMachine.ExecBootstrap(timeoutCtx, bootstrapData, format, np.machinePool.Spec.Template.Spec.Version, np.dockerMachinePool.Spec.Template.CustomImage); err != nil {
+				return ctrl.Result{}, errors.Wrapf(err, "failed to exec DockerMachinePool instance bootstrap for instance named %s", machine.Name())
+			}
+			// Check for bootstrap success
+			if err := externalMachine.CheckForBootstrapSuccess(timeoutCtx, true); err != nil {
+				return ctrl.Result{}, errors.Wrap(err, "failed to check for existence of bootstrap success file at /run/cluster-api/bootstrap-success.complete")
+			}
+		}
+		machineStatus.Bootstrapped = true
+
+		// return to surface the machine has been bootstrapped.
+		return ctrl.Result{Requeue: true}, nil
+	}
+
+	if machineStatus.Addresses == nil {
+		log.Info("Fetching instance addresses", "instance", machine.Name())
+		// set address in machine status
+		machineAddresses, err := externalMachine.Address(ctx)
+		if err != nil {
+			// Requeue if there is an error, as this is likely momentary load balancer
+			// state changes during control plane provisioning.
+			return ctrl.Result{Requeue: true}, nil //nolint:nilerr
+		}
+
+		machineStatus.Addresses = []clusterv1.MachineAddress{
+			{
+				Type:    clusterv1.MachineHostName,
+				Address: externalMachine.ContainerName(),
+			},
+		}
+		for _, addr := range machineAddresses {
+			machineStatus.Addresses = append(machineStatus.Addresses,
+				clusterv1.MachineAddress{
+					Type:    clusterv1.MachineInternalIP,
+					Address: addr,
+				},
+				clusterv1.MachineAddress{
+					Type:    clusterv1.MachineExternalIP,
+					Address: addr,
+				})
+		}
+	}
+
+	if machineStatus.ProviderID == nil {
+		log.Info("Fetching instance provider ID", "instance", machine.Name())
+		// Usually a cloud provider will do this, but there is no docker-cloud provider.
+		// Requeue if there is an error, as this is likely momentary load balancer
+		// state changes during control plane provisioning.
+		if err = externalMachine.SetNodeProviderID(ctx, remoteClient); err != nil {
+			log.V(4).Info("transient error setting the provider id")
+			return ctrl.Result{Requeue: true}, nil //nolint:nilerr
+		}
+		// Set ProviderID so the Cluster API Machine Controller can pull it
+		providerID := externalMachine.ProviderID()
+		machineStatus.ProviderID = &providerID
+	}
+
+	machineStatus.Ready = true
+	return ctrl.Result{}, nil
+}
+
+// getBootstrapData fetches the bootstrap data for the machine pool.
+func getBootstrapData(ctx context.Context, c client.Client, machinePool *expv1.MachinePool) (string, bootstrapv1.Format, error) {
+	if machinePool.Spec.Template.Spec.Bootstrap.DataSecretName == nil {
+		return "", "", errors.New("error retrieving bootstrap data: linked MachinePool's bootstrap.dataSecretName is nil")
+	}
+
+	s := &corev1.Secret{}
+	key := client.ObjectKey{Namespace: machinePool.GetNamespace(), Name: *machinePool.Spec.Template.Spec.Bootstrap.DataSecretName}
+	if err := c.Get(ctx, key, s); err != nil {
+		return "", "", errors.Wrapf(err, "failed to retrieve bootstrap data secret for DockerMachinePool instance %s", klog.KObj(machinePool))
+	}
+
+	value, ok := s.Data["value"]
+	if !ok {
+		return "", "", errors.New("error retrieving bootstrap data: secret value key is missing")
+	}
+
+	format := s.Data["format"]
+	if len(format) == 0 {
+		format = []byte(bootstrapv1.CloudConfig)
+	}
+
+	return base64.StdEncoding.EncodeToString(value), bootstrapv1.Format(format), nil
+}
diff --git a/test/infrastructure/docker/internal/controllers/dockermachine_controller.go b/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
index 68b3553ff..f24c0bd5b 100644
--- a/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
+++ b/test/infrastructure/docker/internal/controllers/dockermachine_controller.go
@@ -38,14 +38,12 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controllers/remote"
-	utilexp "sigs.k8s.io/cluster-api/exp/util"
 	"sigs.k8s.io/cluster-api/test/infrastructure/container"
 	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1beta1"
 	"sigs.k8s.io/cluster-api/test/infrastructure/docker/internal/docker"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/cluster-api/util/labels"
 	clog "sigs.k8s.io/cluster-api/util/log"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
@@ -159,18 +157,8 @@ func (r *DockerMachineReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		return ctrl.Result{}, nil
 	}
 
-	// Create a helper for managing the Docker container hosting the machine.
-	// The DockerMachine needs a way to know the name of the Docker container, before MachinePool Machines were implemented, it used the name of the owner Machine.
-	// But since the DockerMachine type is used for both MachineDeployment Machines and MachinePool Machines, we need to accommodate both:
-	// - For MachineDeployments/Control Planes, we continue using the name of the Machine so that it is backwards compatible in the CAPI version upgrade scenario .
-	// - For MachinePools, the order of creation is Docker container -> DockerMachine -> Machine. Since the Docker container is created first, and the Machine name is
-	//   randomly generated by the MP controller, we create the DockerMachine with the same name as the container to maintain this association.
-	name := machine.Name
-	if labels.IsMachinePoolOwned(dockerMachine) {
-		name = dockerMachine.Name
-	}
-
-	externalMachine, err := docker.NewMachine(ctx, cluster, name, nil)
+	// Create a helper for managing the docker container hosting the machine.
+	externalMachine, err := docker.NewMachine(ctx, cluster, machine.Name, nil)
 	if err != nil {
 		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalMachine")
 	}
@@ -233,26 +221,6 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, cluster *
 		return ctrl.Result{}, nil
 	}
 
-	var dataSecretName *string
-	var version *string
-
-	if labels.IsMachinePoolOwned(dockerMachine) {
-		machinePool, err := utilexp.GetMachinePoolByLabels(ctx, r.Client, dockerMachine.GetNamespace(), dockerMachine.Labels)
-		if err != nil {
-			return ctrl.Result{}, errors.Wrapf(err, "failed to get machine pool for DockerMachine %s/%s", dockerMachine.GetNamespace(), dockerMachine.GetName())
-		}
-		if machinePool == nil {
-			log.Info("No MachinePool matching labels found, returning without error")
-			return ctrl.Result{}, nil
-		}
-
-		dataSecretName = machinePool.Spec.Template.Spec.Bootstrap.DataSecretName
-		version = machinePool.Spec.Template.Spec.Version
-	} else {
-		dataSecretName = machine.Spec.Bootstrap.DataSecretName
-		version = machine.Spec.Version
-	}
-
 	// if the machine is already provisioned, return
 	if dockerMachine.Spec.ProviderID != nil {
 		// ensure ready state is set.
@@ -272,7 +240,7 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, cluster *
 	}
 
 	// Make sure bootstrap data is available and populated.
-	if dataSecretName == nil {
+	if machine.Spec.Bootstrap.DataSecretName == nil {
 		if !util.IsControlPlaneMachine(machine) && !conditions.IsTrue(cluster, clusterv1.ControlPlaneInitializedCondition) {
 			log.Info("Waiting for the control plane to be initialized")
 			conditions.MarkFalse(dockerMachine, infrav1.ContainerProvisionedCondition, clusterv1.WaitingForControlPlaneAvailableReason, clusterv1.ConditionSeverityInfo, "")
@@ -350,8 +318,7 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, cluster *
 		// but bootstrapped is never set on the object. We only try to bootstrap if the machine
 		// is not already bootstrapped.
 		if err := externalMachine.CheckForBootstrapSuccess(timeoutCtx, false); err != nil {
-			// We know the bootstrap data is not nil because we checked above.
-			bootstrapData, format, err := r.getBootstrapData(timeoutCtx, dockerMachine.Namespace, *dataSecretName)
+			bootstrapData, format, err := r.getBootstrapData(timeoutCtx, machine)
 			if err != nil {
 				return ctrl.Result{}, err
 			}
@@ -379,11 +346,10 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, cluster *
 			}()
 
 			// Run the bootstrap script. Simulates cloud-init/Ignition.
-			if err := externalMachine.ExecBootstrap(timeoutCtx, bootstrapData, format, version, dockerMachine.Spec.CustomImage); err != nil {
+			if err := externalMachine.ExecBootstrap(timeoutCtx, bootstrapData, format, machine.Spec.Version, dockerMachine.Spec.CustomImage); err != nil {
 				conditions.MarkFalse(dockerMachine, infrav1.BootstrapExecSucceededCondition, infrav1.BootstrapFailedReason, clusterv1.ConditionSeverityWarning, "Repeating bootstrap")
 				return ctrl.Result{}, errors.Wrap(err, "failed to exec DockerMachine bootstrap")
 			}
-
 			// Check for bootstrap success
 			if err := externalMachine.CheckForBootstrapSuccess(timeoutCtx, true); err != nil {
 				conditions.MarkFalse(dockerMachine, infrav1.BootstrapExecSucceededCondition, infrav1.BootstrapFailedReason, clusterv1.ConditionSeverityWarning, "Repeating bootstrap")
@@ -535,11 +501,15 @@ func (r *DockerMachineReconciler) DockerClusterToDockerMachines(ctx context.Cont
 	return result
 }
 
-func (r *DockerMachineReconciler) getBootstrapData(ctx context.Context, namespace string, dataSecretName string) (string, bootstrapv1.Format, error) {
+func (r *DockerMachineReconciler) getBootstrapData(ctx context.Context, machine *clusterv1.Machine) (string, bootstrapv1.Format, error) {
+	if machine.Spec.Bootstrap.DataSecretName == nil {
+		return "", "", errors.New("error retrieving bootstrap data: linked Machine's bootstrap.dataSecretName is nil")
+	}
+
 	s := &corev1.Secret{}
-	key := client.ObjectKey{Namespace: namespace, Name: dataSecretName}
+	key := client.ObjectKey{Namespace: machine.GetNamespace(), Name: *machine.Spec.Bootstrap.DataSecretName}
 	if err := r.Client.Get(ctx, key, s); err != nil {
-		return "", "", errors.Wrapf(err, "failed to retrieve bootstrap data secret %s", dataSecretName)
+		return "", "", errors.Wrapf(err, "failed to retrieve bootstrap data secret for DockerMachine %s", klog.KObj(machine))
 	}
 
 	value, ok := s.Data["value"]
diff --git a/util/labels/helpers_test.go b/util/labels/helpers_test.go
index 00af2f5ab..d62701480 100644
--- a/util/labels/helpers_test.go
+++ b/util/labels/helpers_test.go
@@ -84,60 +84,3 @@ func TestHasWatchLabel(t *testing.T) {
 		})
 	}
 }
-
-func TestIsMachinePoolOwned(t *testing.T) {
-	tests := []struct {
-		name     string
-		object   metav1.Object
-		expected bool
-	}{
-		{
-			name: "machine is a MachinePoolMachine",
-			object: &clusterv1.Machine{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						clusterv1.MachinePoolNameLabel: "foo",
-					},
-				},
-			},
-			expected: true,
-		},
-		{
-			name: "random type is machinepool owned",
-			object: &corev1.Node{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						clusterv1.MachinePoolNameLabel: "foo",
-					},
-				},
-			},
-			expected: true,
-		},
-		{
-			name: "machine not a MachinePoolMachine with random labels",
-			object: &clusterv1.Machine{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						"foo": "bar",
-					},
-				},
-			},
-			expected: false,
-		},
-		{
-			name:     "machine is not a MachinePoolMachine with no labels",
-			object:   &clusterv1.Machine{},
-			expected: false,
-		},
-	}
-
-	for i := range tests {
-		tt := tests[i]
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			result := IsMachinePoolOwned(tt.object)
-			g.Expect(result).To(Equal(tt.expected))
-		})
-	}
-}