Repository: kubernetes-sigs/cluster-api
Pull request #9703 (2023-11-10 01:37:33)

🌱  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 adds MachinePools to runtime SDK and rollout tests for ClusterClass. This will help unblock MachinePoolMachines which is affected by these tests not accounting for MachinePools in the quickstart spec. #8842 

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes part of #5991

<!-- 
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
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 | 4c2bcbcb7467eef58d60c312cc2002be31e33cb9 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9703/pull-cluster-api-e2e-full-main/1722790694671618048) | 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=9703). [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 -->
killianmuldoon: /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 | 990f115dbd09a507d5a472fdc592b5999775d268 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9703/pull-cluster-api-e2e-full-main/1722914544562475008) | 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=9703). [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 | 990f115dbd09a507d5a472fdc592b5999775d268 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9703/pull-cluster-api-e2e-full-main/1723055674105008128) | 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=9703). [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 | 4564e25033dde141d02ad9253994a2383b74ade5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9703/pull-cluster-api-e2e-full-main/1723141777151496192) | 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=9703). [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 -->
fabriziopandini: Some tests are failing 
(Note: I'm not sure if test failures are related to this change, but this should be investigated before reviewing this PR;
triggering re-run to rule out flakes)
/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 | 4564e25033dde141d02ad9253994a2383b74ade5 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/9703/pull-cluster-api-e2e-full-main/1724055116413145088) | 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=9703). [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 -->

Patch:
diff --git a/cmd/clusterctl/client/cluster/cert_manager.go b/cmd/clusterctl/client/cluster/cert_manager.go
index 982f17106..62752aa09 100644
--- a/cmd/clusterctl/client/cluster/cert_manager.go
+++ b/cmd/clusterctl/client/cluster/cert_manager.go
@@ -187,7 +187,7 @@ func (cm *certManagerClient) install(ctx context.Context) error {
 		o := objs[i]
 		// Create the Kubernetes object.
 		// Nb. The operation is wrapped in a retry loop to make ensureCerts more resilient to unexpected conditions.
-		if err := retryWithExponentialBackoff(ctx, createCertManagerBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(createCertManagerBackoff, func() error {
 			return cm.createObj(ctx, o)
 		}); err != nil {
 			return err
@@ -306,7 +306,7 @@ func (cm *certManagerClient) deleteObjs(ctx context.Context, objs []unstructured
 			continue
 		}
 
-		if err := retryWithExponentialBackoff(ctx, deleteCertManagerBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(deleteCertManagerBackoff, func() error {
 			if err := cm.deleteObj(ctx, obj); err != nil {
 				// tolerate NotFound errors when deleting the test resources
 				if apierrors.IsNotFound(err) {
@@ -560,7 +560,7 @@ func (cm *certManagerClient) waitForAPIReady(ctx context.Context, retry bool) er
 	deleteCertManagerBackoff := newWriteBackoff()
 	for i := range testObjs {
 		obj := testObjs[i]
-		if err := retryWithExponentialBackoff(ctx, deleteCertManagerBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(deleteCertManagerBackoff, func() error {
 			if err := cm.deleteObj(ctx, obj); err != nil {
 				// tolerate NotFound errors when deleting the test resources
 				if apierrors.IsNotFound(err) {
diff --git a/cmd/clusterctl/client/cluster/client.go b/cmd/clusterctl/client/cluster/client.go
index 9cc824fc1..84f3efda3 100644
--- a/cmd/clusterctl/client/cluster/client.go
+++ b/cmd/clusterctl/client/cluster/client.go
@@ -219,13 +219,13 @@ func newClusterClient(kubeconfig Kubeconfig, configClient config.Client, options
 }
 
 // retryWithExponentialBackoff repeats an operation until it passes or the exponential backoff times out.
-func retryWithExponentialBackoff(ctx context.Context, opts wait.Backoff, operation func(ctx context.Context) error) error {
+func retryWithExponentialBackoff(opts wait.Backoff, operation func() error) error {
 	log := logf.Log
 
 	i := 0
-	err := wait.ExponentialBackoffWithContext(ctx, opts, func(ctx context.Context) (bool, error) {
+	err := wait.ExponentialBackoff(opts, func() (bool, error) {
 		i++
-		if err := operation(ctx); err != nil {
+		if err := operation(); err != nil {
 			if i < opts.Steps {
 				log.V(5).Info("Retrying with backoff", "Cause", err.Error())
 				return false, nil
diff --git a/cmd/clusterctl/client/cluster/components.go b/cmd/clusterctl/client/cluster/components.go
index 3bc6a00e0..ae6800283 100644
--- a/cmd/clusterctl/client/cluster/components.go
+++ b/cmd/clusterctl/client/cluster/components.go
@@ -80,7 +80,7 @@ func (p *providerComponents) Create(ctx context.Context, objs []unstructured.Uns
 
 		// Create the Kubernetes object.
 		// Nb. The operation is wrapped in a retry loop to make Create more resilient to unexpected conditions.
-		if err := retryWithExponentialBackoff(ctx, createComponentObjectBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(createComponentObjectBackoff, func() error {
 			return p.createObj(ctx, obj)
 		}); err != nil {
 			return err
@@ -217,7 +217,7 @@ func (p *providerComponents) Delete(ctx context.Context, options DeleteOptions)
 		// Otherwise delete the object
 		log.V(5).Info("Deleting", logf.UnstructuredToValues(obj)...)
 		deleteBackoff := newWriteBackoff()
-		if err := retryWithExponentialBackoff(ctx, deleteBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(deleteBackoff, func() error {
 			if err := cs.Delete(ctx, &obj); err != nil {
 				if apierrors.IsNotFound(err) {
 					// Tolerate IsNotFound error that might happen because we are not enforcing a deletion order
diff --git a/cmd/clusterctl/client/cluster/crd_migration.go b/cmd/clusterctl/client/cluster/crd_migration.go
index cd770bd79..c7fee727d 100644
--- a/cmd/clusterctl/client/cluster/crd_migration.go
+++ b/cmd/clusterctl/client/cluster/crd_migration.go
@@ -88,7 +88,7 @@ func (m *crdMigrator) run(ctx context.Context, newCRD *apiextensionsv1.CustomRes
 
 	// Get the current CRD.
 	currentCRD := &apiextensionsv1.CustomResourceDefinition{}
-	if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(newReadBackoff(), func() error {
 		return m.Client.Get(ctx, client.ObjectKeyFromObject(newCRD), currentCRD)
 	}); err != nil {
 		// Return if the CRD doesn't exist yet. We only have to migrate if the CRD exists already.
@@ -152,7 +152,7 @@ func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextens
 
 	var i int
 	for {
-		if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(newReadBackoff(), func() error {
 			return m.Client.List(ctx, list, client.Continue(list.GetContinue()))
 		}); err != nil {
 			return errors.Wrapf(err, "failed to list %q", list.GetKind())
@@ -162,7 +162,7 @@ func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextens
 			obj := list.Items[i]
 
 			log.V(5).Info("Migrating", logf.UnstructuredToValues(obj)...)
-			if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
+			if err := retryWithExponentialBackoff(newWriteBackoff(), func() error {
 				return handleMigrateErr(m.Client.Update(ctx, &obj))
 			}); err != nil {
 				return errors.Wrapf(err, "failed to migrate %s/%s", obj.GetNamespace(), obj.GetName())
@@ -187,7 +187,7 @@ func (m *crdMigrator) migrateResourcesForCRD(ctx context.Context, crd *apiextens
 
 func (m *crdMigrator) patchCRDStoredVersions(ctx context.Context, crd *apiextensionsv1.CustomResourceDefinition, currentStorageVersion string) error {
 	crd.Status.StoredVersions = []string{currentStorageVersion}
-	if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(newWriteBackoff(), func() error {
 		return m.Client.Status().Update(ctx, crd)
 	}); err != nil {
 		return errors.Wrapf(err, "failed to update status.storedVersions for CRD %q", crd.Name)
diff --git a/cmd/clusterctl/client/cluster/inventory.go b/cmd/clusterctl/client/cluster/inventory.go
index 4bf8df498..8412e78c6 100644
--- a/cmd/clusterctl/client/cluster/inventory.go
+++ b/cmd/clusterctl/client/cluster/inventory.go
@@ -162,7 +162,7 @@ func (p *inventoryClient) EnsureCustomResourceDefinitions(ctx context.Context) e
 	// Nb. The operation is wrapped in a retry loop to make EnsureCustomResourceDefinitions more resilient to unexpected conditions.
 	var crdIsIstalled bool
 	listInventoryBackoff := newReadBackoff()
-	if err := retryWithExponentialBackoff(ctx, listInventoryBackoff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(listInventoryBackoff, func() error {
 		var err error
 		crdIsIstalled, err = checkInventoryCRDs(ctx, p.proxy)
 		return err
@@ -189,7 +189,7 @@ func (p *inventoryClient) EnsureCustomResourceDefinitions(ctx context.Context) e
 
 		// Create the Kubernetes object.
 		// Nb. The operation is wrapped in a retry loop to make EnsureCustomResourceDefinitions more resilient to unexpected conditions.
-		if err := retryWithExponentialBackoff(ctx, createInventoryObjectBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(createInventoryObjectBackoff, func() error {
 			return p.createObj(ctx, o)
 		}); err != nil {
 			return err
@@ -272,7 +272,7 @@ func (p *inventoryClient) createObj(ctx context.Context, o unstructured.Unstruct
 func (p *inventoryClient) Create(ctx context.Context, m clusterctlv1.Provider) error {
 	// Create the Kubernetes object.
 	createInventoryObjectBackoff := newWriteBackoff()
-	return retryWithExponentialBackoff(ctx, createInventoryObjectBackoff, func(ctx context.Context) error {
+	return retryWithExponentialBackoff(createInventoryObjectBackoff, func() error {
 		cl, err := p.proxy.NewClient()
 		if err != nil {
 			return err
@@ -310,7 +310,7 @@ func (p *inventoryClient) List(ctx context.Context) (*clusterctlv1.ProviderList,
 	providerList := &clusterctlv1.ProviderList{}
 
 	listProvidersBackoff := newReadBackoff()
-	if err := retryWithExponentialBackoff(ctx, listProvidersBackoff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(listProvidersBackoff, func() error {
 		return listProviders(ctx, p.proxy, providerList)
 	}); err != nil {
 		return nil, err
diff --git a/cmd/clusterctl/client/cluster/mover.go b/cmd/clusterctl/client/cluster/mover.go
index ea9c742ab..870687999 100644
--- a/cmd/clusterctl/client/cluster/mover.go
+++ b/cmd/clusterctl/client/cluster/mover.go
@@ -234,7 +234,7 @@ func (o *objectMover) checkProvisioningCompleted(ctx context.Context, graph *obj
 	for i := range clusters {
 		cluster := clusters[i]
 		clusterObj := &clusterv1.Cluster{}
-		if err := retryWithExponentialBackoff(ctx, readClusterBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(readClusterBackoff, func() error {
 			return getClusterObj(ctx, o.fromProxy, cluster, clusterObj)
 		}); err != nil {
 			return err
@@ -264,7 +264,7 @@ func (o *objectMover) checkProvisioningCompleted(ctx context.Context, graph *obj
 	for i := range machines {
 		machine := machines[i]
 		machineObj := &clusterv1.Machine{}
-		if err := retryWithExponentialBackoff(ctx, readMachinesBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(readMachinesBackoff, func() error {
 			return getMachineObj(ctx, o.fromProxy, machine, machineObj)
 		}); err != nil {
 			return err
@@ -574,7 +574,7 @@ func setClusterPause(ctx context.Context, proxy Proxy, clusters []*node, value b
 		log.V(5).Info("Set Cluster.Spec.Paused", "paused", value, "Cluster", klog.KRef(cluster.identity.Namespace, cluster.identity.Name))
 
 		// Nb. The operation is wrapped in a retry loop to make setClusterPause more resilient to unexpected conditions.
-		if err := retryWithExponentialBackoff(ctx, setClusterPauseBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(setClusterPauseBackoff, func() error {
 			return patchCluster(ctx, proxy, cluster, patch, mutators...)
 		}); err != nil {
 			return errors.Wrapf(err, "error setting Cluster.Spec.Paused=%t", value)
@@ -601,7 +601,7 @@ func setClusterClassPause(ctx context.Context, proxy Proxy, clusterclasses []*no
 		}
 
 		// Nb. The operation is wrapped in a retry loop to make setClusterClassPause more resilient to unexpected conditions.
-		if err := retryWithExponentialBackoff(ctx, setClusterClassPauseBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(setClusterClassPauseBackoff, func() error {
 			return pauseClusterClass(ctx, proxy, clusterclass, pause, mutators...)
 		}); err != nil {
 			return errors.Wrapf(err, "error updating ClusterClass %s/%s", clusterclass.identity.Namespace, clusterclass.identity.Name)
@@ -648,7 +648,7 @@ func waitReadyForMove(ctx context.Context, proxy Proxy, nodes []*node, dryRun bo
 		key := client.ObjectKeyFromObject(obj)
 
 		blockLogged := false
-		if err := retryWithExponentialBackoff(ctx, backoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(backoff, func() error {
 			if err := c.Get(ctx, key, obj); err != nil {
 				return errors.Wrapf(err, "error getting %s/%s", obj.GroupVersionKind(), key)
 			}
@@ -788,7 +788,7 @@ func (o *objectMover) ensureNamespaces(ctx context.Context, graph *objectGraph,
 		}
 		namespaces.Insert(namespace)
 
-		if err := retryWithExponentialBackoff(ctx, ensureNamespaceBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(ensureNamespaceBackoff, func() error {
 			return o.ensureNamespace(ctx, toProxy, namespace)
 		}); err != nil {
 			return err
@@ -872,7 +872,7 @@ func (o *objectMover) createGroup(ctx context.Context, group moveGroup, toProxy
 	for _, nodeToCreate := range group {
 		// Creates the Kubernetes object corresponding to the nodeToCreate.
 		// Nb. The operation is wrapped in a retry loop to make move more resilient to unexpected conditions.
-		err := retryWithExponentialBackoff(ctx, createTargetObjectBackoff, func(ctx context.Context) error {
+		err := retryWithExponentialBackoff(createTargetObjectBackoff, func() error {
 			return o.createTargetObject(ctx, nodeToCreate, toProxy, mutators, existingNamespaces)
 		})
 		if err != nil {
@@ -894,7 +894,7 @@ func (o *objectMover) backupGroup(ctx context.Context, group moveGroup, director
 	for _, nodeToBackup := range group {
 		// Backs-up the Kubernetes object corresponding to the nodeToBackup.
 		// Nb. The operation is wrapped in a retry loop to make move more resilient to unexpected conditions.
-		err := retryWithExponentialBackoff(ctx, backupTargetObjectBackoff, func(ctx context.Context) error {
+		err := retryWithExponentialBackoff(backupTargetObjectBackoff, func() error {
 			return o.backupTargetObject(ctx, nodeToBackup, directory)
 		})
 		if err != nil {
@@ -916,7 +916,7 @@ func (o *objectMover) restoreGroup(ctx context.Context, group moveGroup, toProxy
 	for _, nodeToRestore := range group {
 		// Creates the Kubernetes object corresponding to the nodeToRestore.
 		// Nb. The operation is wrapped in a retry loop to make move more resilient to unexpected conditions.
-		err := retryWithExponentialBackoff(ctx, restoreTargetObjectBackoff, func(ctx context.Context) error {
+		err := retryWithExponentialBackoff(restoreTargetObjectBackoff, func() error {
 			return o.restoreTargetObject(ctx, nodeToRestore, toProxy)
 		})
 		if err != nil {
@@ -1174,7 +1174,7 @@ func (o *objectMover) deleteGroup(ctx context.Context, group moveGroup) error {
 
 		// Delete the Kubernetes object corresponding to the current node.
 		// Nb. The operation is wrapped in a retry loop to make move more resilient to unexpected conditions.
-		err := retryWithExponentialBackoff(ctx, deleteSourceObjectBackoff, func(ctx context.Context) error {
+		err := retryWithExponentialBackoff(deleteSourceObjectBackoff, func() error {
 			return o.deleteSourceObject(ctx, nodeToDelete)
 		})
 
diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go
index 360ca8999..d72a69ae9 100644
--- a/cmd/clusterctl/client/cluster/objectgraph.go
+++ b/cmd/clusterctl/client/cluster/objectgraph.go
@@ -333,7 +333,7 @@ func (o *objectGraph) objMetaToNode(obj *unstructured.Unstructured, n *node) {
 func (o *objectGraph) getDiscoveryTypes(ctx context.Context) error {
 	crdList := &apiextensionsv1.CustomResourceDefinitionList{}
 	getDiscoveryTypesBackoff := newReadBackoff()
-	if err := retryWithExponentialBackoff(ctx, getDiscoveryTypesBackoff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(getDiscoveryTypesBackoff, func() error {
 		return getCRDList(ctx, o.proxy, crdList)
 	}); err != nil {
 		return err
@@ -432,7 +432,7 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
 		typeMeta := discoveryType.typeMeta
 		objList := new(unstructured.UnstructuredList)
 
-		if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(discoveryBackoff, func() error {
 			return getObjList(ctx, o.proxy, typeMeta, selectors, objList)
 		}); err != nil {
 			return err
@@ -448,7 +448,7 @@ func (o *objectGraph) Discovery(ctx context.Context, namespace string) error {
 				if p.Type == string(clusterctlv1.InfrastructureProviderType) {
 					providerNamespaceSelector := []client.ListOption{client.InNamespace(p.Namespace)}
 					providerNamespaceSecretList := new(unstructured.UnstructuredList)
-					if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
+					if err := retryWithExponentialBackoff(discoveryBackoff, func() error {
 						return getObjList(ctx, o.proxy, typeMeta, providerNamespaceSelector, providerNamespaceSecretList)
 					}); err != nil {
 						return err
diff --git a/cmd/clusterctl/client/cluster/ownergraph.go b/cmd/clusterctl/client/cluster/ownergraph.go
index e3e2553e9..1a9e867d3 100644
--- a/cmd/clusterctl/client/cluster/ownergraph.go
+++ b/cmd/clusterctl/client/cluster/ownergraph.go
@@ -76,7 +76,7 @@ func discoverOwnerGraph(ctx context.Context, namespace string, o *objectGraph) (
 		typeMeta := discoveryType.typeMeta
 		objList := new(unstructured.UnstructuredList)
 
-		if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
+		if err := retryWithExponentialBackoff(discoveryBackoff, func() error {
 			return getObjList(ctx, o.proxy, typeMeta, selectors, objList)
 		}); err != nil {
 			return nil, err
@@ -92,7 +92,7 @@ func discoverOwnerGraph(ctx context.Context, namespace string, o *objectGraph) (
 				if p.Type == string(clusterctlv1.InfrastructureProviderType) {
 					providerNamespaceSelector := []client.ListOption{client.InNamespace(p.Namespace)}
 					providerNamespaceSecretList := new(unstructured.UnstructuredList)
-					if err := retryWithExponentialBackoff(ctx, discoveryBackoff, func(ctx context.Context) error {
+					if err := retryWithExponentialBackoff(discoveryBackoff, func() error {
 						return getObjList(ctx, o.proxy, typeMeta, providerNamespaceSelector, providerNamespaceSecretList)
 					}); err != nil {
 						return nil, err
diff --git a/cmd/clusterctl/client/cluster/proxy.go b/cmd/clusterctl/client/cluster/proxy.go
index c370eb270..9337b802e 100644
--- a/cmd/clusterctl/client/cluster/proxy.go
+++ b/cmd/clusterctl/client/cluster/proxy.go
@@ -165,7 +165,7 @@ func (k *proxy) NewClient() (client.Client, error) {
 	var c client.Client
 	// Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems.
 	connectBackoff := newConnectBackoff()
-	if err := retryWithExponentialBackoff(context.TODO(), connectBackoff, func(_ context.Context) error {
+	if err := retryWithExponentialBackoff(connectBackoff, func() error {
 		var err error
 		c, err = client.New(config, client.Options{Scheme: localScheme})
 		if err != nil {
@@ -191,7 +191,7 @@ func (k *proxy) CheckClusterAvailable() error {
 	}
 
 	connectBackoff := newShortConnectBackoff()
-	return retryWithExponentialBackoff(context.TODO(), connectBackoff, func(_ context.Context) error {
+	return retryWithExponentialBackoff(connectBackoff, func() error {
 		_, err := client.New(config, client.Options{Scheme: localScheme})
 		return err
 	})
@@ -224,7 +224,7 @@ func (k *proxy) ListResources(ctx context.Context, labels map[string]string, nam
 	// Get all the API resources in the cluster.
 	resourceListBackoff := newReadBackoff()
 	var resourceList []*metav1.APIResourceList
-	if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(resourceListBackoff, func() error {
 		resourceList, err = cs.Discovery().ServerPreferredResources()
 		return err
 	}); err != nil {
@@ -236,7 +236,7 @@ func (k *proxy) ListResources(ctx context.Context, labels map[string]string, nam
 	crdsToExclude := sets.Set[string]{}
 
 	crdList := &apiextensionsv1.CustomResourceDefinitionList{}
-	if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(newReadBackoff(), func() error {
 		return c.List(ctx, crdList)
 	}); err != nil {
 		return nil, errors.Wrap(err, "failed to list CRDs")
@@ -348,7 +348,7 @@ func listObjByGVK(ctx context.Context, c client.Client, groupVersion, kind strin
 	objList.SetKind(kind)
 
 	resourceListBackoff := newReadBackoff()
-	if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(resourceListBackoff, func() error {
 		return c.List(ctx, objList, options...)
 	}); err != nil {
 		return nil, errors.Wrapf(err, "failed to list objects for the %q GroupVersionKind", objList.GroupVersionKind())
@@ -402,7 +402,7 @@ func (k *proxy) newClientSet() (*kubernetes.Clientset, error) {
 	var cs *kubernetes.Clientset
 	// Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems.
 	connectBackoff := newConnectBackoff()
-	if err := retryWithExponentialBackoff(context.TODO(), connectBackoff, func(_ context.Context) error {
+	if err := retryWithExponentialBackoff(connectBackoff, func() error {
 		var err error
 		cs, err = kubernetes.NewForConfig(config)
 		if err != nil {
diff --git a/cmd/clusterctl/client/cluster/upgrader.go b/cmd/clusterctl/client/cluster/upgrader.go
index f640dfb24..f5fd8b6e3 100644
--- a/cmd/clusterctl/client/cluster/upgrader.go
+++ b/cmd/clusterctl/client/cluster/upgrader.go
@@ -484,7 +484,7 @@ func (u *providerUpgrader) scaleDownProvider(ctx context.Context, provider clust
 
 // scaleDownDeployment scales down a Deployment to 0 and waits until all replicas have been deleted.
 func scaleDownDeployment(ctx context.Context, c client.Client, deploy appsv1.Deployment) error {
-	if err := retryWithExponentialBackoff(ctx, newWriteBackoff(), func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(newWriteBackoff(), func() error {
 		deployment := &appsv1.Deployment{}
 		if err := c.Get(ctx, client.ObjectKeyFromObject(&deploy), deployment); err != nil {
 			return errors.Wrapf(err, "failed to get Deployment/%s", deploy.GetName())
@@ -511,7 +511,7 @@ func scaleDownDeployment(ctx context.Context, c client.Client, deploy appsv1.Dep
 		Steps:    60,
 		Jitter:   0.4,
 	}
-	if err := retryWithExponentialBackoff(ctx, deploymentScaleToZeroBackOff, func(ctx context.Context) error {
+	if err := retryWithExponentialBackoff(deploymentScaleToZeroBackOff, func() error {
 		deployment := &appsv1.Deployment{}
 		if err := c.Get(ctx, client.ObjectKeyFromObject(&deploy), deployment); err != nil {
 			return errors.Wrapf(err, "failed to get Deployment/%s", deploy.GetName())
diff --git a/docs/book/src/reference/glossary.md b/docs/book/src/reference/glossary.md
index 6b58ead3d..7c21c9211 100644
--- a/docs/book/src/reference/glossary.md
+++ b/docs/book/src/reference/glossary.md
@@ -26,9 +26,9 @@ A temporary cluster that is used to provision a Target Management cluster.
 ### Bootstrap provider
 
 Refers to a [provider](#provider) that implements a solution for the [bootstrap](#bootstrap) process.
-Bootstrap provider's interaction with Cluster API is based on what is defined in the [Cluster API contract](#contract).
+Bootstrap provider's interaction with Cluster API is based on what is defined in the [Cluster API contract](#contract). 
 
-See [CABPK](#cabpk).
+See [CABPK](#cabpk). 
 
 # C
 ---
@@ -132,12 +132,6 @@ See [core provider](#core-provider)
 
 The Cluster API execution model, a set of controllers cooperating in managing the Kubernetes cluster lifecycle.
 
-### Cluster Infrastructure
-
-or __Kubernetes Cluster Infrastructure__
-
-Defines the **infrastructure that supports a Kubernetes cluster**, like e.g. VPC, security groups, load balancers, etc. Please note that in the context of managed Kubernetes some of those components are going to be provided by the corresponding abstraction for a specific Cloud provider (EKS, OKE, AKS etc), and thus Cluster API should not take care of managing a subset or all those components.
-
 ### Contract
 
 Or __Cluster API contract__
@@ -161,7 +155,7 @@ See [KCP](#kcp).
 
 ### Core provider
 
-Refers to a [provider](#provider) that implements Cluster API core controllers; if you
+Refers to a [provider](#provider) that implements Cluster API core controllers; if you 
 consider that the first project that must be deployed in a management Cluster is Cluster API itself, it should be clear why
 the Cluster API project is also referred to as the core provider.
 
@@ -202,7 +196,7 @@ see [Server](#server)
 
 ### Infrastructure provider
 
-Refers to a [provider](#provider) that implements provisioning of infrastructure/computational resources required by
+Refers to a [provider](#provider) that implements provisioning of infrastructure/computational resources required by 
 the Cluster or by Machines (e.g. VMs, networking, etc.).
 Infrastructure provider's interaction with Cluster API is based on what is defined in the [Cluster API contract](#contract).
 
@@ -211,7 +205,7 @@ When there is more than one way to obtain resources from the same infrastructure
 
 For a complete list of providers see [Provider Implementations](providers.md).
 
-### Inline patch
+### Inline patch 
 
 A [patch](#patch) defined inline in a [ClusterClass](#clusterclass). An alternative to an [external patch](#external-patch).
 
@@ -275,10 +269,6 @@ See also: [Server](#server)
 
 Perform create, scale, upgrade, or destroy operations on the cluster.
 
-### Managed Kubernetes
-
-Managed Kubernetes refers to any Kubernetes cluster provisioning and maintenance abstraction, usually exposed as an API, that is natively available in a Cloud provider. For example: [EKS](https://aws.amazon.com/eks/), [OKE](https://www.oracle.com/cloud/cloud-native/container-engine-kubernetes/), [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service), [GKE](https://cloud.google.com/kubernetes-engine), [IBM Cloud Kubernetes Service](https://www.ibm.com/cloud/kubernetes-service), [DOKS](https://www.digitalocean.com/products/kubernetes), and many more throughout the Kubernetes Cloud Native ecosystem.
-
 ### Managed Topology
 
 See [Topology](#topology)
@@ -316,7 +306,7 @@ A generically understood combination of a kernel and system-level userspace inte
 # P
 ---
 
-### Patch
+### Patch 
 
 A set of instructions describing modifications to a Kubernetes object. Examples include JSON Patch and JSON Merge Patch.
 
diff --git a/docs/proposals/20220725-managed-kubernetes.md b/docs/proposals/20220725-managed-kubernetes.md
index 5191e3e22..c77215da9 100644
--- a/docs/proposals/20220725-managed-kubernetes.md
+++ b/docs/proposals/20220725-managed-kubernetes.md
@@ -14,9 +14,9 @@ reviewers:
   - "@shyamradhakrishnan"
   - "@yastij"
 creation-date: 2022-07-25
-last-updated: 2023-06-15
+last-updated: 2022-08-23
 status: implementable
-see-also: ./20230407-flexible-managed-k8s-endpoints.md
+see-also:
 replaces:
 superseded-by:
 ---
@@ -50,14 +50,13 @@ superseded-by:
     - [EKS in CAPA](#eks-in-capa)
     - [AKS in CAPZ](#aks-in-capz)
     - [OKE in CAPOCI](#oke-in-capoci)
-    - [GKE in CAPG](#gke-in-capg)
   - [Managed Kubernetes API Design Approaches](#managed-kubernetes-api-design-approaches)
-    - [Option 1: Two kinds with a ControlPlane and a pass-through InfraCluster](#option-1-two-kinds-with-a-controlplane-and-a-pass-through-infracluster)
-    - [Option 2: Just a ControlPlane kind and no InfraCluster](#option-2-just-a-controlplane-kind-and-no-infracluster)
+    - [Option 1: Single kind for Control Plane and Infrastructure](#option-1-single-kind-for-control-plane-and-infrastructure)
+      - [Background: Why did EKS in CAPA choose this option?](#background-why-did-eks-in-capa-choose-this-option)
+    - [Option 2: Two kinds with a ControlPlane and a pass-through InfraCluster](#option-2-two-kinds-with-a-controlplane-and-a-pass-through-infracluster)
     - [Option 3: Two kinds with a Managed Control Plane and Managed Infra Cluster with Better Separation of Responsibilities](#option-3-two-kinds-with-a-managed-control-plane-and-managed-infra-cluster-with-better-separation-of-responsibilities)
+    - [Option 4: Two kinds with a Managed Control Plane and Shared Infra Cluster with Better Separation of Responsibilities](#option-4-two-kinds-with-a-managed-control-plane-and-shared-infra-cluster-with-better-separation-of-responsibilities)
 - [Recommendations](#recommendations)
-  - [Vanilla Managed Kubernetes (i.e. without any additional infrastructure)](#vanilla-managed-kubernetes-ie-without-any-additional-infrastructure)
-  - [Existing Managed Kubernetes Implementations](#existing-managed-kubernetes-implementations)
   - [Additional notes on option 3](#additional-notes-on-option-3)
   - [Managed Node Groups for Worker Nodes](#managed-node-groups-for-worker-nodes)
   - [Provider Implementers Documentation](#provider-implementers-documentation)
@@ -65,21 +64,17 @@ superseded-by:
   - [ClusterClass support for MachinePool](#clusterclass-support-for-machinepool)
   - [clusterctl integration](#clusterctl-integration)
   - [Add-ons management](#add-ons-management)
-- [Alternatives](#alternatives)
-  - [Alternative 1: Single kind for Control Plane and Infrastructure](#alternative-1-single-kind-for-control-plane-and-infrastructure)
-    - [Background: Why did EKS in CAPA choose this option?](#background-why-did-eks-in-capa-choose-this-option)
-  - [Alternative 2: Two kinds with a Managed Control Plane and Shared Infra Cluster with Better Separation of Responsibilities](#alternative-2-two-kinds-with-a-managed-control-plane-and-shared-infra-cluster-with-better-separation-of-responsibilities)
 - [Upgrade Strategy](#upgrade-strategy)
 - [Implementation History](#implementation-history)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
+  
 ## Glossary
 
 - **Managed Kubernetes** - a Kubernetes service offered/hosted by a service provider where the control plane is run & managed by the service provider. As a cluster service consumer, you don’t have to worry about managing/operating the control plane machines. Additionally, the managed Kubernetes service may extend to cover running managed worker nodes. Examples are EKS in AWS and AKS in Azure. This is different from a traditional implementation in Cluster API, where the control plane and worker nodes are deployed and managed by the cluster admin.
 - **Unmanaged Kubernetes** - a Kubernetes cluster where a cluster admin is responsible for provisioning and operating the control plane and worker nodes. In Cluster API this traditionally means a Kubeadm bootstrapped cluster on infrastructure machines (virtual or physical).
 - **Managed Worker Node** - an individual Kubernetes worker node where the underlying compute (vm or bare-metal) is provisioned and managed by the service provider. This usually includes the joining of the newly provisioned node into a Managed Kubernetes cluster. The lifecycle is normally controlled via a higher level construct such as a Managed Node Group.
-- **Managed Node Group** - is a service that a service provider offers that automates the provisioning of managed worker nodes. Depending on the service provider this group of nodes could contain a fixed number of replicas or it might contain a dynamic pool of replicas that auto-scales up and down. Examples are Node Pools in GCP and EKS managed node groups.
+- **Managed Node Group** - is a service that a service provider offers that automates the provisioning of managed worker nodes. Depending on the service provider this group of nodes could contain a fixed number of replicas or it might contain a dynamic pool of replicas that auto-scales up and down. Examples are Node Pools in GCP and EKS managed node groups.  
 - **Cluster Infrastructure Provider (Infrastructure)** - an Infrastructure provider supplies whatever prerequisites are necessary for creating & running clusters such as networking, load balancers, firewall rules, and so on. ([docs](../book/src/developer/providers/cluster-infrastructure.md))
 - **ControlPlane Provider (ControlPlane)** - a control plane provider instantiates a Kubernetes control plane consisting of k8s control plane components such as kube-apiserver, etcd, kube-scheduler and kube-controller-manager. ([docs](../book/src/developer/architecture/controllers/control-plane.md#control-plane-provider))
 - **MachineDeployment** - a MachineDeployment orchestrates deployments over a fleet of MachineSets, which is an immutable abstraction over Machines. ([docs](../book/src/developer/architecture/controllers/machine-deployment.md))
@@ -95,9 +90,7 @@ Cluster API was originally designed with unmanaged Kubernetes clusters in mind a
 
 Some Cluster API Providers (i.e. Azure with AKS first and then AWS with EKS) have implemented support for their managed Kubernetes services. These implementations have followed the existing documentation & contracts (that were designed for unmanaged Kubernetes) and have ended up with 2 different implementations.
 
-While working on supporting ClusterClass for EKS in Cluster API Provider AWS (CAPA), it was discovered that the current implementation of EKS within CAPA, where a single resource kind (AWSManagedControlPlane) is used for both ControlPlane and Infrastructure, is incompatible with other parts of CAPI assuming the two objects are different (Reference [issue here](https://github.com/kubernetes-sigs/cluster-api/issues/6126)).
-
-Separation of ControlPlane and Infrastructure is expected for the ClusterClass implementation to work correctly. However, after the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) have been implemented there is the option to supply only the control plane, but you still cannot supply the same resource for both.
+While working on supporting ClusterClass for EKS in Cluster API Provider AWS (CAPA), it was discovered that the current implementation of EKS within CAPA, where a single resource kind (AWSManagedControlPlane) is used for both ControlPlane and Infrastructure, is incompatible with ClusterClass (See the [issue](https://github.com/kubernetes-sigs/cluster-api/issues/6126)). Separation of ControlPlane and Infrastructure is expected for the ClusterClass implementation to work correctly.
 
 The responsibilities between the CAPI control plane and infrastructure are blurred with a managed Kubernetes service like AKS or EKS. For example, when you create a EKS control plane in AWS it also creates infrastructure that CAPI would traditionally view as the responsibility of the cluster “infrastructure provider”.
 
@@ -118,8 +111,9 @@ A good example here is the API server load balancer:
 - Enforce the Managed Kubernetes recommendations as a requirement for Cluster API providers when they implement Managed Kubernetes.
   - If providers that have already implemented Managed Kubernetes and would like guidance on if/how they could move to be aligned with the recommendations of this proposal then discussions should be facilitated.
 - Provide advice in this proposal on how to refactor the existing implementations of managed Kubernetes in CAPA & CAPZ.
-- Propose a new architecture or API changes to CAPI for managed Kubernetes. This has been covered by the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md).
+- Propose a new architecture or API changes to CAPI for managed Kubernetes
 - Be a concrete design for the GKE implementation in Cluster API Provider GCP (CAPG).
+  - A separate CAPG proposal will be created for GKE implementation based on the recommendations of this proposal.
 - Recommend how Managed Kubernetes services would leverage CAPI internally to run their offer.
 
 ## Proposal
@@ -209,7 +203,7 @@ So that I can eliminate the responsibility of owning and SREing the Control Plan
 #### AKS in CAPZ
 
 - [Docs](https://capz.sigs.k8s.io/topics/managedcluster.html)
-- Feature Status: GA
+- Feature Status: Experimental
 - CRDs
   - AzureManagedControlPlane, AzureManagedCluster - provision AKS cluster
   - AzureManagedMachinePool - corresponds to AKS node pool
@@ -218,41 +212,22 @@ So that I can eliminate the responsibility of owning and SREing the Control Plan
 
 #### OKE in CAPOCI
 
-- [Docs](https://oracle.github.io/cluster-api-provider-oci/managed/managedcluster.html)
-- Feature Status: Experimental
-- CRDs
-  - OCIManagedControlPlane, OCIManagedCluster - provision OKE cluster
-  - OCIManagedMachinePool, OCIVirtualMachinePool - machine pool implementations
-- Supported Flavors:
-  - OCIManagedControlPlane + OCIManagedCluster with OCIManagedMachinePool
-  - OCIManagedControlPlane + OCIManagedCluster with OCIVirtualMachinePool
-
-#### GKE in CAPG
-
-- [Docs](https://github.com/kubernetes-sigs/cluster-api-provider-gcp/blob/main/docs/book/src/topics/gke/index.md)
-- Feature Status: Experimental
-- CRDs
-  - GCPManagedControlPlane, GCPManagedCluster - provision GKE cluster
-  - GCPManagedMachinePool - corresponds to managed node pool
-- Support flavor
-  - GCPManagedControlPlane + GCPManagedCluster with GCPManagedMachinePool
+- [Issue](https://github.com/oracle/cluster-api-provider-oci/issues/110)
+- Design discussion starting
 
 ### Managed Kubernetes API Design Approaches
 
-When discussing the different approaches to represent a managed Kubernetes service in CAPI, we will be using the implementation of GKE support in CAPG as an example.
+When discussing the different approaches to represent a managed Kubernetes service in CAPI, we will be using the implementation of GKE support in CAPG as an example, as this isn’t currently implemented.
 
-> NOTE: “naming things is hard” so the names of the kinds/structs/fields used in the CAPG examples below are illustrative only and are not the focus of this proposal. There is debate, for example, as to whether `GCPManagedCluster` or `GKECluster` should be used.
+> NOTE: “naming things is hard” so the names of the kinds/structs/fields used in the CAPG examples below are illustrative only and are not the focus of this proposal. There is debate, for example, as to whether `GCPManagedCluster` or `GKECluster` should be used. This type of discussion will be within the CAPG proposal.
 
 The following section discusses different API implementation options along with pros and cons of each.
 
-#### Option 1: Two kinds with a ControlPlane and a pass-through InfraCluster
-
-**This option will be no longer needed when the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) have been implemented as option 2 can be used for a simpler solution**
+#### Option 1: Single kind for Control Plane and Infrastructure
 
-This option introduces 2 new resource kinds:
+This option introduces a new single resource kind:
 
 - **GCPManagedControlPlane**: this represents both a control-plane (i.e. GKE) and infrastructure required for the cluster. It contains properties for both the general cloud infrastructure (that would traditionally be represented by an infrastructure cluster) and the managed Kubernetes control plane (that would traditionally be represented by a control plane provider).
-- **GCPManagedCluster**: contains the minimum properties in its spec and status to satisfy the [CAPI contract for an infrastructure cluster](../book/src/developer/providers/cluster-infrastructure.md) (i.e. ControlPlaneEndpoint, Ready condition). Its controller watches GCPManagedControlPlane and copies the ControlPlaneEndpoint field to GCPManagedCluster to report back to CAPI. This is used as a pass-through layer only.
 
 ```go
 type GCPManagedControlPlaneSpec struct {
@@ -263,7 +238,7 @@ type GCPManagedControlPlaneSpec struct {
     // +optional
     Network NetworkSpec `json:"network"`
 
-    // AddonsConfig defines the addons to enable with the GKE cluster.
+    // AddonsConfig defines the addons to enable with the GKE cluster.  
     // +optional
     AddonsConfig *AddonsConfig `json:"addonsConfig,omitempty"`
 
@@ -282,39 +257,44 @@ type GCPManagedControlPlaneSpec struct {
 }
 ```
 
-```go
-type GCPManagedClusterSpec struct {
-    // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
-    // +optional
-    ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
-}
-```
+**This is the design pattern used by EKS in CAPA.**
+
+##### Background: Why did EKS in CAPA choose this option?
+
+CAPA decided to represent an EKS cluster as a CAPI control-plane. This meant that control-plane is responsible for creating the API server load balancer.
+
+Initially CAPA had an infrastructure cluster kind that reported back the control plane endpoint. This required less than ideal code in its controller to watch the control plane and use its value of the control plane endpoint.
+
+As the infrastructure cluster kind only acted as a passthrough (to satisfy the contract with CAPI) it was decided that it would be removed and the control-plane kind (AWSManagedControlPlane) could be used to satisfy both the “infrastructure” and “control-plane” contracts. This worked well until ClusterClass arrived with its expectation that the “infrastructure” and “control-plane” are 2 different resource kinds.
 
-**This is the design pattern currently used by CAPZ and CAPA**. [An example of how ManagedCluster watches ControlPlane in CAPZ.](https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/5c69b44ed847365525504b242da83b5e5da75e4f/controllers/azuremanagedcluster_controller.go#L71)
+Note that CAPZ had a similar discussion and an [issue](https://github.com/kubernetes-sigs/cluster-api-provider-azure/issues/1396) to remove AzureManagedCluster: AzureManagedCluster is useless; let's remove it (and keep AzureManagedControlPlane)
 
 **Pros**
 
-- Better aligned with CAPI’s traditional infra provider model
-- Works with ClusterClass
+- A simple design with a single resource kind and controller.
 
 **Cons**
 
-- Need to maintain Infra cluster kind, which is a pass-through layer and has no other functions. In addition to the CRD, controllers, webhooks and conversions webhooks need to be maintained.
-- Infra provider doesn’t provision infrastructure and whilst it may meet the CAPI contract, it doesn’t actually create infrastructure as this is done via the control plane.
-
-#### Option 2: Just a ControlPlane kind and no InfraCluster
+- Doesn’t work with the current implementation of ClusterClass, which expects a separation of ControlPlane and Infrastructure.
+- Doesn’t provide separation of responsibilities between creating the general cloud infrastructure for the cluster and the actual cluster control plane.
+- Managed Kubernetes look different from unmanaged Kubernetes where two separate kinds are used for a control plane and infrastructure. This would impact products building on top of CAPI.
 
-**This option is enabled when the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) have been implemented.**
+#### Option 2: Two kinds with a ControlPlane and a pass-through InfraCluster
 
-This option introduces 1 new resource kind:
+This option introduces 2 new resource kinds:
 
-- **GCPManagedControlPlane**: this represents a control-plane (i.e. GKE) required for the cluster. It contains properties for the managed Kubernetes control plane (that would traditionally be represented by a control plane provider).
+- **GCPManagedControlPlane**: same as in option 1
+- **GCPManagedCluster**: contains the minimum properties in its spec and status to satisfy the [CAPI contract for an infrastructure cluster](../book/src/developer/providers/cluster-infrastructure.md) (i.e. ControlPlaneEndpoint, Ready condition). Its controller watches GCPManagedControlPlane and copies the ControlPlaneEndpoint field to GCPManagedCluster to report back to CAPI. This is used as a pass-through layer only.
 
 ```go
 type GCPManagedControlPlaneSpec struct {
     // Project is the name of the project to deploy the cluster to.
     Project string `json:"project"`
 
+    // NetworkSpec encapsulates all things related to the GCP network.
+    // +optional
+    Network NetworkSpec `json:"network"`
+
     // AddonsConfig defines the addons to enable with the GKE cluster.
     // +optional
     AddonsConfig *AddonsConfig `json:"addonsConfig,omitempty"`
@@ -334,15 +314,25 @@ type GCPManagedControlPlaneSpec struct {
 }
 ```
 
+```go
+type GCPManagedClusterSpec struct {
+    // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
+    // +optional
+    ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
+}
+```
+
+**This is the design pattern used by AKS in CAPZ**. [An example of how ManagedCluster watches ControlPlane in CAPZ.](https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/5c69b44ed847365525504b242da83b5e5da75e4f/controllers/azuremanagedcluster_controller.go#L71)
+
 **Pros**
 
-- Simpler implementation
-  - No need for a pass-through infra cluster as control plane endpoint can be reported back via the control plane
+- Better aligned with CAPI’s traditional infra provider model
 - Works with ClusterClass
 
 **Cons**
 
-- If the configuration/functionality related to the base infrastructure are included then we have mixed concerns of the API type.
+- Need to maintain Infra cluster kind, which is a pass-through layer and has no other functions. In addition to the CRD, controllers, webhooks and conversions webhooks need to be maintained.
+- Infra provider doesn’t provision infrastructure and whilst it may meet the CAPI contract, it doesn’t actually create infrastructure as this is done via the control plane.
 
 #### Option 3: Two kinds with a Managed Control Plane and Managed Infra Cluster with Better Separation of Responsibilities
 
@@ -353,6 +343,10 @@ This option more closely follows the original separation of concerns with the di
 
 ```go
 type GCPManagedControlPlaneSpec struct {
+    // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
+    // +optional
+    ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"`
+
     // AddonsConfig defines the addons to enable with the GKE cluster.
     // +optional
     AddonsConfig *AddonsConfig `json:"addonsConfig,omitempty"`
@@ -400,8 +394,6 @@ type GCPManagedClusterSpec struct {
 }
 ```
 
-When the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) have been implemented there is the option to return the control plane endpoint directly from the ControlPlane instead of passing it via the Infracluster.
-
 **Pros**
 
 - Clearer separation between the lifecycle management of the general cloud infrastructure required for the cluster and the actual managed control plane  (i.e. GKE in this example)
@@ -413,9 +405,32 @@ When the changes documented in the [Contract Changes to Support Managed Kubernet
 
 - Duplication of API definitions between GCPCluster and GCPManagedCluster and reconciliation for the infrastructure cluster
 
+#### Option 4: Two kinds with a Managed Control Plane and Shared Infra Cluster with Better Separation of Responsibilities
+
+This option is a variation of option 3 and as such it more closely follows the original separation of concerns with the different CAPI provider types. The difference with this option compared to option 3 is that only 1 new resource kind is introduced:
+
+- **GCPManagedControlPlane**: this presents the actual GKE control plane in GCP. Its spec would only contain properties that are specific to provisioning & management of GKE. It would not contain any general properties related to the general GCP operating infrastructure, like the networking or project.
+
+The general cluster infrastructure will be declared via the existing **GCPCluster** kind and reconciled via the existing controller.
+
+However, this approach will require changes to the controller for **GCPCluster**. The steps to create the required infrastructure may be different between an unmanaged cluster and a GKE based cluster. For example, for an unmanaged cluster a load balancer will need to be created but with a GKE based cluster this won’t be needed and instead we’d need to use the endpoint created as part of **GCPManagedControlPlane** reconciliation.
+
+So the **GCPCluster** controller will need to know if its creating infrastructure for an unmanaged or managed cluster (probably by looking at the parent's (i.e. `Cluster`) **controlPlaneRef**) and do different steps.
+
+**Pros**
+
+- Single infra cluster kind irrespective of if you are creating an unmanaged or GKE based cluster. It doesn’t require the user to pick the right one.
+- Clear separation between cluster infrastructure and the actual managed (i.e. GKE) control plane
+- Works with cluster class
+
+**Cons**
+
+- Additional complexity and logic in the infra cluster controller
+- API definition could be messy if only certain fields are required for one type of cluster
+
 ## Recommendations
 
-It is proposed that option 3 (two kinds with a managed control plane and managed infra cluster with better separation of responsibilities) is the best way to proceed for **new implementations** of managed Kubernetes in a provider where there is additional infrastructure required (e.g. VPC, resource groups).
+It is proposed that option 3 (two kinds with a managed control plane and managed infra cluster with better separation of responsibilities) is the best way to proceed for **new implementations** of managed Kubernetes in a provider.
 
 The reasons for this recommendation are as follows:
 
@@ -423,25 +438,17 @@ The reasons for this recommendation are as follows:
 - The infra cluster provisions and manages the general infrastructure required for the cluster but not the control plane.
 - By having a separate infra cluster API definition, it allows differences in the API between managed and unmanaged clusters.
 
-> This is the model currently adopted by the managed Kubernetes part of CAPG & CAPOCI and all non-managed K8s implementations.
+Providers like CAPZ and CAPA have already implemented managed Kubernetes support and there should be no requirement on them to move to Option 3. Both Options 2 and 4 are solutions that would work with ClusterClass and so could be used if required.
 
-### Vanilla Managed Kubernetes (i.e. without any additional infrastructure)
+Option 1 is the only option that will not work with ClusterClass and would require a change to CAPI. Therefore this option is not recommended.
 
-If the managed Kubernetes services does not require any base infrastructure to be setup before creating the instance of the service then option 2 (Just a ControlPlane kind (and no InfraCluster) is the recommendation.
-
-This recommendation assumes that the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) have been implemented. Until that point option 1 (Two kinds with a ControlPlane and a pass-through InfraCluster) will have to be used.
-
-### Existing Managed Kubernetes Implementations
-
-Providers like CAPZ and CAPA have already implemented managed Kubernetes support and there should be no requirement on them to move to Option 3 (if there is additional infrastructure) or option 2 (if there isn't any have additional infrastructure).
-
-There is a desire to have consistency across all managed Kubernetes implementations and across all cluster types (i.e. managed and unmanaged) but the choice remains with the providers of existing implementations.
+*** This means that CAPA will have to make changes to move away from Option 1 if it wants to support ClusterClass.
 
 ### Additional notes on option 3
 
 There are a number of cons listed for option 3. With having 2 API kinds for the infra cluster (and associated controllers), there is a risk of code duplication. To reduce this the 2 controllers can utilize shared reconciliation code from the different controllers so as to reduce this duplication.
 
-The user will need to be aware of when to use which specific infra cluster kind. In our example this means that a user will need to know when to use `GCPCluster` vs `GCPManagedCluster`. To give clear guidance to users, we will provide templates (including ClusterClasses) and documentation for both the unmanaged and managed varieties of clusters. If we used the same infra cluster kind across both unmanaged & managed (i.e. alternative 2) then we run the risk of complicating the API for the infra cluster & controller if the required properties diverge.
+The user will need to be aware of when to use which specific infra cluster kind. In our example this means that a user will need to know when to use `GCPCluster` vs `GCPManagedCluster`. To give clear guidance to users, we will provide templates (including ClusterClasses) and documentation for both the unmanaged and managed varieties of clusters. If we used the same infra cluster kind across both unmanaged & managed (i.e. option 4) then we run the risk of complicating the API for the infra cluster & controller if the required properties diverge.
 
 ### Managed Node Groups for Worker Nodes
 
@@ -480,11 +487,10 @@ Its recommended that changes are made to the [Provider Implementers documentatio
 
 Some of the areas of change (this is not an exhaustive list):
 
-- A new "implementing managed kubernetes" guide that contains details about how to represent a managed Kubernetes service in CAPI. The content will be based on the recommendations from this proposal along with other considerations such as managed node and addon management.
+- A new "implementing managed kubernetes" guide that contains details about how to represent a managed Kubernetes service in CAPI. The content will be based on option 3 from this proposal along with other considerations such as managed node and addon management.
 - Update the [Provider contracts documentation](../book/src/developer/providers/contracts.md) to state that the same kind should not be used to satisfy 2 different provider contracts.
 - Update the [Cluster Infrastructure documentation](../book/src/developer/providers/cluster-infrastructure.md) to provide guidance on how to populate the `controlPlaneEndpoint` in the scenario where the control plane creates the api server load balancer. We should include sample code.
 - Update the [Control Plane Controller](../book/src/developer/architecture/controllers/control-plane.md) diagram for managed k8s services case. The Control Plane reconcile needs to start when `InfrastructureReady` is true.
-- Updates based on the changes documented in the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md).
 
 ## Other Considerations for CAPI
 
@@ -508,91 +514,6 @@ Some of the areas of change (this is not an exhaustive list):
   - [CAPZ](https://github.com/kubernetes-sigs/cluster-api-provider-azure/pull/2095)
 - Managed Kubernetes implementations should be able to opt-in/opt-out of what will be provided by [CAPI’s add-ons orchestration solution](https://github.com/kubernetes-sigs/cluster-api/issues/5491)
 
-## Alternatives
-
-A number of different representations where also considered but discounted.
-
-### Alternative 1: Single kind for Control Plane and Infrastructure
-
-This option introduces a new single resource kind:
-
-- **GCPManagedControlPlane**: this represents both a control-plane (i.e. GKE) and infrastructure required for the cluster. It contains properties for both the general cloud infrastructure (that would traditionally be represented by an infrastructure cluster) and the managed Kubernetes control plane (that would traditionally be represented by a control plane provider).
-
-```go
-type GCPManagedControlPlaneSpec struct {
-    // Project is the name of the project to deploy the cluster to.
-    Project string `json:"project"`
-
-    // NetworkSpec encapsulates all things related to the GCP network.
-    // +optional
-    Network NetworkSpec `json:"network"`
-
-    // AddonsConfig defines the addons to enable with the GKE cluster.
-    // +optional
-    AddonsConfig *AddonsConfig `json:"addonsConfig,omitempty"`
-
-    // Logging contains the logging configuration for the GKE cluster.
-    // +optional
-    Logging *ControlPlaneLoggingSpec `json:"logging,omitempty"`
-
-    // EnableKubernetesAlpha will indicate the kubernetes alpha features are enabled
-    // +optional
-    EnableKubernetesAlpha bool
-
-    // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
-    // +optional
-    ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
-    ....
-}
-```
-
-**This was the design pattern originally used for the EKS implementation in CAPA.**
-
-#### Background: Why did EKS in CAPA choose this option?
-
-CAPA decided to represent an EKS cluster as a CAPI control-plane. This meant that control-plane is responsible for creating the API server load balancer.
-
-Initially CAPA had an infrastructure cluster kind that reported back the control plane endpoint. This required less than ideal code in its controller to watch the control plane and use its value of the control plane endpoint.
-
-As the infrastructure cluster kind only acted as a passthrough (to satisfy the contract with CAPI) it was decided that it would be removed and the control-plane kind (AWSManagedControlPlane) could be used to satisfy both the “infrastructure” and “control-plane” contracts. _This worked well until ClusterClass arrived with its expectation that the “infrastructure” and “control-plane” are 2 different resource kinds._
-
-(Note: the above italicized text matter is no longer relevant once CAEP https://github.com/kubernetes-sigs/cluster-api/pull/8500 merges is implemented.)
-
-Note that CAPZ had a similar discussion and an [issue](https://github.com/kubernetes-sigs/cluster-api-provider-azure/issues/1396) to remove AzureManagedCluster: AzureManagedCluster is useless; let's remove it (and keep AzureManagedControlPlane)
-
-**Pros**
-
-- A simple design with a single resource kind and controller.
-
-**Cons**
-
-- Doesn’t work with the current implementation of ClusterClass, which expects a separation of ControlPlane and Infrastructure.
-- Doesn’t provide separation of responsibilities between creating the general cloud infrastructure for the cluster and the actual cluster control plane.
-- Managed Kubernetes look different from unmanaged Kubernetes where two separate kinds are used for a control plane and infrastructure. This would impact products building on top of CAPI.
-
-### Alternative 2: Two kinds with a Managed Control Plane and Shared Infra Cluster with Better Separation of Responsibilities
-
-This option is a variation of option 3 and as such it more closely follows the original separation of concerns with the different CAPI provider types. The difference with this option compared to option 3 is that only 1 new resource kind is introduced:
-
-- **GCPManagedControlPlane**: this presents the actual GKE control plane in GCP. Its spec would only contain properties that are specific to provisioning & management of GKE. It would not contain any general properties related to the general GCP operating infrastructure, like the networking or project.
-
-The general cluster infrastructure will be declared via the existing **GCPCluster** kind and reconciled via the existing controller.
-
-However, this approach will require changes to the controller for **GCPCluster**. The steps to create the required infrastructure may be different between an unmanaged cluster and a GKE based cluster. For example, for an unmanaged cluster a load balancer will need to be created but with a GKE based cluster this won’t be needed and instead we’d need to use the endpoint created as part of **GCPManagedControlPlane** reconciliation.
-
-So the **GCPCluster** controller will need to know if its creating infrastructure for an unmanaged or managed cluster (probably by looking at the parent's (i.e. `Cluster`) **controlPlaneRef**) and do different steps.
-
-**Pros**
-
-- Single infra cluster kind irrespective of if you are creating an unmanaged or GKE based cluster. It doesn’t require the user to pick the right one.
-- Clear separation between cluster infrastructure and the actual managed (i.e. GKE) control plane
-- Works with cluster class
-
-**Cons**
-
-- Additional complexity and logic in the infra cluster controller
-- API definition could be messy if only certain fields are required for one type of cluster
-
 ## Upgrade Strategy
 
 As mentioned in the goals section, it is up to providers with existing implementations, CAPA and CAPZ, to decide how they want to proceed.
@@ -606,4 +527,3 @@ As mentioned in the goals section, it is up to providers with existing implement
 - [x] 03/17/2022: Compile a Google Doc following the CAEP template ([link](https://docs.google.com/document/d/1dMN4-KppBkA51sxXPSQhYpqETp2AG_kHzByXTmznxFA/edit?usp=sharing))
 - [x] 04/20/2022: Present proposal at a community meeting
 - [x] 07/27/2022: Move the proposal to a PR in CAPI repo
-- [x] 06/15/2023: Updates as a result of the [Contract Changes to Support Managed Kubernetes CAEP](./20230407-flexible-managed-k8s-endpoints.md) and also updates as a result of the current state of managed k8s in CAPI.
diff --git a/docs/proposals/20230407-flexible-managed-k8s-endpoints.md b/docs/proposals/20230407-flexible-managed-k8s-endpoints.md
deleted file mode 100644
index 9b3e1b706..000000000
--- a/docs/proposals/20230407-flexible-managed-k8s-endpoints.md
+++ /dev/null
@@ -1,312 +0,0 @@
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
-
-- [Flexible Managed Kubernetes Endpoints](#flexible-managed-kubernetes-endpoints)
-  - [Glossary](#glossary)
-  - [Summary](#summary)
-  - [Motivation](#motivation)
-    - [Goals](#goals)
-    - [Non-Goals](#non-goals)
-    - [Future Work](#future-work)
-  - [Proposal](#proposal)
-    - [User Stories](#user-stories)
-      - [Story 1](#story-1)
-      - [Story 2](#story-2)
-      - [Story 3](#story-3)
-      - [Story 4](#story-4)
-      - [Story 5](#story-5)
-      - [Story 6](#story-6)
-      - [Story 7](#story-7)
-    - [Design](#design)
-      - [Core Cluster API changes](#core-cluster-api-changes)
-      - [Infra Providers API changes](#infra-providers-api-changes)
-      - [Core Cluster API Controllers changes](#core-cluster-api-controllers-changes)
-      - [Provider controller changes](#provider-controller-changes)
-    - [Guidelines for infra providers implementation](#guidelines-for-infra-providers-implementation)
-  - [Background work](#background-work)
-    - [EKS in CAPA](#eks-in-capa)
-      - [AKS in CAPZ](#aks-in-capz)
-      - [GKE in CAPG](#gke-in-capg)
-      - [Learnings from original Proposal: Two kinds with a Managed Control Plane & Managed Infra Cluster adhering to the current CAPI contracts](#learnings-from-original-proposal-two-kinds-with-a-managed-control-plane--managed-infra-cluster-adhering-to-the-current-capi-contracts)
-    - [Two New Flows](#two-new-flows)
-      - [Flow 1: `<Infra>Cluster` and `<Infra>ControlPlane`, with `ControlPlaneEndpoint` reported via `<Infra>ControlPlane`](#flow-1-infracluster-and-infracontrolplane-with-controlplaneendpoint-reported-via-infracontrolplane)
-      - [Flow 2: Change CAPI to make `<Infra>Cluster` optional](#flow-2-change-capi-to-make-infracluster-optional)
-      - [Alternative Option: Introduce a new Managed Kubernetes provider type (with contract)](#alternative-option-introduce-a-new-managed-kubernetes-provider-type-with-contract)
-  - [Recommendations](#recommendations)
-  - [Implementation History](#implementation-history)
-
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
----
-title: Flexible Managed Kubernetes Endpoints
-authors:
-  - "@jackfrancis"
-reviewers:
-  - "@richardcase"
-  - "@pydctw"
-  - "@mtougeron"
-  - "@CecileRobertMichon"
-  - "@fabriziopandini"
-  - "@sbueringer"
-  - "@killianmuldoon"
-  - "@mboersma"
-  - "@nojnhuh"
-creation-date: 2023-04-07
-last-updated: 2023-04-07
-status: provisional
-see-also:
-  - "/docs/proposals/20220725-managed-kubernetes.md"
----
-
-# Flexible Managed Kubernetes Endpoints
-
-## Glossary
-
-Refer to the [Cluster API Book Glossary](https://cluster-api.sigs.k8s.io/reference/glossary.html).
-
-The following terms will be used in this document.
-
-- Managed Kubernetes
-  - Managed Kubernetes refers to any Kubernetes Cluster provisioning and maintenance abstraction, usually exposed as an API, that is natively available in a Cloud provider. For example: [EKS](https://aws.amazon.com/eks/), [OKE](https://www.oracle.com/cloud/cloud-native/container-engine-kubernetes/), [AKS](https://azure.microsoft.com/en-us/products/kubernetes-service), [GKE](https://cloud.google.com/kubernetes-engine), [IBM Cloud Kubernetes Service](https://www.ibm.com/cloud/kubernetes-service), [DOKS](https://www.digitalocean.com/products/kubernetes), and many more throughout the Kubernetes Cloud Native ecosystem.
-- `ControlPlane Provider`
-  - When we say `ControlPlane Provider` we refer to a solution that implements a solution for the management of a Kubernetes [control plane](https://kubernetes.io/docs/concepts/#kubernetes-control-plane) according to the Cluster API contract. Please note that in the context of managed Kubernetes, the `ControlPlane Provider` usually wraps the corresponding abstraction for a specific Cloud provider. Concrete example for Microsoft Azure is the `AzureManagedControlPlane`, for AWS the `AWSManagedControlPlane`, for Google the `GCPManagedControlPlane` etc.
-- _Kubernetes Cluster Infrastructure_
-  - When we refer to _Kubernetes Cluster Infrastructure_ (abbr. _Cluster Infrastructure_) we refer to the **infrastructure that supports a Kubernetes cluster**, like e.g. VPC, security groups, load balancers etc. Please note that in the context of Managed Kubernetes some of those components are going to be provided by the corresponding abstraction for a specific Cloud provider (EKS, OKE, AKS etc), and thus Cluster API should not take care of managing a subset or all those components.
-- `<Infra>Cluster`
-  - When we say `<Infra>Cluster` we refer to any provider that provides Kubernetes Cluster Infrastructure for a specific Cloud provider. Concrete example for Microsoft Azure is the `AzureCluster` and the `AzureManagedCluster`, for AWS the `AWSCluster` and the `AWSManagedCluster`, for Google Cloud the `GCPCluster` and the `GCPManagedCluster`).
-- e.g.
-  - This just means "For example:"!
-
-## Summary
-
-This proposal aims to address the lesson learned by running Managed Kubernetes solution on top of Cluster API, and make this use case simpler and more straight forward both for Cluster API users and for the maintainers of the Cluster API providers.
-
-More specifically we would like to introduce first class support for two scenarios:
-
-- Permit omitting the `<Infra>Cluster` entirely, thus making it simpler to use with Cluster API all the Managed Kubernetes implementations which do not require any additional Kubernetes Cluster Infrastructure (network settings, security groups, etc) on top of what is provided out of the box by the managed Kubernetes primitive offered by a Cloud provider.
-- Allow the `ControlPlane Provider` component to take ownership of the responsibility of creating the control plane endpoint, thus making it simpler to use with Cluster API all the Managed Kubernetes implementations which are taking care out of the box of this piece of Cluster Infrastructure.
-
-The above capabilities can be used alone or in combination depending on the requirements of a specific Managed Kubernetes or on the specific architecture/set of Cloud components being implemented.
-
-## Motivation
-
-The implementation of Managed Kubernetes scenarios by Cluster API providers occurred after the architectural design of Cluster API, and thus that design process did not consider these Managed Kubernetes scenarios as a user story. In practice, Cluster API's specification has allowed Managed Kubernetes solutions to emerge that aid running fleets of clusters at scale, with CAPA's `AWSManagedCluster` and `AzureManagedCluster` being notable examples. However, because these Managed Kubernetes solutions arrived after the Cluster API contract was defined, providers have not settled on a consistent rendering of how a "Service-Managed Kubernetes" specification fits into a "Cluster API-Managed Kubernetes" surface area.
-
-One particular part of the existing Cluster API surface area that is inconsistent with most Managed Kubernetes user experiences is the accounting of the [Kubernetes API server](https://kubernetes.io/docs/concepts/overview/components/#kube-apiserver). In the canonical "self-managed" user story that Cluster API addresses, it is the provider implementation of Cluster API (e.g., CAPA) that is responsible for scaffolding the necessary _Kubernetes Cluster Infrastructure_ that is required in order to create the Kubernetes API server (e.g., a Load Balancer and a public IP address). This provider responsibility is declared in the `<Infra>Cluster` resource, and carried out via its controllers; and then finally this reconciliation is synchronized with the parent `Cluster` Cluster API resource.
-
-Because there exist Managed Kubernetes scenarios that handle a subset or all _Kubernetes Cluster Infrastructure_ responsibilities themselves, Cluster API's requirement of a `<Infra>Cluster` resource leads to undesirable implementation decisions, because in these scenarios there is no actual work for a Cluster API provider to do to scaffold _Kubernetes Cluster Infrastructure_.
-
-Finally, for Managed Kubernetes scenarios that _do_ include additional, user-exposed infra (e.g., GKE and EKS as of this writing), we want to make it easier to account for the representation of the Managed Kubernetes API server endpoint, which is not always best owned by a `<Infra>Cluster` resource.
-
-### Goals
-
-- Build upon [the existing Cluster API Managed Kubernetes proposal](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220725-managed-kubernetes.md). Any net new recommendations and/or proposals will be a continuation of the existing proposal, and consistent with its original conclusions.
-- Identify and document API changes and controllers changes required to omit the `<Infra>Cluster` entirely, where this is applicable.
-- Identify and document API changes and controllers changes required to allow the `ControlPlane Provider` component to take ownership of the responsibility of creating the control plane endpoint.
-- Ensure any changes to the current behavioral contract are backwards-compatible.
-
-### Non-Goals
-
-- Introduce new "Managed Kubernetes" data types in Cluster API.
-- Invalidate [the existing Cluster API Managed Kubernetes proposal and concluding recommendations](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220725-managed-kubernetes.md).
-
-### Future Work
-
-- Detailed documentation that references the flavors of Managed Kubernetes scenarios and how they can be implemented in Cluster API, with provider examples.
-
-## Proposal
-
-### User Stories
-
-#### Story 1
-
-As a cluster operator, I want to use Cluster API to provision and manage the lifecycle of a control plane that utilizes my service provider's managed Kubernetes control plane (i.e. EKS, AKS, GKE), so that I don’t have to worry about the management/provisioning of control plane nodes, and so I can take advantage of any value add services offered by my cloud provider.
-
-#### Story 2
-
-As a cluster operator, I want to be able to provision both "unmanaged" and "managed" Kubernetes clusters from the same management cluster, so that I can support different requirements and use cases as needed whilst using a single operating model.
-
-#### Story 3
-
-As a Cluster API provider implementor, I want to be able to return the control plane endpoint created by the `ControlPlane Provider`, so that it fits naturally with how most of the native Managed Kubernetes implementations works.
-
-#### Story 4
-
-As a Cluster API provider developer, I want guidance on how to incorporate a managed Kubernetes service into my provider, so that its usage is compatible with Cluster API architecture/features and its usage is consistant with other providers.
-
-#### Story 5
-
-As a Cluster API provider developer, I want to enable the ClusterClass feature for a Managed Kubernetes service, so that users can take advantage of an improved UX with ClusterClass-based clusters.
-
-#### Story 6
-
-As a cluster operator, I want to use Cluster API to provision and manage the lifecycle of worker nodes that utilizes my cloud providers' managed instances (if they support them), so that I don't have to worry about the management of these instances.
-
-#### Story 7
-
-As a service provider I want to be able to offer Managed Kubernetes clusters by using CAPI referencing my own managed control plane implementation that satisfies Cluster API contracts.
-
-### Design
-
-Below we are documenting API changes and controllers changes required to omit the `<Infra>Cluster` entirely and to allow the `ControlPlane Provider` component  to take ownership of the responsibility of creating the control plane endpoint.
-
-#### Core Cluster API changes
-
-This proposal does not introduce any breaking changes for the existing "core" API. More specifically:
-
-The existing Cluster API types are already able to omit the `<Infra>Cluster`:
-
-- The `infrastructureRef` field on the Cluster object is already a pointer and thus it could be set to nil, and in fact we are already creating Clusters without `infrastructureRef` when we use a cluster class).
-- The `infrastructure.Ref` field on the ClusterClass objects already a pointer and thus it could be set to nil, but in this case it is required to change the validation webhook to allow the user to not specify it; on top of that, when validating inline patches, we should reject patches targeting the infrastructure template objects if not specified.
-
-In order to allow the `ControlPlane Provider` component to take ownership of the responsibility of creating the control plane endpoint we are going to introduce a new `ClusterEndpoint` CRD, below some example:
-
-```yaml
-apiVersion: cluster.x-k8s.io/v1beta1
-kind: ClusterEndpoint
-metadata:
-   labels:
-      cluster.x-k8s.io/cluster-name: my-cluster
-spec:
-  cluster: my-cluster
-  host: "my-cluster-1234567890.region.elb.amazonaws.com"
-  port: 1234
-  type: ExternalControlPlaneEndpoint
-```
-
-```yaml
-apiVersion: cluster.x-k8s.io/v1beta1
-kind: ClusterEndpoint
-metadata:
-   labels:
-      cluster.x-k8s.io/cluster-name: my-cluster-2
-spec:
-  cluster: my-cluster-2
-  host: "10.40.85.102"
-  port: 1234
-  type: ExternalControlPlaneEndpoint
-```
-
-This is how the type specification would look:
-
-```go
-// ClusterEndpointType describes the type of cluster endpoint.
-type ClusterEndpointType string
-
-// ClusterEndpoint represents a reachable Kubernetes API endpoint serving a particular cluster function.
-type ClusterEndpoint struct {
-  metav1.TypeMeta   `json:",inline"`
-	metav1.ObjectMeta `json:"metadata,omitempty"`
-
-  Spec ClusterEndpointSpec   `json:"spec,omitempty"`
-}
-
-// ClusterEndpointSpec defines the desired state of the Cluster endpoint.
-type ClusterEndpointSpec struct {
-	// The Host is the DNS record or the IP address that the endpoint is reachable on.
-	Host string `json:"host"`
-
-	// The port on which the endpoint is serving.
-	Port int32 `json:"port"`
-
-  // Cluster is a reference to the cluster name that this endpoint is reachable on.
-  Cluster string `json:"cluster"`
-
-  // Type describes the function that this cluster endpoint serves.
-  // +kubebuilder:validation:Enum=apiserver
-  Type ClusterEndpointType `json:"type"`
-}
-```
-
-The `<Infra>Cluster` object which is currently using the `spec.controlPlaneEndpoint` for the same scope will continue to work because "core" Cluster API controllers will continue to recognize when this field is set and take care of generating the `ClusterEndpoint` automatically; however this mechanism should be considered as a temporary machinery to migrate to the new CRD, and it will be removed in future versions of Cluster API. In addition, once the legacy behavior is removed, we will deprecate and eventually remove the `spec.controlPlaneEndpoint` field from the `Cluster` CustomResourceDefinition, and recommend that providers do the same for their `<Infra>Cluster` CustomResourceDefinitions as well.
-
-Future Notes:
-
-- A future `type` field can be introduced to enable CAPI to extend the usage of this CRD to address https://github.com/kubernetes-sigs/cluster-api/issues/5295 in a future iteration
-- The current implementation originates from the `Cluster.spec.ControlPlaneEndpoint` field, which defines the info we need for this proposal; but in future iterations we might consider to support more addressed or more ports for each ClusterEndpoint, similarly what is implemented in the core v1 Endpoint type.
-
-#### Infra Providers API changes
-
-This proposal does not introduce any breaking changes for the provider's API.
-
-However, Infra providers will be made aware that `spec.controlPlaneEndpoint` will be scheduled for deprecation in `<Infra>Cluster` resources in a future CAPI API version, with corresponding warning messages in controller logs. We will recommend that they remove it in a future API version of their provider.
-
-#### Core Cluster API Controllers changes
-
-- All the controllers working with ClusterClass objects must take into account that the `infrastructure.Ref` field could be omitted; most notably:
-  - The ClusterClass controller must ignore nil `infrastructure.Ref` fields while adding owner references to all the objects referenced by a ClusterClass.
-  - The Topology controller must skip the generation of the `<Infra>Cluster` objects when the `infrastructure.Ref` field in a ClusterClass is empty.
-
-- All the controllers working with Cluster objects must take into account that the `infrastructureRef` field could be omitted; most notably:
-  - The Cluster controller must use skip reconciling this external reference when the `infrastructureRef` is missing; also, the `status.InfrastructureReady` field must be automatically set to true in this case.
-
-- A controller (details TBD) will reconcile the new `ClusterEndpoint` CR. Please note that:
-  - The value from the `ClusterEndpoint` CRD must surface on the `spec.ControlPlaneEndpoint` field on the `Cluster` object.
-  - If both are present, the value from the `ClusterEndpoint` CRD must take precedence on the value from `<Infra>Cluster` objects still using the `spec.controlPlaneEndpoint`.
-
-- The Cluster controller must implement the temporary machinery to migrate to the new CRD existing Clusters and to deal with `<Infra>Cluster` objects still using the `spec.controlPlaneEndpoint` field as a way to communicate the ClusterAddress to "core" Cluster API controllers:
-  - If there is the `spec.ControlPlaneEndpoint` on the `Cluster` object but not a corresponding `ClusterEndpoint` CR, the CR must be created.
-
-#### Provider controller changes
-
-- All the `<Infra>Cluster` controllers who are responsible for creating a control plane endpoint
-  - As soon as the `spec.controlPlaneEndpoint` field in the `<Infra>Cluster` object will removed, the `<Infra>Cluster` controller must instead create a `ClusterEndpoint` CR to communicate the control plane endpoint to the Cluster API core controllers
-    - NOTE: technically it is possible to start creating the `ClusterEndpoint` CR *before* the removal of the `spec.controlPlaneEndpoint` field, because the new CR will take precedence on the value read from the field, but this is up to the infra provider maintainers.
-  - The `ClusterEndpoint` CR must have an owner reference to the `<Infra>Cluster` object from which it is originated.
-
-- All the `ControlPlane Provider` controllers who are responsible for creating a control plane endpoint
-  - Must no longer wait for the `spec.ControlPlaneEndpoint` field on the `Cluster` object to be set before starting to provision the control plane.
-  - As soon as the Managed Kubernetes Service-provided control plane endpoint is available, the controller must create a `ClusterEndpoint` CR to communicate this to the control plane endpoint to the Cluster API core controllers
-  - The `ClusterEndpoint` CR must have an owner reference to the `ControlPlane` object from which is originated.
-
-### Guidelines for infra providers implementation
-
-Let's consider following scenarios for an hypothetical `cluster-api-provider-foo` infra provider:
-
-_Scenario 1._
-
-If the `Foo` cloud provider has a `FKS` managed Kubernetes offering that is taking care of _the entire Kubernetes Cluster infrastructure_, the maintainers of the `cluster-api-provider-foo` provider:
-- Must not implement a `FKSCluster` CRD and the corresponding `FKSClusterTemplate` CRD (nor the related controllers)
-- Must implement a `FKRControlControlplane provider`, a `FKRControlControlplane` CRD, the corresponding `FKRControlControlplane` and related controllers
-- The `FKRControlControlplane` controller:
-  - Must not wait for `spec.ControlPlaneEndpoint` field on the `Cluster` object to be set before starting to provision the `FKS` managed Kubernetes instance.
-  - As soon as the control plane endpoint is available, Must create a `ClusterEndpoint` CR to communicate the control plane endpoint to the Cluster API core controllers; the `ClusterEndpoint` CR must have an owner reference to the `FKRControlControlplane` object from which is originated.
-  - Must set the `status.Ready` field on the `FKRControlControlplane` object when the provisioning is complete
-
-_Scenario 2._
-
-If the `Foo` cloud provider has a `FKS` managed Kubernetes offering that is taking care of _only of a subset of the Kubernetes Cluster infrastructure_, or it is required to provision some additional pieces of infrastructure on top of what provisioned out of the box, e.g. a SSH bastion host, the maintainers of the `cluster-api-provider-foo` provider:
-- Must implement a `FKSCluster` CRD and the corresponding `FKSClusterTemplate` CRD and the related controllers
-  - The `FKSCluster` controller
-    - Must create only the additional piece of the _Kubernetes Cluster infrastructure_ not provisioned by the `FKS` managed Kubernetes instance (in this example a SSH bastion host)
-    - Must not create a `ClusterEndpoint` CR (nor set the `spec.controlPlaneEndpoint` field in the `FKSCluster` object), because provisioning the control plane endpoint is not responsibility of this controller.
-    - Must set the `status.Ready` field on the `FKSCluster` object when the provisioning is complete
-- Must implement a `FKRControlControlplane provider`, a `FKRControlControlplane` CRD, the corresponding `FKRControlControlplane` and related controllers
-  - The `FKRControlControlplane` controller:
-    - Must wait for `status.InfrastructureReady` field on the `Cluster` object to be set to true before starting to provision the control plane.
-    - Must not wait for `spec.ControlPlaneEndpoint` field on the `Cluster` object to be set before starting to provision the control plane.
-    - As soon as the control plane endpoint is available, Must create a `ClusterEndpoint` CR to communicate the control plane endpoint to the Cluster API core controllers; the `ClusterEndpoint` CR must have an owner reference to the `FKRControlControlplane` object from which is originated.
-    - Must set the `status.Ready` field on the `FKRControlControlplane` object when the provisioning is complete
-
-_Scenario 3._
-
-If the `Foo` cloud provider has a `FKS` managed Kubernetes offering that is not taking care of the control plane endpoint e.g. because it requires an existing `FooElasticIP`, a `FooElacticLoadBalancer` to be provisioned before creating the `FKS` managed Kubernetes cluster, the maintainers of the `cluster-api-provider-foo` provider:
-- Must implement a `FKSCluster` CRD and the corresponding `FKSClusterTemplate` CRD and the related controllers; those controllers must create a `ClusterEndpoint` CR as soon as the control plane endpoint is available
-  - The `FKSCluster` controller
-    - Must create only the additional piece of the _Kubernetes Cluster infrastructure_ not provisioned by the `FKS` managed Kubernetes instance (in this example `FooElasticIP`, a `FooElacticLoadBalancer`)
-    - As soon as the control plane endpoint is available, Must create a `ClusterEndpoint` CR; the `ClusterEndpoint` CR must have an owner reference to the `FKSCluster` object from which is originated.
-    - Must set the `status.Ready` field on the `FKSCluster` object when the provisioning is complete
-- Must implement a `FKRControlControlplane provider`, a `FKRControlControlplane` CRD, the corresponding `FKRControlControlplane` and related controllers
-  - The `FKRControlControlplane` controller:
-    - Must wait for `status.InfrastructureReady` field on the `Cluster` object to be set to true before starting to provision the `FKS` managed Kubernetes instance.
-    - Must wait for `spec.ControlPlaneEndpoint` field on the `Cluster` object to be set before starting to provision the `FKS` managed Kubernetes instance.
-    - Must set the `status.Ready` field on the `FKRControlControlplane` object when the provisioning is complete
-
-Please note that this scenario is equivalent to what is implemented for a non managed Kubernetes `FooCluster`, backed by Cluster API managed `FooMachines`, with the only difference that in this case it possible to rely on `KCP` as `ControlControlplane provider`, and thus point 2 of the above list do not apply.
-
-## Implementation History
-
-- [x] 01/11/2023: Compile a Google Doc to organize thoughts prior to CAEP [link here](https://docs.google.com/document/d/1rqzZfsO6k_RmOHUxx47cALSr_6SeTG89e9C44-oHHdQ/)
-
-[managedKubernetesRecommendation]: https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220725-managed-kubernetes.md#option-3-two-kinds-with-a-managed-control-plane-and-managed-infra-cluster-with-better-separation-of-responsibilities
diff --git a/go.mod b/go.mod
index 37765b98a..449936eba 100644
--- a/go.mod
+++ b/go.mod
@@ -75,7 +75,7 @@ require (
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
 	github.com/fatih/camelcase v1.0.0 // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/fvbommel/sortorder v1.1.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
@@ -143,14 +143,14 @@ require (
 	github.com/vincent-petithory/dataurl v1.0.0 // indirect
 	github.com/xlab/treeprint v1.2.0 // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0 // indirect
-	go.opentelemetry.io/otel v1.20.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect
-	go.opentelemetry.io/otel/metric v1.20.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.20.0 // indirect
-	go.opentelemetry.io/otel/trace v1.20.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
+	go.opentelemetry.io/otel v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
+	go.opentelemetry.io/otel/metric v1.19.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.19.0 // indirect
+	go.opentelemetry.io/otel/trace v1.19.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
 	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
 	go.uber.org/multierr v1.11.0 // indirect
diff --git a/go.sum b/go.sum
index 8fae56665..6732e3c25 100644
--- a/go.sum
+++ b/go.sum
@@ -162,8 +162,8 @@ github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwo
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/flatcar/container-linux-config-transpiler v0.9.4 h1:yXQ0NB8PeNrKJPrZvbv5/DV63PNhTqt8vaf8YxmX/RA=
 github.com/flatcar/container-linux-config-transpiler v0.9.4/go.mod h1:LxanhPvXkWgHG9PrkT4rX/p7YhUPdDGGsUdkNpV3L5U=
 github.com/flatcar/ignition v0.36.2 h1:xGHgScUe0P4Fkprjqv7L2CE58emiQgP833OCCn9z2v4=
@@ -581,22 +581,22 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0 h1:1eHu3/pUSWaOgltNK3WJFaywKsTIr/PwvHyDmi0lQA0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0/go.mod h1:HyABWq60Uy1kjJSa2BVOxUVao8Cdick5AWSKPutqy6U=
-go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
-go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0=
-go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA=
-go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM=
-go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM=
-go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0=
-go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
-go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 h1:xFSRQBbXF6VvYRf2lqMJXxoB72XI1K/azav8TekHHSw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0/go.mod h1:h8TWwRAhQpOd0aM5nYsRD8+flnkj+526GEIVlarH7eY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48=
+go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
+go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
+go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
+go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
+go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
+go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
+go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
+go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
 go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
@@ -604,7 +604,7 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=


Test patch:
diff --git a/test/e2e/cluster_upgrade.go b/test/e2e/cluster_upgrade.go
index 514383e8c..4b3b59216 100644
--- a/test/e2e/cluster_upgrade.go
+++ b/test/e2e/cluster_upgrade.go
@@ -208,19 +208,18 @@ func ClusterUpgradeConformanceSpec(ctx context.Context, inputGetter func() Clust
 					MachineDeployments:          clusterResources.MachineDeployments,
 					WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
 				})
-			}
-		}
 
-		// Only attempt to upgrade MachinePools if they were provided in the template.
-		if len(clusterResources.MachinePools) > 0 && workerMachineCount > 0 {
-			By("Upgrading the machinepool instances")
-			framework.UpgradeMachinePoolAndWait(ctx, framework.UpgradeMachinePoolAndWaitInput{
-				ClusterProxy:                   input.BootstrapClusterProxy,
-				Cluster:                        clusterResources.Cluster,
-				UpgradeVersion:                 input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
-				WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
-				MachinePools:                   clusterResources.MachinePools,
-			})
+				if len(clusterResources.MachinePools) > 0 {
+					By("Upgrading the machinepool instances")
+					framework.UpgradeMachinePoolAndWait(ctx, framework.UpgradeMachinePoolAndWaitInput{
+						ClusterProxy:                   input.BootstrapClusterProxy,
+						Cluster:                        clusterResources.Cluster,
+						UpgradeVersion:                 input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
+						WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
+						MachinePools:                   clusterResources.MachinePools,
+					})
+				}
+			}
 		}
 
 		By("Waiting until nodes are ready")
diff --git a/test/e2e/cluster_upgrade_runtimesdk.go b/test/e2e/cluster_upgrade_runtimesdk.go
index 56b53bd97..e2d62e5d4 100644
--- a/test/e2e/cluster_upgrade_runtimesdk.go
+++ b/test/e2e/cluster_upgrade_runtimesdk.go
@@ -247,18 +247,6 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
 			},
 		})
 
-		// Only attempt to upgrade MachinePools if they were provided in the template.
-		if len(clusterResources.MachinePools) > 0 && workerMachineCount > 0 {
-			By("Upgrading the machinepool instances")
-			framework.UpgradeMachinePoolAndWait(ctx, framework.UpgradeMachinePoolAndWaitInput{
-				ClusterProxy:                   input.BootstrapClusterProxy,
-				Cluster:                        clusterResources.Cluster,
-				UpgradeVersion:                 input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
-				WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
-				MachinePools:                   clusterResources.MachinePools,
-			})
-		}
-
 		By("Waiting until nodes are ready")
 		workloadProxy := input.BootstrapClusterProxy.GetWorkloadCluster(ctx, namespace.Name, clusterResources.Cluster.Name)
 		workloadClient := workloadProxy.GetClient()
diff --git a/test/e2e/clusterclass_changes.go b/test/e2e/clusterclass_changes.go
index d9a309a3c..3f2e9cd5e 100644
--- a/test/e2e/clusterclass_changes.go
+++ b/test/e2e/clusterclass_changes.go
@@ -34,6 +34,7 @@ import (
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controllers/external"
+	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api/internal/contract"
 	"sigs.k8s.io/cluster-api/test/e2e/internal/log"
 	"sigs.k8s.io/cluster-api/test/framework"
@@ -426,6 +427,131 @@ func modifyMachineDeploymentViaClusterClassAndWait(ctx context.Context, input mo
 	}
 }
 
+// modifyMachinePoolViaClusterClassAndWaitInput is the input type for modifyMachinePoolViaClusterClassAndWait.
+type modifyMachinePoolViaClusterClassAndWaitInput struct {
+	ClusterProxy                                  framework.ClusterProxy
+	ClusterClass                                  *clusterv1.ClusterClass
+	Cluster                                       *clusterv1.Cluster
+	ModifyBootstrapConfigTemplateFields           map[string]interface{}
+	ModifyInfrastructureMachinePoolTemplateFields map[string]interface{}
+	WaitForMachinePools                           []interface{}
+}
+
+// modifyMachinePoolViaClusterClassAndWait modifies the BootstrapConfigTemplate of MachinePoolClasses of a ClusterClass
+// by setting ModifyBootstrapConfigTemplateFields and waits until the changes are rolled out to the MachinePools of the Cluster.
+// NOTE: This helper is really specific to this test, so we are keeping this private vs. adding it to the framework.
+func modifyMachinePoolViaClusterClassAndWait(ctx context.Context, input modifyMachinePoolViaClusterClassAndWaitInput) {
+	Expect(ctx).NotTo(BeNil(), "ctx is required for modifyMachinePoolViaClusterClassAndWait")
+	Expect(input.ClusterProxy).ToNot(BeNil(), "Invalid argument. input.ClusterProxy can't be nil when calling modifyMachinePoolViaClusterClassAndWait")
+	Expect(input.ClusterClass).ToNot(BeNil(), "Invalid argument. input.ClusterClass can't be nil when calling modifyMachinePoolViaClusterClassAndWait")
+	Expect(input.Cluster).ToNot(BeNil(), "Invalid argument. input.Cluster can't be nil when calling modifyMachinePoolViaClusterClassAndWait")
+
+	mgmtClient := input.ClusterProxy.GetClient()
+
+	for _, mpClass := range input.ClusterClass.Spec.Workers.MachinePools {
+		// Only try to modify the BootstrapConfigTemplate if the MachinePoolClass is using a BootstrapConfigTemplate.
+		var bootstrapConfigTemplateRef *corev1.ObjectReference
+		var newBootstrapConfigTemplateName string
+		if mpClass.Template.Bootstrap.Ref != nil {
+			log.Logf("Modifying the BootstrapConfigTemplate of MachinePoolClass %q of ClusterClass %s", mpClass.Class, klog.KObj(input.ClusterClass))
+
+			// Retrieve BootstrapConfigTemplate object.
+			bootstrapConfigTemplateRef = mpClass.Template.Bootstrap.Ref
+			bootstrapConfigTemplate, err := external.Get(ctx, mgmtClient, bootstrapConfigTemplateRef, input.Cluster.Namespace)
+			Expect(err).ToNot(HaveOccurred())
+			// Create a new BootstrapConfigTemplate object with a new name and ModifyBootstrapConfigTemplateFields set.
+			newBootstrapConfigTemplate := bootstrapConfigTemplate.DeepCopy()
+			newBootstrapConfigTemplateName = fmt.Sprintf("%s-%s", bootstrapConfigTemplateRef.Name, util.RandomString(6))
+			newBootstrapConfigTemplate.SetName(newBootstrapConfigTemplateName)
+			newBootstrapConfigTemplate.SetResourceVersion("")
+			for fieldPath, value := range input.ModifyBootstrapConfigTemplateFields {
+				Expect(unstructured.SetNestedField(newBootstrapConfigTemplate.Object, value, strings.Split(fieldPath, ".")...)).To(Succeed())
+			}
+			Expect(mgmtClient.Create(ctx, newBootstrapConfigTemplate)).To(Succeed())
+		}
+
+		log.Logf("Modifying the InfrastructureMachinePoolTemplate of MachinePoolClass %q of ClusterClass %s", mpClass.Class, klog.KObj(input.ClusterClass))
+
+		// Retrieve InfrastructureMachineTemplate object.
+		infrastructureMachinePoolTemplateRef := mpClass.Template.Infrastructure.Ref
+		infrastructureMachinePoolTemplate, err := external.Get(ctx, mgmtClient, infrastructureMachinePoolTemplateRef, input.Cluster.Namespace)
+		Expect(err).ToNot(HaveOccurred())
+		// Create a new InfrastructureMachinePoolTemplate object with a new name and ModifyInfrastructureMachinePoolTemplateFields set.
+		newInfrastructureMachinePoolTemplate := infrastructureMachinePoolTemplate.DeepCopy()
+		newInfrastructureMachinePoolTemplateName := fmt.Sprintf("%s-%s", infrastructureMachinePoolTemplateRef.Name, util.RandomString(6))
+		newInfrastructureMachinePoolTemplate.SetName(newInfrastructureMachinePoolTemplateName)
+		newInfrastructureMachinePoolTemplate.SetResourceVersion("")
+		for fieldPath, value := range input.ModifyInfrastructureMachinePoolTemplateFields {
+			Expect(unstructured.SetNestedField(newInfrastructureMachinePoolTemplate.Object, value, strings.Split(fieldPath, ".")...)).To(Succeed())
+		}
+		Expect(mgmtClient.Create(ctx, newInfrastructureMachinePoolTemplate)).To(Succeed())
+
+		// Patch the refs of the MachinePoolClass to reference the new templates.
+		patchHelper, err := patch.NewHelper(input.ClusterClass, mgmtClient)
+		Expect(err).ToNot(HaveOccurred())
+		if mpClass.Template.Bootstrap.Ref != nil {
+			bootstrapConfigTemplateRef.Name = newBootstrapConfigTemplateName
+		}
+		infrastructureMachinePoolTemplateRef.Name = newInfrastructureMachinePoolTemplateName
+		Expect(patchHelper.Patch(ctx, input.ClusterClass)).To(Succeed())
+
+		log.Logf("Waiting for MachinePool rollout for MachinePoolClass %q to complete.", mpClass.Class)
+		for _, mpTopology := range input.Cluster.Spec.Topology.Workers.MachinePools {
+			// Continue if the MachinePoolTopology belongs to another MachinePoolClass.
+			if mpTopology.Class != mpClass.Class {
+				continue
+			}
+
+			// NOTE: We only wait until the change is rolled out to the MachinePool objects and not to the worker machines
+			// to speed up the test and focus the test on the ClusterClass feature.
+			log.Logf("Waiting for MachinePool rollout for MachinePoolTopology %q (class %q) to complete.", mpTopology.Name, mpTopology.Class)
+			Eventually(func(g Gomega) error {
+				// Get MachinePool for the current MachinePoolTopology.
+				mpList := &expv1.MachinePoolList{}
+				g.Expect(mgmtClient.List(ctx, mpList, client.InNamespace(input.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]
+
+				// Verify that the fields from Cluster topology are set on the MachinePool.
+				assertMachinePoolTopologyFields(g, mp, mpTopology)
+
+				if mpClass.Template.Bootstrap.Ref != nil {
+					// Get the corresponding BootstrapConfig object.
+					bootstrapConfigObjectRef := mp.Spec.Template.Spec.Bootstrap.ConfigRef
+					bootstrapConfigObject, err := external.Get(ctx, mgmtClient, bootstrapConfigObjectRef, input.Cluster.Namespace)
+					g.Expect(err).ToNot(HaveOccurred())
+
+					// Verify that ModifyBootstrapConfigTemplateFields have been set and propagates to the BootstrapConfig.
+					for fieldPath, expectedValue := range input.ModifyBootstrapConfigTemplateFields {
+						// MachinePools have a BootstrapConfig, not a BootstrapConfigTemplate, so we need to convert the fieldPath so it can find it on the object.
+						fieldPath = strings.TrimPrefix(fieldPath, "spec.template.")
+						currentValue, ok, err := unstructured.NestedFieldNoCopy(bootstrapConfigObject.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))
+					}
+				}
+
+				// Get the corresponding InfrastructureMachinePoolTemplate.
+				infrastructureMachinePoolTemplateRef := mp.Spec.Template.Spec.InfrastructureRef
+				infrastructureMachinePoolTemplate, err := external.Get(ctx, mgmtClient, &infrastructureMachinePoolTemplateRef, 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, ".")...)
+					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))
+				}
+				return nil
+			}, input.WaitForMachinePools...).Should(BeNil())
+		}
+	}
+}
+
 // assertMachineDeploymentTopologyFields asserts that all fields set in the MachineDeploymentTopology have been set on the MachineDeployment.
 // Note: We intentionally focus on the fields set in the MachineDeploymentTopology and ignore the ones set through ClusterClass
 // as we want to validate that the fields of the MachineDeploymentTopology have been propagated correctly.
@@ -464,6 +590,40 @@ func assertMachineDeploymentTopologyFields(g Gomega, md clusterv1.MachineDeploym
 	}
 }
 
+// assertMachinePoolTopologyFields asserts that all fields set in the MachinePoolTopology have been set on the MachinePool.
+// Note: We intentionally focus on the fields set in the MachinePoolTopology and ignore the ones set through ClusterClass
+// as we want to validate that the fields of the MachinePoolTopology have been propagated correctly.
+func assertMachinePoolTopologyFields(g Gomega, mp expv1.MachinePool, mpTopology clusterv1.MachinePoolTopology) {
+	// Note: We only verify that all labels and annotations from the Cluster topology exist to keep it simple here.
+	// This is fully covered by the ClusterClass rollout test.
+	for k, v := range mpTopology.Metadata.Labels {
+		g.Expect(mp.Labels).To(HaveKeyWithValue(k, v))
+	}
+	for k, v := range mpTopology.Metadata.Annotations {
+		g.Expect(mp.Annotations).To(HaveKeyWithValue(k, v))
+	}
+
+	if mpTopology.NodeDrainTimeout != nil {
+		g.Expect(mp.Spec.Template.Spec.NodeDrainTimeout).To(Equal(mpTopology.NodeDrainTimeout))
+	}
+
+	if mpTopology.NodeDeletionTimeout != nil {
+		g.Expect(mp.Spec.Template.Spec.NodeDeletionTimeout).To(Equal(mpTopology.NodeDeletionTimeout))
+	}
+
+	if mpTopology.NodeVolumeDetachTimeout != nil {
+		g.Expect(mp.Spec.Template.Spec.NodeVolumeDetachTimeout).To(Equal(mpTopology.NodeVolumeDetachTimeout))
+	}
+
+	if mpTopology.MinReadySeconds != nil {
+		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))
+	}
+}
+
 // rebaseClusterClassAndWaitInput is the input type for rebaseClusterClassAndWait.
 type rebaseClusterClassAndWaitInput struct {
 	ClusterProxy              framework.ClusterProxy
diff --git a/test/e2e/clusterclass_rollout.go b/test/e2e/clusterclass_rollout.go
index 4746eb499..4b897d553 100644
--- a/test/e2e/clusterclass_rollout.go
+++ b/test/e2e/clusterclass_rollout.go
@@ -40,6 +40,7 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/controllers/external"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
+	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
 	"sigs.k8s.io/cluster-api/internal/contract"
 	"sigs.k8s.io/cluster-api/internal/controllers/topology/machineset"
 	"sigs.k8s.io/cluster-api/test/e2e/internal/log"
@@ -134,10 +135,11 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 			WaitForClusterIntervals:      input.E2EConfig.GetIntervals(specName, "wait-cluster"),
 			WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"),
 			WaitForMachineDeployments:    input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
+			WaitForMachinePools:          input.E2EConfig.GetIntervals(specName, "wait-machine-pool-nodes"),
 		}, clusterResources)
 		assertClusterObjects(ctx, input.BootstrapClusterProxy, clusterResources.Cluster, clusterResources.ClusterClass)
 
-		By("Rolling out changes to control plane and MachineDeployments (in-place)")
+		By("Rolling out changes to control plane, MachineDeployments, and MachinePools (in-place)")
 		machinesBeforeUpgrade := getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster)
 		By("Modifying the control plane configuration via Cluster topology and wait for changes to be applied to the control plane object (in-place)")
 		modifyControlPlaneViaClusterAndWait(ctx, modifyControlPlaneViaClusterAndWaitInput{
@@ -184,6 +186,25 @@ 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)")
+		modifyMachinePoolViaClusterAndWait(ctx, modifyMachinePoolViaClusterAndWaitInput{
+			ClusterProxy: input.BootstrapClusterProxy,
+			Cluster:      clusterResources.Cluster,
+			ModifyMachinePoolTopology: func(topology *clusterv1.MachinePoolTopology) {
+				// Drop existing labels and annotations and set new ones.
+				topology.Metadata.Labels = map[string]string{
+					"Cluster.topology.machinePool.newLabel": "Cluster.topology.machinePool.newLabelValue",
+				}
+				topology.Metadata.Annotations = map[string]string{
+					"Cluster.topology.machinePool.newAnnotation": "Cluster.topology.machinePool.newAnnotationValue",
+				}
+				topology.NodeDrainTimeout = &metav1.Duration{Duration: time.Duration(rand.Intn(20)) * time.Second}        //nolint:gosec
+				topology.NodeDeletionTimeout = &metav1.Duration{Duration: time.Duration(rand.Intn(20)) * time.Second}     //nolint:gosec
+				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"),
+		})
 		By("Verifying there are no unexpected rollouts through in-place rollout")
 		Consistently(func(g Gomega) {
 			machinesAfterUpgrade := getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster)
@@ -191,7 +212,7 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 		}, 30*time.Second, 1*time.Second).Should(Succeed())
 		assertClusterObjects(ctx, input.BootstrapClusterProxy, clusterResources.Cluster, clusterResources.ClusterClass)
 
-		By("Rolling out changes to control plane and MachineDeployments (rollout)")
+		By("Rolling out changes to control plane, MachineDeployments, and MachinePools (rollout)")
 		machinesBeforeUpgrade = getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster)
 		By("Modifying the control plane configuration via ClusterClass and wait for changes to be applied to the control plane object (rollout)")
 		modifyControlPlaneViaClusterClassAndWait(ctx, modifyClusterClassControlPlaneAndWaitInput{
@@ -213,6 +234,16 @@ func ClusterClassRolloutSpec(ctx context.Context, inputGetter func() ClusterClas
 			},
 			WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
 		})
+		By("Modifying the MachinePool configuration via ClusterClass and wait for changes to be applied to the MachinePools (rollout)")
+		modifyMachinePoolViaClusterClassAndWait(ctx, modifyMachinePoolViaClusterClassAndWaitInput{
+			ClusterProxy: input.BootstrapClusterProxy,
+			ClusterClass: clusterResources.ClusterClass,
+			Cluster:      clusterResources.Cluster,
+			ModifyBootstrapConfigTemplateFields: map[string]interface{}{
+				"spec.template.spec.verbosity": int64(4),
+			},
+			WaitForMachinePools: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
+		})
 		By("Verifying all Machines are replaced through rollout")
 		Eventually(func(g Gomega) {
 			machinesAfterUpgrade := getMachinesByCluster(ctx, input.BootstrapClusterProxy.GetClient(), clusterResources.Cluster)
@@ -278,6 +309,9 @@ func assertClusterObjects(ctx context.Context, clusterProxy framework.ClusterPro
 		assertMachineSets(g, clusterObjects, cluster)
 		assertMachineSetsMachines(g, clusterObjects, cluster)
 
+		// MachinePools
+		assertMachinePools(g, clusterClassObjects, clusterObjects, cluster, clusterClass)
+
 		By("All cluster objects have the right labels, annotations and selectors")
 	}, 30*time.Second, 1*time.Second).Should(Succeed())
 }
@@ -590,6 +624,119 @@ func assertMachineDeployments(g Gomega, clusterClassObjects clusterClassObjects,
 	}
 }
 
+func assertMachinePools(g Gomega, clusterClassObjects clusterClassObjects, clusterObjects clusterObjects, cluster *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) {
+	for _, machinePool := range clusterObjects.MachinePools {
+		mpTopology := getMPTopology(cluster, machinePool)
+		mpClass := getMPClass(cluster, clusterClass, machinePool)
+
+		// MachinePool.metadata
+		g.Expect(machinePool.Labels).To(BeEquivalentTo(
+			union(
+				map[string]string{
+					clusterv1.ClusterNameLabel:                    cluster.Name,
+					clusterv1.ClusterTopologyOwnedLabel:           "",
+					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
+				},
+				mpTopology.Metadata.Labels,
+				mpClass.Template.Metadata.Labels,
+			),
+		))
+		g.Expect(
+			union(
+				machinePool.Annotations,
+			).without(g, clusterv1.RevisionAnnotation),
+		).To(BeEquivalentTo(
+			union(
+				mpTopology.Metadata.Annotations,
+				mpClass.Template.Metadata.Annotations,
+			),
+		))
+
+		// MachinePool.spec.template.metadata
+		g.Expect(machinePool.Spec.Template.Labels).To(BeEquivalentTo(
+			union(
+				map[string]string{
+					clusterv1.ClusterNameLabel:                    cluster.Name,
+					clusterv1.ClusterTopologyOwnedLabel:           "",
+					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
+				},
+				mpTopology.Metadata.Labels,
+				mpClass.Template.Metadata.Labels,
+			),
+		))
+		g.Expect(machinePool.Spec.Template.Annotations).To(BeEquivalentTo(
+			union(
+				mpTopology.Metadata.Annotations,
+				mpClass.Template.Metadata.Annotations,
+			),
+		))
+
+		// 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(
+			union(
+				map[string]string{
+					clusterv1.ClusterNameLabel:                    cluster.Name,
+					clusterv1.ClusterTopologyOwnedLabel:           "",
+					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
+				},
+				ccInfrastructureMachineTemplate.GetLabels(),
+			),
+		))
+		g.Expect(infrastructureMachineTemplate.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,
+		))
+
+		// MachinePool BootstrapConfigTemplate.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(
+			union(
+				map[string]string{
+					clusterv1.ClusterNameLabel:                    cluster.Name,
+					clusterv1.ClusterTopologyOwnedLabel:           "",
+					clusterv1.ClusterTopologyMachinePoolNameLabel: mpTopology.Name,
+				},
+				ccBootstrapConfigTemplate.GetLabels(),
+			),
+		))
+		g.Expect(bootstrapConfigTemplate.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,
+		))
+	}
+}
+
 func assertMachineSets(g Gomega, clusterObjects clusterObjects, cluster *clusterv1.Cluster) {
 	for _, machineDeployment := range clusterObjects.MachineDeployments {
 		mdTopology := getMDTopology(cluster, machineDeployment)
@@ -788,6 +935,30 @@ func getMDTopology(cluster *clusterv1.Cluster, md *clusterv1.MachineDeployment)
 	return nil
 }
 
+// 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)
+
+	for _, mdClass := range clusterClass.Spec.Workers.MachinePools {
+		if mdClass.Class == mpTopology.Class {
+			return &mdClass
+		}
+	}
+	Fail(fmt.Sprintf("could not find MachinePool class %q", mpTopology.Class))
+	return nil
+}
+
+// getMPTopology looks up the MachinePoolTopology for a mp in the Cluster.
+func getMPTopology(cluster *clusterv1.Cluster, mp *expv1.MachinePool) *clusterv1.MachinePoolTopology {
+	for _, mpTopology := range cluster.Spec.Topology.Workers.MachinePools {
+		if mpTopology.Name == mp.Labels[clusterv1.ClusterTopologyMachinePoolNameLabel] {
+			return &mpTopology
+		}
+	}
+	Fail(fmt.Sprintf("could not find MachinePool topology %q", mp.Labels[clusterv1.ClusterTopologyMachinePoolNameLabel]))
+	return nil
+}
+
 // groupKind returns the GroupKind string of a ref.
 func groupKind(ref *corev1.ObjectReference) string {
 	gv, err := schema.ParseGroupVersion(ref.APIVersion)
@@ -857,6 +1028,9 @@ type clusterClassObjects struct {
 
 	InfrastructureMachineTemplateByMachineDeploymentClass map[string]*unstructured.Unstructured
 	BootstrapConfigTemplateByMachineDeploymentClass       map[string]*unstructured.Unstructured
+
+	InfrastructureMachinePoolTemplateByMachinePoolClass map[string]*unstructured.Unstructured
+	BootstrapConfigTemplateByMachinePoolClass           map[string]*unstructured.Unstructured
 }
 
 // getClusterClassObjects retrieves objects from the ClusterClass.
@@ -903,9 +1077,14 @@ type clusterObjects struct {
 	MachinesByMachineSet           map[string][]*clusterv1.Machine
 	NodesByMachine                 map[string]*corev1.Node
 
+	MachinePools []*expv1.MachinePool
+
 	InfrastructureMachineTemplateByMachineDeployment map[string]*unstructured.Unstructured
 	BootstrapConfigTemplateByMachineDeployment       map[string]*unstructured.Unstructured
 
+	InfrastructureMachinePoolTemplateByMachinePool map[string]*unstructured.Unstructured
+	BootstrapConfigTemplateByMachinePool           map[string]*unstructured.Unstructured
+
 	InfrastructureMachineByMachine map[string]*unstructured.Unstructured
 	BootstrapConfigByMachine       map[string]*unstructured.Unstructured
 }
@@ -1095,3 +1274,49 @@ func modifyMachineDeploymentViaClusterAndWait(ctx context.Context, input modifyM
 		}
 	}
 }
+
+// modifyMachinePoolViaClusterAndWaitInput is the input type for modifyMachinePoolViaClusterAndWait.
+type modifyMachinePoolViaClusterAndWaitInput struct {
+	ClusterProxy              framework.ClusterProxy
+	Cluster                   *clusterv1.Cluster
+	ModifyMachinePoolTopology func(topology *clusterv1.MachinePoolTopology)
+	WaitForMachinePools       []interface{}
+}
+
+// modifyMachinePoolViaClusterAndWait modifies the MachinePoolTopology of a Cluster topology via ModifyMachinePoolTopology.
+// It then waits until the changes are rolled out to the MachinePools of the Cluster.
+// NOTE: This helper is really specific to this test, so we are keeping this private vs. adding it to the framework.
+func modifyMachinePoolViaClusterAndWait(ctx context.Context, input modifyMachinePoolViaClusterAndWaitInput) {
+	Expect(ctx).NotTo(BeNil(), "ctx is required for modifyMachinePoolViaClusterAndWait")
+	Expect(input.ClusterProxy).ToNot(BeNil(), "Invalid argument. input.ClusterProxy can't be nil when calling modifyMachinePoolViaClusterAndWait")
+	Expect(input.Cluster).ToNot(BeNil(), "Invalid argument. input.Cluster can't be nil when calling modifyMachinePoolViaClusterAndWait")
+
+	mgmtClient := input.ClusterProxy.GetClient()
+
+	for i, mpTopology := range input.Cluster.Spec.Topology.Workers.MachinePools {
+		log.Logf("Modifying the MachinePool topology %q of ClusterClass %s", mpTopology.Name, klog.KObj(input.Cluster))
+
+		// Patch the MachinePool topology in the Cluster.
+		patchHelper, err := patch.NewHelper(input.Cluster, mgmtClient)
+		Expect(err).ToNot(HaveOccurred())
+		input.ModifyMachinePoolTopology(&input.Cluster.Spec.Topology.Workers.MachinePools[i])
+		Expect(patchHelper.Patch(ctx, input.Cluster)).To(Succeed())
+
+		for _, mpTopology := range input.Cluster.Spec.Topology.Workers.MachinePools {
+			// NOTE: We only wait until the change is rolled out to the MachinePools and not to the worker machines.
+			log.Logf("Waiting for MachinePool rollout for MachinePoolTopology %q to complete.", mpTopology.Name)
+			Eventually(func(g Gomega) {
+				// Get MachinePool for the current MachinePoolTopology.
+				mpList := &expv1.MachinePoolList{}
+				g.Expect(mgmtClient.List(ctx, mpList, client.InNamespace(input.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]
+
+				// Verify that the fields from Cluster topology are set on the MachinePool.
+				assertMachinePoolTopologyFields(g, mp, mpTopology)
+			}, input.WaitForMachinePools...).Should(BeNil())
+		}
+	}
+}
diff --git a/test/e2e/self_hosted.go b/test/e2e/self_hosted.go
index 0924ac0e9..484dea5a0 100644
--- a/test/e2e/self_hosted.go
+++ b/test/e2e/self_hosted.go
@@ -365,19 +365,18 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 					MachineDeployments:          clusterResources.MachineDeployments,
 					WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
 				})
-			}
-		}
 
-		// Only attempt to upgrade MachinePools if they were provided in the template.
-		if len(clusterResources.MachinePools) > 0 && workerMachineCount > 0 {
-			By("Upgrading the machinepool instances")
-			framework.UpgradeMachinePoolAndWait(ctx, framework.UpgradeMachinePoolAndWaitInput{
-				ClusterProxy:                   selfHostedClusterProxy,
-				Cluster:                        clusterResources.Cluster,
-				UpgradeVersion:                 input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
-				WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
-				MachinePools:                   clusterResources.MachinePools,
-			})
+				if len(clusterResources.MachinePools) > 0 {
+					By("Upgrading the machinepool instances")
+					framework.UpgradeMachinePoolAndWait(ctx, framework.UpgradeMachinePoolAndWaitInput{
+						ClusterProxy:                   selfHostedClusterProxy,
+						Cluster:                        clusterResources.Cluster,
+						UpgradeVersion:                 input.E2EConfig.GetVariable(KubernetesVersionUpgradeTo),
+						WaitForMachinePoolToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-pool-upgrade"),
+						MachinePools:                   clusterResources.MachinePools,
+					})
+				}
+			}
 		}
 
 		By("Waiting until nodes are ready")
diff --git a/test/go.mod b/test/go.mod
index 381065ec5..0e2acc8d2 100644
--- a/test/go.mod
+++ b/test/go.mod
@@ -63,7 +63,7 @@ require (
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-logr/zapr v1.2.4 // indirect
@@ -122,14 +122,14 @@ require (
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/valyala/fastjson v1.6.4 // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0 // indirect
-	go.opentelemetry.io/otel v1.20.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect
-	go.opentelemetry.io/otel/metric v1.20.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.20.0 // indirect
-	go.opentelemetry.io/otel/trace v1.20.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
+	go.opentelemetry.io/otel v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
+	go.opentelemetry.io/otel/metric v1.19.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.19.0 // indirect
+	go.opentelemetry.io/otel/trace v1.19.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.25.0 // indirect
diff --git a/test/go.sum b/test/go.sum
index 7061ebe36..c06756cc6 100644
--- a/test/go.sum
+++ b/test/go.sum
@@ -136,8 +136,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
 github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
 github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/flatcar/ignition v0.36.2 h1:xGHgScUe0P4Fkprjqv7L2CE58emiQgP833OCCn9z2v4=
 github.com/flatcar/ignition v0.36.2/go.mod h1:uk1tpzLFRXus4RrvzgMI+IqmmB8a/RGFSBlI+tMTbbA=
 github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
@@ -437,27 +437,27 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0 h1:1eHu3/pUSWaOgltNK3WJFaywKsTIr/PwvHyDmi0lQA0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.0/go.mod h1:HyABWq60Uy1kjJSa2BVOxUVao8Cdick5AWSKPutqy6U=
-go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc=
-go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0=
-go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA=
-go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM=
-go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM=
-go.opentelemetry.io/otel/sdk v1.20.0/go.mod h1:rmkSx1cZCm/tn16iWDn1GQbLtsW/LvsdEEFzCSRM6V0=
-go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ=
-go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 h1:xFSRQBbXF6VvYRf2lqMJXxoB72XI1K/azav8TekHHSw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0/go.mod h1:h8TWwRAhQpOd0aM5nYsRD8+flnkj+526GEIVlarH7eY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 h1:KfYpVmrjI7JuToy5k8XV3nkapjWx48k4E4JOtVstzQI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0/go.mod h1:SeQhzAEccGVZVEy7aH87Nh0km+utSpo1pTv6eMMop48=
+go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
+go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
+go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
+go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
+go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
+go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
+go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
+go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
 go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
diff --git a/test/infrastructure/docker/internal/controllers/dockercluster_controller.go b/test/infrastructure/docker/internal/controllers/dockercluster_controller.go
index b5ace259e..231be59eb 100644
--- a/test/infrastructure/docker/internal/controllers/dockercluster_controller.go
+++ b/test/infrastructure/docker/internal/controllers/dockercluster_controller.go
@@ -80,6 +80,12 @@ func (r *DockerClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 	log = log.WithValues("Cluster", klog.KObj(cluster))
 	ctx = ctrl.LoggerInto(ctx, log)
 
+	// Create a helper for managing a docker container hosting the loadbalancer.
+	externalLoadBalancer, err := docker.NewLoadBalancer(ctx, cluster, dockerCluster)
+	if err != nil {
+		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalLoadBalancer")
+	}
+
 	// Initialize the patch helper
 	patchHelper, err := patch.NewHelper(dockerCluster, r.Client)
 	if err != nil {
@@ -95,13 +101,6 @@ func (r *DockerClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		}
 	}()
 
-	// Create a helper for managing a docker container hosting the loadbalancer.
-	externalLoadBalancer, err := docker.NewLoadBalancer(ctx, cluster, dockerCluster)
-	if err != nil {
-		conditions.MarkFalse(dockerCluster, infrav1.LoadBalancerAvailableCondition, infrav1.LoadBalancerProvisioningFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
-		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalLoadBalancer")
-	}
-
 	// Support FailureDomains
 	// In cloud providers this would likely look up which failure domains are supported and set the status appropriately.
 	// In the case of Docker, failure domains don't mean much so we simply copy the Spec into the Status.