Repository: kubernetes-sigs/cluster-api
Pull request #4200 (2021-02-18 09:53:04)

🌱  clusterctl v1alpha4 should not install v1alpha3 providers
**What this PR does / why we need it**:
Cluster API v1alpha4 is introducing changes in how providers are deployed (see e.g webhooks running with manager).

As a consequence, clusterctl version v1alpha4 should not be allowed to install v1alpha3 providers

NB: While doing the change, also clusterctl version v1alpha4 should not be allowed to install v1alpha5 providers was implemented, and tests are now refactored in order to make changes for supporting the next contract less invasive.

This will also ensure easier backport

**Which issue(s) this PR fixes**:
Rif #4191


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

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **fabriziopandini** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @fabriziopandini` in a comment when ready.

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:

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

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | 4a9b1ecc7afc1efdbb25fe1747b1b133b6a18dfa | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362339345104637952) | `/test pull-cluster-api-e2e-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | 4a9b1ecc7afc1efdbb25fe1747b1b133b6a18dfa | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362339345104637952) | `/test pull-cluster-api-e2e-main`
pull-cluster-api-test-main | c62b77bfb77fba02ae8161f32185a3521dcee80a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362394849772310528) | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test-main | c62b77bfb77fba02ae8161f32185a3521dcee80a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362394849772310528) | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-main | c62b77bfb77fba02ae8161f32185a3521dcee80a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362394849801670656) | `/test pull-cluster-api-e2e-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | c62b77bfb77fba02ae8161f32185a3521dcee80a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362394849801670656) | `/test pull-cluster-api-e2e-main`
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362405253667885056) | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362405253667885056) | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362405253701439488) | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=4200). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afabriziopandini). 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: /retest
k8s-ci-robot: @fabriziopandini: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362405253701439488) | `/test pull-cluster-api-e2e-main`
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362407481401151488) | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362407481401151488) | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362407481438900224) | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=4200). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afabriziopandini). 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: /retest
k8s-ci-robot: @fabriziopandini: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362407481438900224) | `/test pull-cluster-api-e2e-main`
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362410135846129664) | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362410135846129664) | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362410135875489792) | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=4200). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afabriziopandini). 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: /retest
k8s-ci-robot: @fabriziopandini: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362410135875489792) | `/test pull-cluster-api-e2e-main`
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362416214680276992) | `/test pull-cluster-api-test-main`

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-test-main/1362416214680276992) | `/test pull-cluster-api-test-main`
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362416214718025728) | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=4200). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afabriziopandini). 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 -->
vincepri: /milestone v0.4.0
fabriziopandini: /retest
k8s-ci-robot: @fabriziopandini: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1362699093553451008) | `/test pull-cluster-api-e2e-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=4200). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Afabriziopandini). 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: /retest
k8s-ci-robot: @fabriziopandini: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e-main | a281f28ca2c51e1f4e18c8763ebdfac1df26c420 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/4200/pull-cluster-api-e2e-main/1363803284636176384) | `/test pull-cluster-api-e2e-main`

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

<details>

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

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **fabriziopandini** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @fabriziopandini` in a comment when ready.

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/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["fabriziopandini"]} -->
vincepri: /retitle 🌱  clusterctl v1alpha4 should not install v1alpha3 providers
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api/pull/4200#pullrequestreview-595458404" title="LGTM">vincepri</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/kubernetes-sigs/cluster-api/blob/master/OWNERS)~~ [vincepri]

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

Patch:
diff --git a/Makefile b/Makefile
index 8663926f6..23f787a65 100644
--- a/Makefile
+++ b/Makefile
@@ -70,7 +70,7 @@ CLOUDINIT_SCRIPT := $(CLOUDINIT_PKG_DIR)/kubeadm-bootstrap-script.sh
 
 # Define Docker related variables. Releases should modify and double check these vars.
 REGISTRY ?= gcr.io/$(shell gcloud config get-value project)
-PROD_REGISTRY ?= k8s.gcr.io/cluster-api
+PROD_REGISTRY ?= us.gcr.io/k8s-artifacts-prod/cluster-api
 
 STAGING_REGISTRY ?= gcr.io/k8s-staging-cluster-api
 STAGING_BUCKET ?= artifacts.k8s-staging-cluster-api.appspot.com
@@ -116,15 +116,11 @@ help:  ## Display this help
 
 .PHONY: test
 test: ## Run tests.
-	source ./scripts/fetch_ext_bins.sh; fetch_tools; setup_envs; go test ./... $(TEST_ARGS)
-
-.PHONY: test-verbose
-test-verbose: ## Run tests with verbose settings.
-	TEST_ARGS="$(TEST_ARGS) -v" $(MAKE) test
+	source ./scripts/fetch_ext_bins.sh; fetch_tools; setup_envs; go test -v ./... $(TEST_ARGS)
 
 .PHONY: test-cover
-test-cover: ## Run tests with code coverage and code generate reports.
-	TEST_ARGS="$(TEST_ARGS) -coverprofile=out/coverage.out" $(MAKE) test
+test-cover: ## Run tests with code coverage and code generate  reports
+	source ./scripts/fetch_ext_bins.sh; fetch_tools; setup_envs; go test -v -coverprofile=out/coverage.out ./... $(TEST_ARGS)
 	go tool cover -func=out/coverage.out -o out/coverage.txt
 	go tool cover -html=out/coverage.out -o out/coverage.html
 
@@ -503,8 +499,6 @@ release-manifests: $(RELEASE_DIR) $(KUSTOMIZE) ## Builds the manifests to publis
 	cat $(RELEASE_DIR)/bootstrap-components.yaml >> $(RELEASE_DIR)/cluster-api-components.yaml
 	echo "---" >> $(RELEASE_DIR)/cluster-api-components.yaml
 	cat $(RELEASE_DIR)/control-plane-components.yaml >> $(RELEASE_DIR)/cluster-api-components.yaml
-	# Add metadata to the release artifacts
-	cp metadata.yaml $(RELEASE_DIR)/metadata.yaml
 
 .PHONY: release-manifests-dev
 release-manifests-dev: ## Builds the development manifests and copies them in the release folder
diff --git a/api/v1alpha4/common_types.go b/api/v1alpha4/common_types.go
index 576c1d312..2062ff8e7 100644
--- a/api/v1alpha4/common_types.go
+++ b/api/v1alpha4/common_types.go
@@ -71,9 +71,6 @@ const (
 	// that was cloned for the machine. This annotation is set only during cloning a template. Older/adopted machines will not have this annotation.
 	TemplateClonedFromGroupKindAnnotation = "cluster.x-k8s.io/cloned-from-groupkind"
 
-	// MachineSkipRemediationAnnotation is the annotation used to mark the machines that should not be considered for remediation by MachineHealthCheck reconciler.
-	MachineSkipRemediationAnnotation = "cluster.x-k8s.io/skip-remediation"
-
 	// ClusterSecretType defines the type of secret created by core components
 	ClusterSecretType corev1.SecretType = "cluster.x-k8s.io/secret" //nolint:gosec
 
diff --git a/api/v1alpha4/machine_webhook.go b/api/v1alpha4/machine_webhook.go
index 6ed488164..e4250a351 100644
--- a/api/v1alpha4/machine_webhook.go
+++ b/api/v1alpha4/machine_webhook.go
@@ -18,11 +18,11 @@ package v1alpha4
 
 import (
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/version"
+	"regexp"
 	"strings"
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/runtime"
+	runtime "k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -40,6 +40,8 @@ func (m *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
 var _ webhook.Validator = &Machine{}
 var _ webhook.Defaulter = &Machine{}
 
+var kubeSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
+
 // Default implements webhook.Defaulter so a webhook will be registered for the type
 func (m *Machine) Default() {
 	if m.Labels == nil {
@@ -122,7 +124,7 @@ func (m *Machine) validate(old *Machine) error {
 	}
 
 	if m.Spec.Version != nil {
-		if !version.KubeSemver.MatchString(*m.Spec.Version) {
+		if !kubeSemver.MatchString(*m.Spec.Version) {
 			allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "version"), *m.Spec.Version, "must be a valid semantic version"))
 		}
 	}
diff --git a/api/v1alpha4/machinedeployment_types.go b/api/v1alpha4/machinedeployment_types.go
index 73eeae9ae..cc274ec5c 100644
--- a/api/v1alpha4/machinedeployment_types.go
+++ b/api/v1alpha4/machinedeployment_types.go
@@ -52,8 +52,6 @@ type MachineDeploymentSpec struct {
 
 	// Number of desired machines. Defaults to 1.
 	// This is a pointer to distinguish between explicit zero and not specified.
-	// +optional
-	// +kubebuilder:default=1
 	Replicas *int32 `json:"replicas,omitempty"`
 
 	// Label selector for machines. Existing MachineSets whose machines are
diff --git a/api/v1alpha4/machinedeployment_webhook.go b/api/v1alpha4/machinedeployment_webhook.go
index 68a9f4ba6..c55241a17 100644
--- a/api/v1alpha4/machinedeployment_webhook.go
+++ b/api/v1alpha4/machinedeployment_webhook.go
@@ -107,6 +107,10 @@ func PopulateDefaultsMachineDeployment(d *MachineDeployment) {
 	}
 	d.Labels[ClusterLabelName] = d.Spec.ClusterName
 
+	if d.Spec.Replicas == nil {
+		d.Spec.Replicas = pointer.Int32Ptr(1)
+	}
+
 	if d.Spec.MinReadySeconds == nil {
 		d.Spec.MinReadySeconds = pointer.Int32Ptr(0)
 	}
diff --git a/api/v1alpha4/machineset_types.go b/api/v1alpha4/machineset_types.go
index afd415a59..4df1b426d 100644
--- a/api/v1alpha4/machineset_types.go
+++ b/api/v1alpha4/machineset_types.go
@@ -36,7 +36,6 @@ type MachineSetSpec struct {
 	// This is a pointer to distinguish between explicit zero and unspecified.
 	// Defaults to 1.
 	// +optional
-	// +kubebuilder:default=1
 	Replicas *int32 `json:"replicas,omitempty"`
 
 	// MinReadySeconds is the minimum number of seconds for which a newly created machine should be ready.
diff --git a/api/v1alpha4/machineset_webhook.go b/api/v1alpha4/machineset_webhook.go
index 5a2fac2e3..9d37556ae 100644
--- a/api/v1alpha4/machineset_webhook.go
+++ b/api/v1alpha4/machineset_webhook.go
@@ -24,6 +24,7 @@ import (
 	"k8s.io/apimachinery/pkg/labels"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
+	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 )
@@ -47,6 +48,10 @@ func (m *MachineSet) Default() {
 	}
 	m.Labels[ClusterLabelName] = m.Spec.ClusterName
 
+	if m.Spec.Replicas == nil {
+		m.Spec.Replicas = pointer.Int32Ptr(1)
+	}
+
 	if m.Spec.DeletePolicy == "" {
 		randomPolicy := string(RandomMachineSetDeletePolicy)
 		m.Spec.DeletePolicy = randomPolicy
diff --git a/bootstrap/kubeadm/main.go b/bootstrap/kubeadm/main.go
index 3c3ada523..c28ed1364 100644
--- a/bootstrap/kubeadm/main.go
+++ b/bootstrap/kubeadm/main.go
@@ -69,7 +69,6 @@ var (
 	kubeadmConfigConcurrency    int
 	syncPeriod                  time.Duration
 	webhookPort                 int
-	webhookCertDir              string
 )
 
 func InitFlags(fs *pflag.FlagSet) {
@@ -106,9 +105,6 @@ func InitFlags(fs *pflag.FlagSet) {
 	fs.IntVar(&webhookPort, "webhook-port", 9443,
 		"Webhook Server port")
 
-	fs.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/",
-		"Webhook cert dir, only used when webhook-port is specified.")
-
 	feature.MutableGates.AddFlag(fs)
 }
 
@@ -142,8 +138,7 @@ func main() {
 			&corev1.ConfigMap{},
 			&corev1.Secret{},
 		},
-		Port:    webhookPort,
-		CertDir: webhookCertDir,
+		Port: webhookPort,
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")
diff --git a/bootstrap/kubeadm/types/v1beta2/groupversion_info.go b/bootstrap/kubeadm/types/v1beta2/groupversion_info.go
deleted file mode 100644
index 903a9d561..000000000
--- a/bootstrap/kubeadm/types/v1beta2/groupversion_info.go
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1beta2
-
-import (
-	"k8s.io/apimachinery/pkg/runtime/schema"
-)
-
-var (
-	// GroupVersion is group version used to register these objects
-	GroupVersion = schema.GroupVersion{Group: "kubeadm.k8s.io", Version: "v1beta2"}
-)
diff --git a/bootstrap/kubeadm/types/v1beta2/types.go b/bootstrap/kubeadm/types/v1beta2/types.go
index 24109a1a8..7f604557d 100644
--- a/bootstrap/kubeadm/types/v1beta2/types.go
+++ b/bootstrap/kubeadm/types/v1beta2/types.go
@@ -34,13 +34,9 @@ type InitConfiguration struct {
 
 	// BootstrapTokens is respected at `kubeadm init` time and describes a set of Bootstrap Tokens to create.
 	// This information IS NOT uploaded to the kubeadm cluster configmap, partly because of its sensitive nature
-	// +optional
 	BootstrapTokens []BootstrapToken `json:"bootstrapTokens,omitempty"`
 
-	// NodeRegistration holds fields that relate to registering the new control-plane node to the cluster.
-	// When used in the context of control plane nodes, NodeRegistration should remain consistent
-	// across both InitConfiguration and JoinConfiguration
-	// +optional
+	// NodeRegistration holds fields that relate to registering the new control-plane node to the cluster
 	NodeRegistration NodeRegistrationOptions `json:"nodeRegistration,omitempty"`
 
 	// LocalAPIEndpoint represents the endpoint of the API server instance that's deployed on this control plane node
@@ -49,12 +45,10 @@ type InitConfiguration struct {
 	// configuration object lets you customize what IP/DNS name and port the local API server advertises it's accessible
 	// on. By default, kubeadm tries to auto-detect the IP of the default interface and use that, but in case that process
 	// fails you may set the desired value here.
-	// +optional
 	LocalAPIEndpoint APIEndpoint `json:"localAPIEndpoint,omitempty"`
 
 	// CertificateKey sets the key with which certificates and keys are encrypted prior to being uploaded in
 	// a secret in the cluster during the uploadcerts init phase.
-	// +optional
 	CertificateKey string `json:"certificateKey,omitempty"`
 }
 
@@ -65,18 +59,12 @@ type ClusterConfiguration struct {
 	metav1.TypeMeta `json:",inline"`
 
 	// Etcd holds configuration for etcd.
-	// NB: This value defaults to a Local (stacked) etcd
-	// +optional
 	Etcd Etcd `json:"etcd,omitempty"`
 
 	// Networking holds configuration for the networking topology of the cluster.
-	// NB: This value defaults to the Cluster object spec.clusterNetwork.
-	// +optional
 	Networking Networking `json:"networking,omitempty"`
 
 	// KubernetesVersion is the target version of the control plane.
-	// NB: This value defaults to the Machine object spec.version
-	// +optional
 	KubernetesVersion string `json:"kubernetesVersion,omitempty"`
 
 	// ControlPlaneEndpoint sets a stable IP address or DNS name for the control plane; it
@@ -90,50 +78,36 @@ type ClusterConfiguration struct {
 	// control plane instances.
 	// e.g.  in environments with enforced node recycling, the ControlPlaneEndpoint
 	// could be used for assigning a stable DNS to the control plane.
-	// NB: This value defaults to the first value in the Cluster object status.apiEndpoints array.
-	// +optional
 	ControlPlaneEndpoint string `json:"controlPlaneEndpoint,omitempty"`
 
 	// APIServer contains extra settings for the API server control plane component
-	// +optional
 	APIServer APIServer `json:"apiServer,omitempty"`
 
 	// ControllerManager contains extra settings for the controller manager control plane component
-	// +optional
 	ControllerManager ControlPlaneComponent `json:"controllerManager,omitempty"`
 
 	// Scheduler contains extra settings for the scheduler control plane component
-	// +optional
 	Scheduler ControlPlaneComponent `json:"scheduler,omitempty"`
 
 	// DNS defines the options for the DNS add-on installed in the cluster.
-	// +optional
 	DNS DNS `json:"dns,omitempty"`
 
 	// CertificatesDir specifies where to store or look for all required certificates.
-	// NB: if not provided, this will default to `/etc/kubernetes/pki`
-	// +optional
 	CertificatesDir string `json:"certificatesDir,omitempty"`
 
 	// ImageRepository sets the container registry to pull images from.
 	// If empty, `k8s.gcr.io` will be used by default; in case of kubernetes version is a CI build (kubernetes version starts with `ci/` or `ci-cross/`)
 	// `gcr.io/k8s-staging-ci-images` will be used as a default for control plane components and for kube-proxy, while `k8s.gcr.io`
 	// will be used for all the other images.
-	// +optional
 	ImageRepository string `json:"imageRepository,omitempty"`
 
 	// UseHyperKubeImage controls if hyperkube should be used for Kubernetes components instead of their respective separate images
-	// DEPRECATED: As hyperkube is itself deprecated, this fields is too. It will be removed in future kubeadm config versions, kubeadm
-	// will print multiple warnings when set to true, and at some point it may become ignored.
-	// +optional
 	UseHyperKubeImage bool `json:"useHyperKubeImage,omitempty"`
 
 	// FeatureGates enabled by the user.
-	// +optional
 	FeatureGates map[string]bool `json:"featureGates,omitempty"`
 
 	// The cluster name
-	// +optional
 	ClusterName string `json:"clusterName,omitempty"`
 }
 
@@ -173,8 +147,7 @@ const (
 // DNS defines the DNS addon that should be used in the cluster
 type DNS struct {
 	// Type defines the DNS add-on to be used
-	// +optional
-	Type DNSAddOnType `json:"type,omitempty"`
+	Type DNSAddOnType `json:"type"`
 
 	// ImageMeta allows to customize the image used for the DNS component
 	ImageMeta `json:",inline"`
@@ -222,44 +195,32 @@ type NodeRegistrationOptions struct {
 	// Name is the `.Metadata.Name` field of the Node API object that will be created in this `kubeadm init` or `kubeadm join` operation.
 	// This field is also used in the CommonName field of the kubelet's client certificate to the API server.
 	// Defaults to the hostname of the node if not provided.
-	// +optional
 	Name string `json:"name,omitempty"`
 
 	// CRISocket is used to retrieve container runtime info. This information will be annotated to the Node API object, for later re-use
-	// +optional
 	CRISocket string `json:"criSocket,omitempty"`
 
 	// Taints specifies the taints the Node API object should be registered with. If this field is unset, i.e. nil, in the `kubeadm init` process
 	// it will be defaulted to []corev1.Taint{'node-role.kubernetes.io/master=""'}. If you don't want to taint your control-plane node, set this field to an
 	// empty slice, i.e. `taints: {}` in the YAML file. This field is solely used for Node registration.
-	// +optional
-	Taints []corev1.Taint `json:"taints,omitempty"`
+	Taints []corev1.Taint `json:"taints"`
 
 	// KubeletExtraArgs passes through extra arguments to the kubelet. The arguments here are passed to the kubelet command line via the environment file
 	// kubeadm writes at runtime for the kubelet to source. This overrides the generic base-level configuration in the kubelet-config-1.X ConfigMap
 	// Flags have higher priority when parsing. These values are local and specific to the node kubeadm is executing on.
-	// +optional
 	KubeletExtraArgs map[string]string `json:"kubeletExtraArgs,omitempty"`
 
 	// IgnorePreflightErrors provides a slice of pre-flight errors to be ignored when the current node is registered.
-	// +optional
 	IgnorePreflightErrors []string `json:"ignorePreflightErrors,omitempty"`
 }
 
 // Networking contains elements describing cluster's networking configuration
 type Networking struct {
-	// ServiceSubnet is the subnet used by k8s services.
-	// Defaults to a comma-delimited string of the Cluster object's spec.clusterNetwork.pods.cidrBlocks, or
-	// to "10.96.0.0/12" if that's unset.
-	// +optional
+	// ServiceSubnet is the subnet used by k8s services. Defaults to "10.96.0.0/12".
 	ServiceSubnet string `json:"serviceSubnet,omitempty"`
 	// PodSubnet is the subnet used by pods.
-	// If unset, the API server will not allocate CIDR ranges for every node.
-	// Defaults to a comma-delimited string of the Cluster object's spec.clusterNetwork.services.cidrBlocks if that is set
-	// +optional
 	PodSubnet string `json:"podSubnet,omitempty"`
 	// DNSDomain is the dns domain used by k8s services. Defaults to "cluster.local".
-	// +optional
 	DNSDomain string `json:"dnsDomain,omitempty"`
 }
 
@@ -342,25 +303,19 @@ type ExternalEtcd struct {
 type JoinConfiguration struct {
 	metav1.TypeMeta `json:",inline"`
 
-	// NodeRegistration holds fields that relate to registering the new control-plane node to the cluster.
-	// When used in the context of control plane nodes, NodeRegistration should remain consistent
-	// across both InitConfiguration and JoinConfiguration
-	// +optional
+	// NodeRegistration holds fields that relate to registering the new control-plane node to the cluster
 	NodeRegistration NodeRegistrationOptions `json:"nodeRegistration,omitempty"`
 
 	// CACertPath is the path to the SSL certificate authority used to
 	// secure comunications between node and control-plane.
 	// Defaults to "/etc/kubernetes/pki/ca.crt".
-	// +optional
 	CACertPath string `json:"caCertPath,omitempty"`
 
 	// Discovery specifies the options for the kubelet to use during the TLS Bootstrap process
-	// +optional
-	Discovery Discovery `json:"discovery,omitempty"`
+	Discovery Discovery `json:"discovery"`
 
 	// ControlPlane defines the additional control plane instance to be deployed on the joining node.
 	// If nil, no additional control plane instance will be deployed.
-	// +optional
 	ControlPlane *JoinControlPlane `json:"controlPlane,omitempty"`
 }
 
@@ -371,7 +326,6 @@ type JoinControlPlane struct {
 
 	// CertificateKey is the key that is used for decryption of certificates after they are downloaded from the secret
 	// upon joining a new control plane node. The corresponding encryption key is in the InitConfiguration.
-	// +optional
 	CertificateKey string `json:"certificateKey,omitempty"`
 }
 
@@ -388,7 +342,6 @@ type Discovery struct {
 	// TLSBootstrapToken is a token used for TLS bootstrapping.
 	// If .BootstrapToken is set, this field is defaulted to .BootstrapToken.Token, but can be overridden.
 	// If .File is set, this field **must be set** in case the KubeConfigFile does not contain any other authentication information
-	// +optional
 	TLSBootstrapToken string `json:"tlsBootstrapToken,omitempty"`
 
 	// Timeout modifies the discovery timeout
@@ -411,6 +364,7 @@ type BootstrapTokenDiscovery struct {
 	// where the only currently supported type is "sha256". This is a hex-encoded
 	// SHA-256 hash of the Subject Public Key Info (SPKI) object in DER-encoded
 	// ASN.1. These hashes can be calculated using, for example, OpenSSL:
+	// openssl x509 -pubkey -in ca.crt openssl rsa -pubin -outform der 2>&/dev/null | openssl dgst -sha256 -hex
 	CACertHashes []string `json:"caCertHashes,omitempty"`
 
 	// UnsafeSkipCAVerification allows token-based discovery
diff --git a/cmd/clusterctl/client/cluster/installer.go b/cmd/clusterctl/client/cluster/installer.go
index abe639d5b..6de499362 100644
--- a/cmd/clusterctl/client/cluster/installer.go
+++ b/cmd/clusterctl/client/cluster/installer.go
@@ -20,6 +20,7 @@ import (
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/apimachinery/pkg/util/version"
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
@@ -244,6 +245,10 @@ func (i *providerInstaller) getProviderContract(providerInstanceContracts map[st
 		return "", errors.Errorf("invalid provider metadata: version %s for the provider %s does not match any release series", provider.Version, provider.InstanceName())
 	}
 
+	if releaseSeries.Contract != clusterv1.GroupVersion.Version {
+		return "", errors.Errorf("current version of clusterctl could install only %s providers, detected %s for provider %s", clusterv1.GroupVersion.Version, releaseSeries.Contract, provider.ManifestLabel())
+	}
+
 	providerInstanceContracts[provider.InstanceName()] = releaseSeries.Contract
 	return releaseSeries.Contract, nil
 }
diff --git a/cmd/clusterctl/cmd/completion.go b/cmd/clusterctl/cmd/completion.go
index bbf4405c8..da391fea2 100644
--- a/cmd/clusterctl/cmd/completion.go
+++ b/cmd/clusterctl/cmd/completion.go
@@ -131,10 +131,7 @@ func runCompletionZsh(out io.Writer, cmd *cobra.Command) error {
 	if err != nil {
 		return err
 	}
-	fmt.Fprintf(out, "%s\n%s%s\n", string(line), completionBoilerPlate, b.String())
-
-	// Cobra doesn't source zsh completion file, explicitly doing it here
-	fmt.Fprintln(out, "compdef _clusterctl clusterctl")
+	fmt.Fprintf(out, "%s\n%s%s", string(line), completionBoilerPlate, b.String())
 
 	return nil
 }
diff --git a/cmd/clusterctl/hack/create-local-repository.py b/cmd/clusterctl/hack/create-local-repository.py
index 1e2610ef3..69cbef14b 100755
--- a/cmd/clusterctl/hack/create-local-repository.py
+++ b/cmd/clusterctl/hack/create-local-repository.py
@@ -44,7 +44,6 @@ import json
 import subprocess
 import os
 from distutils.dir_util import copy_tree
-from distutils.file_util import copy_file
 import errno
 import sys
 
@@ -53,24 +52,24 @@ settings = {}
 providers = {
       'cluster-api': {
               'componentsFile': 'core-components.yaml',
-              'nextVersion': 'v0.4.0',
+              'nextVersion': 'v0.3.8',
               'type': 'CoreProvider',
       },
       'bootstrap-kubeadm': {
             'componentsFile': 'bootstrap-components.yaml',
-            'nextVersion': 'v0.4.0',
+            'nextVersion': 'v0.3.8',
             'type': 'BootstrapProvider',
             'configFolder': 'bootstrap/kubeadm/config/default',
       },
       'control-plane-kubeadm': {
             'componentsFile': 'control-plane-components.yaml',
-            'nextVersion': 'v0.4.0',
+            'nextVersion': 'v0.3.8',
             'type': 'ControlPlaneProvider',
             'configFolder': 'controlplane/kubeadm/config/default',
       },
       'infrastructure-docker': {
           'componentsFile': 'infrastructure-components.yaml',
-          'nextVersion': 'v0.4.0',
+          'nextVersion': 'v0.3.8',
           'type': 'InfrastructureProvider',
           'configFolder': 'test/infrastructure/docker/config/default',
       },
@@ -117,7 +116,7 @@ def get_repository_folder():
     home = get_home()
     return os.path.join(home, '.cluster-api', 'dev-repository')
 
-def write_local_repository(provider, version, components_file, components_yaml, metadata_file):
+def write_local_repository(provider, version, components_file, components_yaml):
     try:
         repository_folder = get_repository_folder()
         provider_folder = os.path.join(repository_folder, provider, version)
@@ -131,8 +130,6 @@ def write_local_repository(provider, version, components_file, components_yaml,
         f.write(components_yaml)
         f.close()
 
-        copy_file(metadata_file, provider_folder)
-
         if provider == "infrastructure-docker":
             copy_tree("test/infrastructure/docker/templates", provider_folder)
 
@@ -151,7 +148,6 @@ def create_local_repositories():
 
         repo = p.get('repo', '.')
         config_folder = p.get('configFolder', 'config/default')
-        metadata_file = repo+'/metadata.yaml'
 
         next_version = p.get('nextVersion')
         assert next_version is not None, 'invalid configuration for provider {}: please provide nextVersion value'.format(provider)
@@ -163,7 +159,7 @@ def create_local_repositories():
         assert components_file is not None, 'invalid configuration for provider {}: please provide componentsFile value'.format(provider)
 
         components_yaml = execCmd(['kustomize', 'build', os.path.join(repo, config_folder)])
-        components_path = write_local_repository(provider, next_version, components_file, components_yaml, metadata_file)
+        components_path = write_local_repository(provider, next_version, components_file, components_yaml)
 
         yield name, type, next_version, components_path
 
diff --git a/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml b/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml
index 3fbae417c..785efab72 100644
--- a/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml
+++ b/config/crd/bases/cluster.x-k8s.io_machinedeployments.yaml
@@ -375,7 +375,6 @@ spec:
                 format: int32
                 type: integer
               replicas:
-                default: 1
                 description: Number of desired machines. Defaults to 1. This is a pointer to distinguish between explicit zero and not specified.
                 format: int32
                 type: integer
diff --git a/config/crd/bases/cluster.x-k8s.io_machinesets.yaml b/config/crd/bases/cluster.x-k8s.io_machinesets.yaml
index 8f43f6015..110b66d0c 100644
--- a/config/crd/bases/cluster.x-k8s.io_machinesets.yaml
+++ b/config/crd/bases/cluster.x-k8s.io_machinesets.yaml
@@ -329,7 +329,6 @@ spec:
                 format: int32
                 type: integer
               replicas:
-                default: 1
                 description: Replicas is the number of desired replicas. This is a pointer to distinguish between explicit zero and unspecified. Defaults to 1.
                 format: int32
                 type: integer
diff --git a/controllers/external/tracker.go b/controllers/external/tracker.go
index 8c8a6546b..29088fdac 100644
--- a/controllers/external/tracker.go
+++ b/controllers/external/tracker.go
@@ -44,8 +44,7 @@ func (o *ObjectTracker) Watch(log logr.Logger, obj runtime.Object, handler handl
 	}
 
 	gvk := obj.GetObjectKind().GroupVersionKind()
-	key := gvk.GroupKind().String()
-	_, loaded := o.m.LoadOrStore(key, struct{}{})
+	_, loaded := o.m.LoadOrStore(gvk.GroupKind().String(), struct{}{})
 	if loaded {
 		return nil
 	}
@@ -60,7 +59,7 @@ func (o *ObjectTracker) Watch(log logr.Logger, obj runtime.Object, handler handl
 		predicates.ResourceNotPaused(log),
 	)
 	if err != nil {
-		o.m.Delete(key)
+		o.m.Delete(obj)
 		return errors.Wrapf(err, "failed to add watcher on external object %q", gvk.String())
 	}
 	return nil
diff --git a/controllers/machinehealthcheck_controller.go b/controllers/machinehealthcheck_controller.go
index c5e145831..935bd8a28 100644
--- a/controllers/machinehealthcheck_controller.go
+++ b/controllers/machinehealthcheck_controller.go
@@ -194,7 +194,7 @@ func (r *MachineHealthCheckReconciler) reconcile(ctx context.Context, logger log
 
 	// fetch all targets
 	logger.V(3).Info("Finding targets")
-	targets, err := r.getTargetsFromMHC(ctx, logger, remoteClient, m)
+	targets, err := r.getTargetsFromMHC(ctx, remoteClient, m)
 	if err != nil {
 		logger.Error(err, "Failed to fetch targets from MachineHealthCheck")
 		return ctrl.Result{}, err
diff --git a/controllers/machinehealthcheck_targets.go b/controllers/machinehealthcheck_targets.go
index 5493b54ef..3de56dbed 100644
--- a/controllers/machinehealthcheck_targets.go
+++ b/controllers/machinehealthcheck_targets.go
@@ -28,7 +28,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -160,7 +159,7 @@ func (t *healthCheckTarget) needsRemediation(logger logr.Logger, timeoutForMachi
 
 // getTargetsFromMHC uses the MachineHealthCheck's selector to fetch machines
 // and their nodes targeted by the health check, ready for health checking.
-func (r *MachineHealthCheckReconciler) getTargetsFromMHC(ctx context.Context, logger logr.Logger, clusterClient client.Reader, mhc *clusterv1.MachineHealthCheck) ([]healthCheckTarget, error) {
+func (r *MachineHealthCheckReconciler) getTargetsFromMHC(ctx context.Context, clusterClient client.Reader, mhc *clusterv1.MachineHealthCheck) ([]healthCheckTarget, error) {
 	machines, err := r.getMachinesFromMHC(ctx, mhc)
 	if err != nil {
 		return nil, errors.Wrap(err, "error getting machines from MachineHealthCheck")
@@ -171,12 +170,6 @@ func (r *MachineHealthCheckReconciler) getTargetsFromMHC(ctx context.Context, lo
 
 	targets := []healthCheckTarget{}
 	for k := range machines {
-		skip, reason := shouldSkipRemediation(&machines[k])
-		if skip {
-			logger.Info("skipping remediation", "machine", machines[k].Name, "reason", reason)
-			continue
-		}
-
 		patchHelper, err := patch.NewHelper(&machines[k], r.Client)
 		if err != nil {
 			return nil, errors.Wrap(err, "unable to initialize patch helper")
@@ -305,17 +298,3 @@ func minDuration(durations []time.Duration) time.Duration {
 	}
 	return minDuration
 }
-
-// shouldSkipRemediation checks if the machine should be skipped for remediation.
-// Returns true if it should be skipped along with the reason for skipping.
-func shouldSkipRemediation(m *clusterv1.Machine) (bool, string) {
-	if annotations.HasPausedAnnotation(m) {
-		return true, fmt.Sprintf("machine has %q annotation", clusterv1.PausedAnnotation)
-	}
-
-	if annotations.HasSkipRemediationAnnotation(m) {
-		return true, fmt.Sprintf("machine has %q annotation", clusterv1.MachineSkipRemediationAnnotation)
-	}
-
-	return false, ""
-}
diff --git a/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook.go b/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook.go
index a1eb29c0a..35a4897d9 100644
--- a/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook.go
+++ b/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook.go
@@ -19,6 +19,7 @@ package v1alpha4
 import (
 	"encoding/json"
 	"fmt"
+	"regexp"
 	"strings"
 
 	"github.com/blang/semver"
@@ -29,8 +30,8 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1"
+	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/container"
-	"sigs.k8s.io/cluster-api/util/version"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
 )
@@ -47,6 +48,8 @@ func (in *KubeadmControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error {
 var _ webhook.Defaulter = &KubeadmControlPlane{}
 var _ webhook.Validator = &KubeadmControlPlane{}
 
+var kubeSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
+
 // Default implements webhook.Defaulter so a webhook will be registered for the type
 func (in *KubeadmControlPlane) Default() {
 	if in.Spec.Replicas == nil {
@@ -265,7 +268,7 @@ func (in *KubeadmControlPlane) validateCommon() (allErrs field.ErrorList) {
 		)
 	}
 
-	if !version.KubeSemver.MatchString(in.Spec.Version) {
+	if !kubeSemver.MatchString(in.Spec.Version) {
 		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "version"), in.Spec.Version, "must be a valid semantic version"))
 	}
 
@@ -305,7 +308,7 @@ func (in *KubeadmControlPlane) validateCoreDNSVersion(prev *KubeadmControlPlane)
 		return allErrs
 	}
 
-	fromVersion, err := version.ParseMajorMinorPatchTolerant(prev.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag)
+	fromVersion, err := util.ParseMajorMinorPatch(prev.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag)
 	if err != nil {
 		allErrs = append(allErrs,
 			field.InternalError(
@@ -316,7 +319,7 @@ func (in *KubeadmControlPlane) validateCoreDNSVersion(prev *KubeadmControlPlane)
 		return allErrs
 	}
 
-	toVersion, err := version.ParseMajorMinorPatchTolerant(targetDNS.ImageTag)
+	toVersion, err := util.ParseMajorMinorPatch(targetDNS.ImageTag)
 	if err != nil {
 		allErrs = append(allErrs,
 			field.Invalid(
@@ -368,7 +371,7 @@ func (in *KubeadmControlPlane) validateEtcd(prev *KubeadmControlPlane) (allErrs
 	}
 
 	// update validations
-	if prev != nil && prev.Spec.KubeadmConfigSpec.ClusterConfiguration != nil {
+	if prev != nil {
 		if in.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.External != nil && prev.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local != nil {
 			allErrs = append(
 				allErrs,
@@ -394,7 +397,7 @@ func (in *KubeadmControlPlane) validateEtcd(prev *KubeadmControlPlane) (allErrs
 }
 
 func (in *KubeadmControlPlane) validateVersion(previousVersion string) (allErrs field.ErrorList) {
-	fromVersion, err := version.ParseMajorMinorPatch(previousVersion)
+	fromVersion, err := util.ParseMajorMinorPatch(previousVersion)
 	if err != nil {
 		allErrs = append(allErrs,
 			field.InternalError(
@@ -405,7 +408,7 @@ func (in *KubeadmControlPlane) validateVersion(previousVersion string) (allErrs
 		return allErrs
 	}
 
-	toVersion, err := version.ParseMajorMinorPatch(in.Spec.Version)
+	toVersion, err := util.ParseMajorMinorPatch(in.Spec.Version)
 	if err != nil {
 		allErrs = append(allErrs,
 			field.InternalError(
diff --git a/controlplane/kubeadm/controllers/controller.go b/controlplane/kubeadm/controllers/controller.go
index bb9609cff..6ddf82952 100644
--- a/controlplane/kubeadm/controllers/controller.go
+++ b/controlplane/kubeadm/controllers/controller.go
@@ -19,7 +19,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"time"
 
 	"github.com/blang/semver"
@@ -37,6 +36,7 @@ import (
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/conditions"
@@ -270,20 +270,20 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, cluster *
 		return result, err
 	}
 
-	controlPlaneMachines, err := r.managementClusterUncached.GetMachinesForCluster(ctx, util.ObjectKey(cluster), collections.ControlPlaneMachines(cluster.Name))
+	controlPlaneMachines, err := r.managementClusterUncached.GetMachinesForCluster(ctx, util.ObjectKey(cluster), machinefilters.ControlPlaneMachines(cluster.Name))
 	if err != nil {
 		log.Error(err, "failed to retrieve control plane machines for cluster")
 		return ctrl.Result{}, err
 	}
 
-	adoptableMachines := controlPlaneMachines.Filter(collections.AdoptableControlPlaneMachines(cluster.Name))
+	adoptableMachines := controlPlaneMachines.Filter(machinefilters.AdoptableControlPlaneMachines(cluster.Name))
 	if len(adoptableMachines) > 0 {
 		// We adopt the Machines and then wait for the update event for the ownership reference to re-queue them so the cache is up-to-date
 		err = r.adoptMachines(ctx, kcp, adoptableMachines, cluster)
 		return ctrl.Result{}, err
 	}
 
-	ownedMachines := controlPlaneMachines.Filter(collections.OwnedMachines(kcp))
+	ownedMachines := controlPlaneMachines.Filter(machinefilters.OwnedMachines(kcp))
 	if len(ownedMachines) != len(controlPlaneMachines) {
 		log.Info("Not all control plane machines are owned by this KubeadmControlPlane, refusing to operate in mixed management mode")
 		return ctrl.Result{}, nil
@@ -353,7 +353,7 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, cluster *
 	case numMachines > desiredReplicas:
 		log.Info("Scaling down control plane", "Desired", desiredReplicas, "Existing", numMachines)
 		// The last parameter (i.e. machines needing to be rolled out) should always be empty here.
-		return r.scaleDownControlPlane(ctx, cluster, kcp, controlPlane, collections.Machines{})
+		return r.scaleDownControlPlane(ctx, cluster, kcp, controlPlane, internal.FilterableMachineCollection{})
 	}
 
 	// Get the workload cluster client.
@@ -394,7 +394,7 @@ func (r *KubeadmControlPlaneReconciler) reconcileDelete(ctx context.Context, clu
 	if err != nil {
 		return ctrl.Result{}, err
 	}
-	ownedMachines := allMachines.Filter(collections.OwnedMachines(kcp))
+	ownedMachines := allMachines.Filter(machinefilters.OwnedMachines(kcp))
 
 	// If no control plane machines remain, remove the finalizer
 	if len(ownedMachines) == 0 {
@@ -428,7 +428,7 @@ func (r *KubeadmControlPlaneReconciler) reconcileDelete(ctx context.Context, clu
 	}
 
 	// Delete control plane machines in parallel
-	machinesToDelete := ownedMachines.Filter(collections.Not(collections.HasDeletionTimestamp))
+	machinesToDelete := ownedMachines.Filter(machinefilters.Not(machinefilters.HasDeletionTimestamp))
 	var errs []error
 	for i := range machinesToDelete {
 		m := machinesToDelete[i]
@@ -542,7 +542,7 @@ func (r *KubeadmControlPlaneReconciler) reconcileEtcdMembers(ctx context.Context
 	return ctrl.Result{}, nil
 }
 
-func (r *KubeadmControlPlaneReconciler) adoptMachines(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, machines collections.Machines, cluster *clusterv1.Cluster) error {
+func (r *KubeadmControlPlaneReconciler) adoptMachines(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, machines internal.FilterableMachineCollection, cluster *clusterv1.Cluster) error {
 	// We do an uncached full quorum read against the KCP to avoid re-adopting Machines the garbage collector just intentionally orphaned
 	// See https://github.com/kubernetes/kubernetes/issues/42639
 	uncached := controlplanev1.KubeadmControlPlane{}
diff --git a/controlplane/kubeadm/controllers/scale.go b/controlplane/kubeadm/controllers/scale.go
index 890002654..a291bc780 100644
--- a/controlplane/kubeadm/controllers/scale.go
+++ b/controlplane/kubeadm/controllers/scale.go
@@ -18,7 +18,6 @@ package controllers
 
 import (
 	"context"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -28,6 +27,7 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -38,7 +38,7 @@ func (r *KubeadmControlPlaneReconciler) initializeControlPlane(ctx context.Conte
 
 	// Perform an uncached read of all the owned machines. This check is in place to make sure
 	// that the controller cache is not misbehaving and we end up initializing the cluster more than once.
-	ownedMachines, err := r.managementClusterUncached.GetMachinesForCluster(ctx, util.ObjectKey(cluster), collections.OwnedMachines(kcp))
+	ownedMachines, err := r.managementClusterUncached.GetMachinesForCluster(ctx, util.ObjectKey(cluster), machinefilters.OwnedMachines(kcp))
 	if err != nil {
 		logger.Error(err, "failed to perform an uncached read of control plane machines for cluster")
 		return ctrl.Result{}, err
@@ -88,7 +88,7 @@ func (r *KubeadmControlPlaneReconciler) scaleDownControlPlane(
 	cluster *clusterv1.Cluster,
 	kcp *controlplanev1.KubeadmControlPlane,
 	controlPlane *internal.ControlPlane,
-	outdatedMachines collections.Machines,
+	outdatedMachines internal.FilterableMachineCollection,
 ) (ctrl.Result, error) {
 	logger := controlPlane.Logger()
 
@@ -164,7 +164,7 @@ func (r *KubeadmControlPlaneReconciler) preflightChecks(_ context.Context, contr
 
 	// If there are deleting machines, wait for the operation to complete.
 	if controlPlane.HasDeletingMachine() {
-		logger.Info("Waiting for machines to be deleted", "Machines", strings.Join(controlPlane.Machines.Filter(collections.HasDeletionTimestamp).Names(), ", "))
+		logger.Info("Waiting for machines to be deleted", "Machines", strings.Join(controlPlane.Machines.Filter(machinefilters.HasDeletionTimestamp).Names(), ", "))
 		return ctrl.Result{RequeueAfter: deleteRequeueAfter}, nil
 	}
 
@@ -225,7 +225,7 @@ func preflightCheckCondition(kind string, obj conditions.Getter, condition clust
 	return nil
 }
 
-func selectMachineForScaleDown(controlPlane *internal.ControlPlane, outdatedMachines collections.Machines) (*clusterv1.Machine, error) {
+func selectMachineForScaleDown(controlPlane *internal.ControlPlane, outdatedMachines internal.FilterableMachineCollection) (*clusterv1.Machine, error) {
 	machines := controlPlane.Machines
 	switch {
 	case controlPlane.MachineWithDeleteAnnotation(outdatedMachines).Len() > 0:
diff --git a/controlplane/kubeadm/controllers/status.go b/controlplane/kubeadm/controllers/status.go
index 6945226ed..0ff057b09 100644
--- a/controlplane/kubeadm/controllers/status.go
+++ b/controlplane/kubeadm/controllers/status.go
@@ -18,12 +18,12 @@ package controllers
 
 import (
 	"context"
-	"sigs.k8s.io/cluster-api/util/collections"
 
 	"github.com/pkg/errors"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -34,12 +34,12 @@ import (
 func (r *KubeadmControlPlaneReconciler) updateStatus(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, cluster *clusterv1.Cluster) error {
 	log := ctrl.LoggerFrom(ctx, "cluster", cluster.Name)
 
-	selector := collections.ControlPlaneSelectorForCluster(cluster.Name)
+	selector := machinefilters.ControlPlaneSelectorForCluster(cluster.Name)
 	// Copy label selector to its status counterpart in string format.
 	// This is necessary for CRDs including scale subresources.
 	kcp.Status.Selector = selector.String()
 
-	ownedMachines, err := r.managementCluster.GetMachinesForCluster(ctx, util.ObjectKey(cluster), collections.OwnedMachines(kcp))
+	ownedMachines, err := r.managementCluster.GetMachinesForCluster(ctx, util.ObjectKey(cluster), machinefilters.OwnedMachines(kcp))
 	if err != nil {
 		return errors.Wrap(err, "failed to get list of owned machines")
 	}
@@ -79,7 +79,7 @@ func (r *KubeadmControlPlaneReconciler) updateStatus(ctx context.Context, kcp *c
 		// make sure last resize operation is marked as completed.
 		// NOTE: we are checking the number of machines ready so we report resize completed only when the machines
 		// are actually provisioned (vs reporting completed immediately after the last machine object is created).
-		readyMachines := ownedMachines.Filter(collections.IsReady())
+		readyMachines := ownedMachines.Filter(machinefilters.IsReady())
 		if int32(len(readyMachines)) == replicas {
 			conditions.MarkTrue(kcp, controlplanev1.ResizedCondition)
 		}
diff --git a/controlplane/kubeadm/controllers/upgrade.go b/controlplane/kubeadm/controllers/upgrade.go
index 910e5c80f..250ef378f 100644
--- a/controlplane/kubeadm/controllers/upgrade.go
+++ b/controlplane/kubeadm/controllers/upgrade.go
@@ -18,13 +18,13 @@ package controllers
 
 import (
 	"context"
+
 	"github.com/blang/semver"
 	"github.com/pkg/errors"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/collections"
 	ctrl "sigs.k8s.io/controller-runtime"
 )
 
@@ -33,7 +33,7 @@ func (r *KubeadmControlPlaneReconciler) upgradeControlPlane(
 	cluster *clusterv1.Cluster,
 	kcp *controlplanev1.KubeadmControlPlane,
 	controlPlane *internal.ControlPlane,
-	machinesRequireUpgrade collections.Machines,
+	machinesRequireUpgrade internal.FilterableMachineCollection,
 ) (ctrl.Result, error) {
 	logger := controlPlane.Logger()
 
diff --git a/controlplane/kubeadm/internal/cluster.go b/controlplane/kubeadm/internal/cluster.go
index b34fd128e..8ff147cd9 100644
--- a/controlplane/kubeadm/internal/cluster.go
+++ b/controlplane/kubeadm/internal/cluster.go
@@ -21,13 +21,13 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"time"
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controllers/remote"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util/secret"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -42,7 +42,7 @@ const (
 type ManagementCluster interface {
 	ctrlclient.Reader
 
-	GetMachinesForCluster(ctx context.Context, cluster client.ObjectKey, filters ...collections.Func) (collections.Machines, error)
+	GetMachinesForCluster(ctx context.Context, cluster client.ObjectKey, filters ...machinefilters.Func) (FilterableMachineCollection, error)
 	GetWorkloadCluster(ctx context.Context, clusterKey client.ObjectKey) (WorkloadCluster, error)
 }
 
@@ -73,7 +73,7 @@ func (m *Management) List(ctx context.Context, list client.ObjectList, opts ...c
 
 // GetMachinesForCluster returns a list of machines that can be filtered or not.
 // If no filter is supplied then all machines associated with the target cluster are returned.
-func (m *Management) GetMachinesForCluster(ctx context.Context, cluster client.ObjectKey, filters ...collections.Func) (collections.Machines, error) {
+func (m *Management) GetMachinesForCluster(ctx context.Context, cluster client.ObjectKey, filters ...machinefilters.Func) (FilterableMachineCollection, error) {
 	selector := map[string]string{
 		clusterv1.ClusterLabelName: cluster.Name,
 	}
@@ -82,7 +82,7 @@ func (m *Management) GetMachinesForCluster(ctx context.Context, cluster client.O
 		return nil, errors.Wrap(err, "failed to list machines")
 	}
 
-	machines := collections.FromMachineList(ml)
+	machines := NewFilterableMachineCollectionFromMachineList(ml)
 	return machines.Filter(filters...), nil
 }
 
diff --git a/controlplane/kubeadm/internal/control_plane.go b/controlplane/kubeadm/internal/control_plane.go
index 9341a6e9c..2f03d74d6 100644
--- a/controlplane/kubeadm/internal/control_plane.go
+++ b/controlplane/kubeadm/internal/control_plane.go
@@ -18,6 +18,7 @@ package internal
 
 import (
 	"context"
+
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -26,27 +27,22 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	kerrors "k8s.io/apimachinery/pkg/util/errors"
 	"k8s.io/apiserver/pkg/storage/names"
-	"k8s.io/klog/klogr"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controllers/external"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
-	"sigs.k8s.io/cluster-api/util/collections"
-	"sigs.k8s.io/cluster-api/util/failuredomains"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-// Log is the global logger for the internal package.
-var Log = klogr.New()
-
 // ControlPlane holds business logic around control planes.
 // It should never need to connect to a service, that responsibility lies outside of this struct.
 // Going forward we should be trying to add more logic to here and reduce the amount of logic in the reconciler.
 type ControlPlane struct {
 	KCP                  *controlplanev1.KubeadmControlPlane
 	Cluster              *clusterv1.Cluster
-	Machines             collections.Machines
+	Machines             FilterableMachineCollection
 	machinesPatchHelpers map[string]*patch.Helper
 
 	// reconciliationTime is the time of the current reconciliation, and should be used for all "now" calculations
@@ -59,7 +55,7 @@ type ControlPlane struct {
 }
 
 // NewControlPlane returns an instantiated ControlPlane.
-func NewControlPlane(ctx context.Context, client client.Client, cluster *clusterv1.Cluster, kcp *controlplanev1.KubeadmControlPlane, ownedMachines collections.Machines) (*ControlPlane, error) {
+func NewControlPlane(ctx context.Context, client client.Client, cluster *clusterv1.Cluster, kcp *controlplanev1.KubeadmControlPlane, ownedMachines FilterableMachineCollection) (*ControlPlane, error) {
 	infraObjects, err := getInfraResources(ctx, client, ownedMachines)
 	if err != nil {
 		return nil, err
@@ -131,9 +127,9 @@ func (c *ControlPlane) EtcdImageData() (string, string) {
 }
 
 // MachineInFailureDomainWithMostMachines returns the first matching failure domain with machines that has the most control-plane machines on it.
-func (c *ControlPlane) MachineInFailureDomainWithMostMachines(machines collections.Machines) (*clusterv1.Machine, error) {
+func (c *ControlPlane) MachineInFailureDomainWithMostMachines(machines FilterableMachineCollection) (*clusterv1.Machine, error) {
 	fd := c.FailureDomainWithMostMachines(machines)
-	machinesInFailureDomain := machines.Filter(collections.InFailureDomains(fd))
+	machinesInFailureDomain := machines.Filter(machinefilters.InFailureDomains(fd))
 	machineToMark := machinesInFailureDomain.Oldest()
 	if machineToMark == nil {
 		return nil, errors.New("failed to pick control plane Machine to mark for deletion")
@@ -142,19 +138,19 @@ func (c *ControlPlane) MachineInFailureDomainWithMostMachines(machines collectio
 }
 
 // MachineWithDeleteAnnotation returns a machine that has been annotated with DeleteMachineAnnotation key.
-func (c *ControlPlane) MachineWithDeleteAnnotation(machines collections.Machines) collections.Machines {
+func (c *ControlPlane) MachineWithDeleteAnnotation(machines FilterableMachineCollection) FilterableMachineCollection {
 	// See if there are any machines with DeleteMachineAnnotation key.
-	annotatedMachines := machines.Filter(collections.HasAnnotationKey(clusterv1.DeleteMachineAnnotation))
+	annotatedMachines := machines.Filter(machinefilters.HasAnnotationKey(clusterv1.DeleteMachineAnnotation))
 	// If there are, return list of annotated machines.
 	return annotatedMachines
 }
 
 // FailureDomainWithMostMachines returns a fd which exists both in machines and control-plane machines and has the most
 // control-plane machines on it.
-func (c *ControlPlane) FailureDomainWithMostMachines(machines collections.Machines) *string {
+func (c *ControlPlane) FailureDomainWithMostMachines(machines FilterableMachineCollection) *string {
 	// See if there are any Machines that are not in currently defined failure domains first.
 	notInFailureDomains := machines.Filter(
-		collections.Not(collections.InFailureDomains(c.FailureDomains().FilterControlPlane().GetIDs()...)),
+		machinefilters.Not(machinefilters.InFailureDomains(c.FailureDomains().FilterControlPlane().GetIDs()...)),
 	)
 	if len(notInFailureDomains) > 0 {
 		// return the failure domain for the oldest Machine not in the current list of failure domains
@@ -162,7 +158,7 @@ func (c *ControlPlane) FailureDomainWithMostMachines(machines collections.Machin
 		// in the cluster status.
 		return notInFailureDomains.Oldest().Spec.FailureDomain
 	}
-	return failuredomains.PickMost(c.Cluster.Status.FailureDomains.FilterControlPlane(), c.Machines, machines)
+	return PickMost(c, machines)
 }
 
 // NextFailureDomainForScaleUp returns the failure domain with the fewest number of up-to-date machines.
@@ -170,7 +166,7 @@ func (c *ControlPlane) NextFailureDomainForScaleUp() *string {
 	if len(c.Cluster.Status.FailureDomains.FilterControlPlane()) == 0 {
 		return nil
 	}
-	return failuredomains.PickFewest(c.FailureDomains().FilterControlPlane(), c.UpToDateMachines())
+	return PickFewest(c.FailureDomains().FilterControlPlane(), c.UpToDateMachines())
 }
 
 // InitialControlPlaneConfig returns a new KubeadmConfigSpec that is to be used for an initializing control plane.
@@ -247,31 +243,31 @@ func (c *ControlPlane) NeedsReplacementNode() bool {
 
 // HasDeletingMachine returns true if any machine in the control plane is in the process of being deleted.
 func (c *ControlPlane) HasDeletingMachine() bool {
-	return len(c.Machines.Filter(collections.HasDeletionTimestamp)) > 0
+	return len(c.Machines.Filter(machinefilters.HasDeletionTimestamp)) > 0
 }
 
 // MachinesNeedingRollout return a list of machines that need to be rolled out.
-func (c *ControlPlane) MachinesNeedingRollout() collections.Machines {
+func (c *ControlPlane) MachinesNeedingRollout() FilterableMachineCollection {
 	// Ignore machines to be deleted.
-	machines := c.Machines.Filter(collections.Not(collections.HasDeletionTimestamp))
+	machines := c.Machines.Filter(machinefilters.Not(machinefilters.HasDeletionTimestamp))
 
 	// Return machines if they are scheduled for rollout or if with an outdated configuration.
 	return machines.AnyFilter(
 		// Machines that are scheduled for rollout (KCP.Spec.UpgradeAfter set, the UpgradeAfter deadline is expired, and the machine was created before the deadline).
-		collections.ShouldRolloutAfter(&c.reconciliationTime, c.KCP.Spec.UpgradeAfter),
+		machinefilters.ShouldRolloutAfter(&c.reconciliationTime, c.KCP.Spec.UpgradeAfter),
 		// Machines that do not match with KCP config.
-		collections.Not(MatchesKCPConfiguration(c.infraResources, c.kubeadmConfigs, c.KCP)),
+		machinefilters.Not(machinefilters.MatchesKCPConfiguration(c.infraResources, c.kubeadmConfigs, c.KCP)),
 	)
 }
 
 // UpToDateMachines returns the machines that are up to date with the control
 // plane's configuration and therefore do not require rollout.
-func (c *ControlPlane) UpToDateMachines() collections.Machines {
+func (c *ControlPlane) UpToDateMachines() FilterableMachineCollection {
 	return c.Machines.Difference(c.MachinesNeedingRollout())
 }
 
 // getInfraResources fetches the external infrastructure resource for each machine in the collection and returns a map of machine.Name -> infraResource.
-func getInfraResources(ctx context.Context, cl client.Client, machines collections.Machines) (map[string]*unstructured.Unstructured, error) {
+func getInfraResources(ctx context.Context, cl client.Client, machines FilterableMachineCollection) (map[string]*unstructured.Unstructured, error) {
 	result := map[string]*unstructured.Unstructured{}
 	for _, m := range machines {
 		infraObj, err := external.Get(ctx, cl, &m.Spec.InfrastructureRef, m.Namespace)
@@ -287,7 +283,7 @@ func getInfraResources(ctx context.Context, cl client.Client, machines collectio
 }
 
 // getKubeadmConfigs fetches the kubeadm config for each machine in the collection and returns a map of machine.Name -> KubeadmConfig.
-func getKubeadmConfigs(ctx context.Context, cl client.Client, machines collections.Machines) (map[string]*bootstrapv1.KubeadmConfig, error) {
+func getKubeadmConfigs(ctx context.Context, cl client.Client, machines FilterableMachineCollection) (map[string]*bootstrapv1.KubeadmConfig, error) {
 	result := map[string]*bootstrapv1.KubeadmConfig{}
 	for _, m := range machines {
 		bootstrapRef := m.Spec.Bootstrap.ConfigRef
@@ -312,13 +308,13 @@ func (c *ControlPlane) IsEtcdManaged() bool {
 }
 
 // UnhealthyMachines returns the list of control plane machines marked as unhealthy by MHC.
-func (c *ControlPlane) UnhealthyMachines() collections.Machines {
-	return c.Machines.Filter(collections.HasUnhealthyCondition)
+func (c *ControlPlane) UnhealthyMachines() FilterableMachineCollection {
+	return c.Machines.Filter(machinefilters.HasUnhealthyCondition)
 }
 
 // HealthyMachines returns the list of control plane machines not marked as unhealthy by MHC.
-func (c *ControlPlane) HealthyMachines() collections.Machines {
-	return c.Machines.Filter(collections.Not(collections.HasUnhealthyCondition))
+func (c *ControlPlane) HealthyMachines() FilterableMachineCollection {
+	return c.Machines.Filter(machinefilters.Not(machinefilters.HasUnhealthyCondition))
 }
 
 // HasUnhealthyMachine returns true if any machine in the control plane is marked as unhealthy by MHC.
diff --git a/util/failuredomains/failure_domains.go b/controlplane/kubeadm/internal/failure_domain.go
similarity index 79%
rename from util/failuredomains/failure_domains.go
rename to controlplane/kubeadm/internal/failure_domain.go
index 46b5ad412..0a745981d 100644
--- a/util/failuredomains/failure_domains.go
+++ b/controlplane/kubeadm/internal/failure_domain.go
@@ -14,18 +14,20 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package failuredomains
+package internal
 
 import (
-	"k8s.io/klog/klogr"
 	"sort"
 
+	"k8s.io/klog/klogr"
 	"k8s.io/utils/pointer"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
-	"sigs.k8s.io/cluster-api/util/collections"
 )
 
+// Log is the global logger for the internal package.
+var Log = klogr.New()
+
 type failureDomainAggregation struct {
 	id    string
 	count int
@@ -48,10 +50,10 @@ func (f failureDomainAggregations) Swap(i, j int) {
 	f[i], f[j] = f[j], f[i]
 }
 
-// PickMost returns a failure domain that is in machines and has most of the group of machines on.
-func PickMost(failureDomains clusterv1.FailureDomains, groupMachines, machines collections.Machines) *string {
-	// orderDescending sorts failure domains according to all machines belonging to the group.
-	fds := orderDescending(failureDomains, groupMachines)
+// PickMost returns a failure domain that is in machines and has most control-plane machines on.
+func PickMost(c *ControlPlane, machines FilterableMachineCollection) *string {
+	// orderDescending sorts failure domains according to all control plane machines
+	fds := orderDescending(c.Cluster.Status.FailureDomains.FilterControlPlane(), c.Machines)
 	for _, fd := range fds {
 		for _, m := range machines {
 			if m.Spec.FailureDomain == nil {
@@ -66,7 +68,7 @@ func PickMost(failureDomains clusterv1.FailureDomains, groupMachines, machines c
 }
 
 // orderDescending returns the sorted failure domains in decreasing order.
-func orderDescending(failureDomains clusterv1.FailureDomains, machines collections.Machines) failureDomainAggregations {
+func orderDescending(failureDomains clusterv1.FailureDomains, machines FilterableMachineCollection) failureDomainAggregations {
 	aggregations := pick(failureDomains, machines)
 	if len(aggregations) == 0 {
 		return nil
@@ -76,7 +78,7 @@ func orderDescending(failureDomains clusterv1.FailureDomains, machines collectio
 }
 
 // PickFewest returns the failure domain with the fewest number of machines.
-func PickFewest(failureDomains clusterv1.FailureDomains, machines collections.Machines) *string {
+func PickFewest(failureDomains clusterv1.FailureDomains, machines FilterableMachineCollection) *string {
 	aggregations := pick(failureDomains, machines)
 	if len(aggregations) == 0 {
 		return nil
@@ -85,7 +87,7 @@ func PickFewest(failureDomains clusterv1.FailureDomains, machines collections.Ma
 	return pointer.StringPtr(aggregations[0].id)
 }
 
-func pick(failureDomains clusterv1.FailureDomains, machines collections.Machines) failureDomainAggregations {
+func pick(failureDomains clusterv1.FailureDomains, machines FilterableMachineCollection) failureDomainAggregations {
 	if len(failureDomains) == 0 {
 		return failureDomainAggregations{}
 	}
@@ -104,7 +106,7 @@ func pick(failureDomains clusterv1.FailureDomains, machines collections.Machines
 		}
 		id := *m.Spec.FailureDomain
 		if _, ok := failureDomains[id]; !ok {
-			klogr.New().Info("unknown failure domain", "machine-name", m.GetName(), "failure-domain-id", id, "known-failure-domains", failureDomains)
+			Log.Info("unknown failure domain", "machine-name", m.GetName(), "failure-domain-id", id, "known-failure-domains", failureDomains)
 			continue
 		}
 		counters[id]++
diff --git a/util/collections/machine_collection.go b/controlplane/kubeadm/internal/machine_collection.go
similarity index 54%
rename from util/collections/machine_collection.go
rename to controlplane/kubeadm/internal/machine_collection.go
index 1c5f5e0c0..3fb55b0de 100644
--- a/util/collections/machine_collection.go
+++ b/controlplane/kubeadm/internal/machine_collection.go
@@ -21,38 +21,34 @@ limitations under the License.
 //   - Sortable data type is removed in favor of util.MachinesByCreationTimestamp
 //   - nil checks added to account for the pointer
 //   - Added Filter, AnyFilter, and Oldest methods
-//   - Added FromMachineList initializer
+//   - Added NewFilterableMachineCollectionFromMachineList initializer
 //   - Updated Has to also check for equality of Machines
 //   - Removed unused methods
 
-package collections
+package internal
 
 import (
 	"sort"
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/conditions"
 )
 
-// Machines is a set of Machines
-type Machines map[string]*clusterv1.Machine
+// FilterableMachineCollection is a set of Machines
+type FilterableMachineCollection map[string]*clusterv1.Machine
 
-// New creates an empty Machines.
-func New() Machines {
-	return make(Machines)
-}
-
-// FromMachines creates a Machines from a list of values.
-func FromMachines(machines ...*clusterv1.Machine) Machines {
-	ss := make(Machines, len(machines))
+// NewFilterableMachineCollection creates a FilterableMachineCollection from a list of values.
+func NewFilterableMachineCollection(machines ...*clusterv1.Machine) FilterableMachineCollection {
+	ss := make(FilterableMachineCollection, len(machines))
 	ss.Insert(machines...)
 	return ss
 }
 
-// FromMachineList creates a Machines from the given MachineList
-func FromMachineList(machineList *clusterv1.MachineList) Machines {
-	ss := make(Machines, len(machineList.Items))
+// NewFilterableMachineCollectionFromMachineList creates a FilterableMachineCollection from the given MachineList
+func NewFilterableMachineCollectionFromMachineList(machineList *clusterv1.MachineList) FilterableMachineCollection {
+	ss := make(FilterableMachineCollection, len(machineList.Items))
 	for i := range machineList.Items {
 		ss.Insert(&machineList.Items[i])
 	}
@@ -60,7 +56,7 @@ func FromMachineList(machineList *clusterv1.MachineList) Machines {
 }
 
 // Insert adds items to the set.
-func (s Machines) Insert(machines ...*clusterv1.Machine) {
+func (s FilterableMachineCollection) Insert(machines ...*clusterv1.Machine) {
 	for i := range machines {
 		if machines[i] != nil {
 			m := machines[i]
@@ -70,7 +66,7 @@ func (s Machines) Insert(machines ...*clusterv1.Machine) {
 }
 
 // Difference returns a copy without machines that are in the given collection
-func (s Machines) Difference(machines Machines) Machines {
+func (s FilterableMachineCollection) Difference(machines FilterableMachineCollection) FilterableMachineCollection {
 	return s.Filter(func(m *clusterv1.Machine) bool {
 		_, found := machines[m.Name]
 		return !found
@@ -78,7 +74,7 @@ func (s Machines) Difference(machines Machines) Machines {
 }
 
 // SortedByCreationTimestamp returns the machines sorted by creation timestamp
-func (s Machines) SortedByCreationTimestamp() []*clusterv1.Machine {
+func (s FilterableMachineCollection) SortedByCreationTimestamp() []*clusterv1.Machine {
 	res := make(util.MachinesByCreationTimestamp, 0, len(s))
 	for _, value := range s {
 		res = append(res, value)
@@ -88,7 +84,7 @@ func (s Machines) SortedByCreationTimestamp() []*clusterv1.Machine {
 }
 
 // UnsortedList returns the slice with contents in random order.
-func (s Machines) UnsortedList() []*clusterv1.Machine {
+func (s FilterableMachineCollection) UnsortedList() []*clusterv1.Machine {
 	res := make([]*clusterv1.Machine, 0, len(s))
 	for _, value := range s {
 		res = append(res, value)
@@ -97,13 +93,13 @@ func (s Machines) UnsortedList() []*clusterv1.Machine {
 }
 
 // Len returns the size of the set.
-func (s Machines) Len() int {
+func (s FilterableMachineCollection) Len() int {
 	return len(s)
 }
 
-// newFilteredMachineCollection creates a Machines from a filtered list of values.
-func newFilteredMachineCollection(filter Func, machines ...*clusterv1.Machine) Machines {
-	ss := make(Machines, len(machines))
+// newFilteredMachineCollection creates a FilterableMachineCollection from a filtered list of values.
+func newFilteredMachineCollection(filter machinefilters.Func, machines ...*clusterv1.Machine) FilterableMachineCollection {
+	ss := make(FilterableMachineCollection, len(machines))
 	for i := range machines {
 		m := machines[i]
 		if filter(m) {
@@ -113,18 +109,18 @@ func newFilteredMachineCollection(filter Func, machines ...*clusterv1.Machine) M
 	return ss
 }
 
-// Filter returns a Machines containing only the Machines that match all of the given MachineFilters
-func (s Machines) Filter(filters ...Func) Machines {
-	return newFilteredMachineCollection(And(filters...), s.UnsortedList()...)
+// Filter returns a FilterableMachineCollection containing only the Machines that match all of the given MachineFilters
+func (s FilterableMachineCollection) Filter(filters ...machinefilters.Func) FilterableMachineCollection {
+	return newFilteredMachineCollection(machinefilters.And(filters...), s.UnsortedList()...)
 }
 
-// AnyFilter returns a Machines containing only the Machines that match any of the given MachineFilters
-func (s Machines) AnyFilter(filters ...Func) Machines {
-	return newFilteredMachineCollection(Or(filters...), s.UnsortedList()...)
+// AnyFilter returns a FilterableMachineCollection containing only the Machines that match any of the given MachineFilters
+func (s FilterableMachineCollection) AnyFilter(filters ...machinefilters.Func) FilterableMachineCollection {
+	return newFilteredMachineCollection(machinefilters.Or(filters...), s.UnsortedList()...)
 }
 
 // Oldest returns the Machine with the oldest CreationTimestamp
-func (s Machines) Oldest() *clusterv1.Machine {
+func (s FilterableMachineCollection) Oldest() *clusterv1.Machine {
 	if len(s) == 0 {
 		return nil
 	}
@@ -132,7 +128,7 @@ func (s Machines) Oldest() *clusterv1.Machine {
 }
 
 // Newest returns the Machine with the most recent CreationTimestamp
-func (s Machines) Newest() *clusterv1.Machine {
+func (s FilterableMachineCollection) Newest() *clusterv1.Machine {
 	if len(s) == 0 {
 		return nil
 	}
@@ -140,8 +136,8 @@ func (s Machines) Newest() *clusterv1.Machine {
 }
 
 // DeepCopy returns a deep copy
-func (s Machines) DeepCopy() Machines {
-	result := make(Machines, len(s))
+func (s FilterableMachineCollection) DeepCopy() FilterableMachineCollection {
+	result := make(FilterableMachineCollection, len(s))
 	for _, m := range s {
 		result.Insert(m.DeepCopy())
 	}
@@ -149,7 +145,7 @@ func (s Machines) DeepCopy() Machines {
 }
 
 // ConditionGetters returns the slice with machines converted into conditions.Getter.
-func (s Machines) ConditionGetters() []conditions.Getter {
+func (s FilterableMachineCollection) ConditionGetters() []conditions.Getter {
 	res := make([]conditions.Getter, 0, len(s))
 	for _, v := range s {
 		value := *v
@@ -160,7 +156,7 @@ func (s Machines) ConditionGetters() []conditions.Getter {
 
 // Names returns a slice of the names of each machine in the collection.
 // Useful for logging and test assertions.
-func (s Machines) Names() []string {
+func (s FilterableMachineCollection) Names() []string {
 	names := make([]string, 0, s.Len())
 	for _, m := range s {
 		names = append(names, m.Name)
diff --git a/controlplane/kubeadm/internal/filters.go b/controlplane/kubeadm/internal/machinefilters/machine_filters.go
similarity index 63%
rename from controlplane/kubeadm/internal/filters.go
rename to controlplane/kubeadm/internal/machinefilters/machine_filters.go
index 891266618..ae0937b4f 100644
--- a/controlplane/kubeadm/internal/filters.go
+++ b/controlplane/kubeadm/internal/machinefilters/machine_filters.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 The Kubernetes Authors.
+Copyright 2020 The Kubernetes Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,31 +14,203 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package internal
+package machinefilters
 
 import (
 	"encoding/json"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"reflect"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/selection"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha4"
 	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
-	"sigs.k8s.io/cluster-api/util/collections"
+	"sigs.k8s.io/cluster-api/util"
+	"sigs.k8s.io/cluster-api/util/conditions"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+type Func func(machine *clusterv1.Machine) bool
+
+// And returns a filter that returns true if all of the given filters returns true.
+func And(filters ...Func) Func {
+	return func(machine *clusterv1.Machine) bool {
+		for _, f := range filters {
+			if !f(machine) {
+				return false
+			}
+		}
+		return true
+	}
+}
+
+// Or returns a filter that returns true if any of the given filters returns true.
+func Or(filters ...Func) Func {
+	return func(machine *clusterv1.Machine) bool {
+		for _, f := range filters {
+			if f(machine) {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+// Not returns a filter that returns the opposite of the given filter.
+func Not(mf Func) Func {
+	return func(machine *clusterv1.Machine) bool {
+		return !mf(machine)
+	}
+}
+
+// HasControllerRef is a filter that returns true if the machine has a controller ref
+func HasControllerRef(machine *clusterv1.Machine) bool {
+	if machine == nil {
+		return false
+	}
+	return metav1.GetControllerOf(machine) != nil
+}
+
+// InFailureDomains returns a filter to find all machines
+// in any of the given failure domains
+func InFailureDomains(failureDomains ...*string) Func {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		for i := range failureDomains {
+			fd := failureDomains[i]
+			if fd == nil {
+				if fd == machine.Spec.FailureDomain {
+					return true
+				}
+				continue
+			}
+			if machine.Spec.FailureDomain == nil {
+				continue
+			}
+			if *fd == *machine.Spec.FailureDomain {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+// OwnedMachines returns a filter to find all owned control plane machines.
+// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, machinefilters.OwnedMachines(controlPlane))
+func OwnedMachines(owner client.Object) func(machine *clusterv1.Machine) bool {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		return util.IsOwnedByObject(machine, owner)
+	}
+}
+
+// ControlPlaneMachines returns a filter to find all control plane machines for a cluster, regardless of ownership.
+// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, machinefilters.ControlPlaneMachines(cluster.Name))
+func ControlPlaneMachines(clusterName string) func(machine *clusterv1.Machine) bool {
+	selector := ControlPlaneSelectorForCluster(clusterName)
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		return selector.Matches(labels.Set(machine.Labels))
+	}
+}
+
+// AdoptableControlPlaneMachines returns a filter to find all un-controlled control plane machines.
+// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, AdoptableControlPlaneMachines(cluster.Name, controlPlane))
+func AdoptableControlPlaneMachines(clusterName string) func(machine *clusterv1.Machine) bool {
+	return And(
+		ControlPlaneMachines(clusterName),
+		Not(HasControllerRef),
+	)
+}
+
+// HasDeletionTimestamp returns a filter to find all machines that have a deletion timestamp.
+func HasDeletionTimestamp(machine *clusterv1.Machine) bool {
+	if machine == nil {
+		return false
+	}
+	return !machine.DeletionTimestamp.IsZero()
+}
+
+// HasUnhealthyCondition returns a filter to find all machines that have a MachineHealthCheckSucceeded condition set to False,
+// indicating a problem was detected on the machine, and the MachineOwnerRemediated condition set, indicating that KCP is
+// responsible of performing remediation as owner of the machine.
+func HasUnhealthyCondition(machine *clusterv1.Machine) bool {
+	if machine == nil {
+		return false
+	}
+	return conditions.IsFalse(machine, clusterv1.MachineHealthCheckSuccededCondition) && conditions.IsFalse(machine, clusterv1.MachineOwnerRemediatedCondition)
+}
+
+// IsReady returns a filter to find all machines with the ReadyCondition equals to True.
+func IsReady() Func {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		return conditions.IsTrue(machine, clusterv1.ReadyCondition)
+	}
+}
+
+// ShouldRolloutAfter returns a filter to find all machines where
+// CreationTimestamp < rolloutAfter < reconciliationTIme
+func ShouldRolloutAfter(reconciliationTime, rolloutAfter *metav1.Time) Func {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		return machine.CreationTimestamp.Before(rolloutAfter) && rolloutAfter.Before(reconciliationTime)
+	}
+}
+
+// HasAnnotationKey returns a filter to find all machines that have the
+// specified Annotation key present
+func HasAnnotationKey(key string) Func {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil || machine.Annotations == nil {
+			return false
+		}
+		if _, ok := machine.Annotations[key]; ok {
+			return true
+		}
+		return false
+	}
+}
+
+// ControlPlaneSelectorForCluster returns the label selector necessary to get control plane machines for a given cluster.
+func ControlPlaneSelectorForCluster(clusterName string) labels.Selector {
+	must := func(r *labels.Requirement, err error) labels.Requirement {
+		if err != nil {
+			panic(err)
+		}
+		return *r
+	}
+	return labels.NewSelector().Add(
+		must(labels.NewRequirement(clusterv1.ClusterLabelName, selection.Equals, []string{clusterName})),
+		must(labels.NewRequirement(clusterv1.MachineControlPlaneLabelName, selection.Exists, []string{})),
+	)
+}
+
 // MatchesKCPConfiguration returns a filter to find all machines that matches with KCP config and do not require any rollout.
 // Kubernetes version, infrastructure template, and KubeadmConfig field need to be equivalent.
 func MatchesKCPConfiguration(infraConfigs map[string]*unstructured.Unstructured, machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane) func(machine *clusterv1.Machine) bool {
-	return collections.And(
-		collections.MatchesKubernetesVersion(kcp.Spec.Version),
+	return And(
+		MatchesKubernetesVersion(kcp.Spec.Version),
 		MatchesKubeadmBootstrapConfig(machineConfigs, kcp),
 		MatchesTemplateClonedFrom(infraConfigs, kcp),
 	)
 }
 
 // MatchesTemplateClonedFrom returns a filter to find all machines that match a given KCP infra template.
-func MatchesTemplateClonedFrom(infraConfigs map[string]*unstructured.Unstructured, kcp *controlplanev1.KubeadmControlPlane) collections.Func {
+func MatchesTemplateClonedFrom(infraConfigs map[string]*unstructured.Unstructured, kcp *controlplanev1.KubeadmControlPlane) Func {
 	return func(machine *clusterv1.Machine) bool {
 		if machine == nil {
 			return false
@@ -67,8 +239,21 @@ func MatchesTemplateClonedFrom(infraConfigs map[string]*unstructured.Unstructure
 	}
 }
 
+// MatchesKubernetesVersion returns a filter to find all machines that match a given Kubernetes version.
+func MatchesKubernetesVersion(kubernetesVersion string) Func {
+	return func(machine *clusterv1.Machine) bool {
+		if machine == nil {
+			return false
+		}
+		if machine.Spec.Version == nil {
+			return false
+		}
+		return *machine.Spec.Version == kubernetesVersion
+	}
+}
+
 // MatchesKubeadmBootstrapConfig checks if machine's KubeadmConfigSpec is equivalent with KCP's KubeadmConfigSpec.
-func MatchesKubeadmBootstrapConfig(machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane) collections.Func {
+func MatchesKubeadmBootstrapConfig(machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane) Func {
 	return func(machine *clusterv1.Machine) bool {
 		if machine == nil {
 			return false
diff --git a/controlplane/kubeadm/internal/workload_cluster_conditions.go b/controlplane/kubeadm/internal/workload_cluster_conditions.go
index 4e75d5f77..38f1e4bd5 100644
--- a/controlplane/kubeadm/internal/workload_cluster_conditions.go
+++ b/controlplane/kubeadm/internal/workload_cluster_conditions.go
@@ -19,7 +19,6 @@ package internal
 import (
 	"context"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
@@ -334,7 +333,7 @@ func (w *Workload) UpdateStaticPodConditions(ctx context.Context, controlPlane *
 	})
 }
 
-func hasProvisioningMachine(machines collections.Machines) bool {
+func hasProvisioningMachine(machines FilterableMachineCollection) bool {
 	for _, machine := range machines {
 		if machine.Status.NodeRef == nil {
 			return true
diff --git a/controlplane/kubeadm/internal/workload_cluster_coredns.go b/controlplane/kubeadm/internal/workload_cluster_coredns.go
index 4c41ff1f2..5df278b43 100644
--- a/controlplane/kubeadm/internal/workload_cluster_coredns.go
+++ b/controlplane/kubeadm/internal/workload_cluster_coredns.go
@@ -19,7 +19,6 @@ package internal
 import (
 	"context"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/version"
 
 	"github.com/coredns/corefile-migration/migration"
 	"github.com/pkg/errors"
@@ -29,6 +28,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
+	"sigs.k8s.io/cluster-api/util"
 	containerutil "sigs.k8s.io/cluster-api/util/container"
 	"sigs.k8s.io/cluster-api/util/patch"
 	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -298,7 +298,7 @@ func patchCoreDNSDeploymentImage(deployment *appsv1.Deployment, image string) {
 }
 
 func extractImageVersion(tag string) (string, error) {
-	ver, err := version.ParseMajorMinorPatchTolerant(tag)
+	ver, err := util.ParseMajorMinorPatch(tag)
 	if err != nil {
 		return "", err
 	}
@@ -309,11 +309,11 @@ func extractImageVersion(tag string) (string, error) {
 // Some of the checks come from
 // https://github.com/coredns/corefile-migration/blob/v1.0.6/migration/migrate.go#L414
 func validateCoreDNSImageTag(fromTag, toTag string) error {
-	from, err := version.ParseMajorMinorPatchTolerant(fromTag)
+	from, err := util.ParseMajorMinorPatch(fromTag)
 	if err != nil {
 		return errors.Wrapf(err, "failed to parse CoreDNS current version %q", fromTag)
 	}
-	to, err := version.ParseMajorMinorPatchTolerant(toTag)
+	to, err := util.ParseMajorMinorPatch(toTag)
 	if err != nil {
 		return errors.Wrapf(err, "failed to parse CoreDNS target version %q", toTag)
 	}
diff --git a/controlplane/kubeadm/main.go b/controlplane/kubeadm/main.go
index a8af13a8f..45f6e6c96 100644
--- a/controlplane/kubeadm/main.go
+++ b/controlplane/kubeadm/main.go
@@ -69,7 +69,6 @@ var (
 	kubeadmControlPlaneConcurrency int
 	syncPeriod                     time.Duration
 	webhookPort                    int
-	webhookCertDir                 string
 )
 
 // InitFlags initializes the flags.
@@ -103,9 +102,6 @@ func InitFlags(fs *pflag.FlagSet) {
 
 	fs.IntVar(&webhookPort, "webhook-port", 9443,
 		"Webhook Server port")
-
-	fs.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/",
-		"Webhook cert dir, only used when webhook-port is specified.")
 }
 func main() {
 	rand.Seed(time.Now().UnixNano())
@@ -137,8 +133,7 @@ func main() {
 			&corev1.ConfigMap{},
 			&corev1.Secret{},
 		},
-		Port:    webhookPort,
-		CertDir: webhookCertDir,
+		Port: webhookPort,
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")
diff --git a/docs/Makefile b/docs/Makefile
index 8aeeb72fa..6cbcab124 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -32,5 +32,5 @@ diagrams: $(DIAGRAMS)
 		--rm \
 		--volume ${ROOT_DIR}:/workdir$(DOCKER_VOL_OPTS) \
 		--user $(shell id -u):$(shell id -g) \
-		k8s.gcr.io/cluster-api/plantuml:1.2019.6 \
+		us.gcr.io/k8s-artifacts-prod/cluster-api/plantuml:1.2019.6 \
 		-v /workdir/$(shell echo '$^' | sed -e 's,.*docs/,,g' )
diff --git a/docs/book/src/developer/providers/v1alpha3-to-v1alpha4.md b/docs/book/src/developer/providers/v1alpha3-to-v1alpha4.md
index f21314572..715b2c197 100644
--- a/docs/book/src/developer/providers/v1alpha3-to-v1alpha4.md
+++ b/docs/book/src/developer/providers/v1alpha3-to-v1alpha4.md
@@ -6,7 +6,7 @@
 
 ## Controller Runtime version
 
-- The Controller Runtime version is now v0.8.+
+- The Controller Runtime version is now v0.7.+
 
 ## Kind version
 
@@ -59,10 +59,6 @@ Specific changes related to this topic will be detailed in this document.
 
 The conversion-gen code from the `1.20.x` release onward generates incorrect conversion functions for types having arrays of pointers to custom objects. Change the existing types to contain objects instead of pointer references.
 
-## Optional flag for specifying webhook certificates dir
-Add optional flag `--webhook-cert-dir={string-value}` which allows user to specify directory where webhooks will get tls certificates. 
-If flag has not provided, default value from `controller-runtime` should be used.
-
 ## Required kustomize changes to have a single manager watching all namespaces and answer to webhook calls
 
 In an effort to simplify the management of Cluster API components, and realign with Kubebuilder configuration,
@@ -120,7 +116,7 @@ Provider's `/config` folder has the same structure of  `/config` folder in CAPI
    - kind: Deployment
      path: spec/template/spec/volumes/secret/secretName
    ```
-1. Edit the `/config/default/kustomization.yaml` file:
+1. Edit the `/config/manager/kustomization.yaml` file:
     - Add the `namePrefix` and the `commonLabels` configuration values copying values from the `/config/kustomization.yaml` file
     - In the `bases:` list, add the following items:
       ```
@@ -133,8 +129,6 @@ Provider's `/config` folder has the same structure of  `/config` folder in CAPI
       - manager_auth_proxy_patch.yaml
       - manager_image_patch.yaml
       - manager_pull_policy.yaml
-      - manager_webhook_patch.yaml
-      - webhookcainjection_patch.yaml
       ```
     - Add a `vars:` configuration using the value from the temporary file created while modifying `/config/webhook/kustomization.yaml`
     - Add the `configurations:` list with the following items:
@@ -170,7 +164,7 @@ should be executed before this changes.
 
 **Changes in the `/config/certmanager` folder:**
 
-1. Edit the `/config/certmanager/certificates.yaml` file and replace all the occurrences of `cert-manager.io/v1alpha2`
+1. Edit the `/config/certmanager/certificates.yaml` file and replace all the occurencies of `cert-manager.io/v1alpha2`
 with `cert-manager.io/v1`
 
 **Changes in the `/config/default` folder:**
diff --git a/docs/book/src/tasks/healthcheck.md b/docs/book/src/tasks/healthcheck.md
index 50c10647f..af86c175f 100644
--- a/docs/book/src/tasks/healthcheck.md
+++ b/docs/book/src/tasks/healthcheck.md
@@ -100,18 +100,6 @@ If `maxUnhealthy` is set to `40%` and there are 6 Machines being checked:
 
 Note, when the percentage is not a whole number, the allowed number is rounded down.
 
-## Skipping Remediation
-
-There are scenarios where remediation for a machine may be undesirable (eg. during cluster migration using `clustrctl move`). For such cases, MachineHealthCheck provides 2 mechanisms to skip machines for remediation.
-
-Implicit skipping when the resource is paused (using `cluster.x-k8s.io/paused` annotation):
-- When a cluster is paused, none of the machines in that cluster are considered for remediation.
-- When a machine is paused, only that machine is not considered for remediation.
-- A cluster or a machine is usually paused automatically by cluster api when it detects a migration.
-
-Explicit skipping using `cluster.x-k8s.io/skip-remediation` annotation:
-- Users can also skip any machine for remediation by setting the `cluster.x-k8s.io/skip-remediation` for that machine.
-
 ## Limitations and Caveats of a MachineHealthCheck
 
 Before deploying a MachineHealthCheck, please familiarise yourself with the following limitations and caveats:
diff --git a/docs/book/src/user/quick-start.md b/docs/book/src/user/quick-start.md
index 75ad2eff1..b5886e4a8 100644
--- a/docs/book/src/user/quick-start.md
+++ b/docs/book/src/user/quick-start.md
@@ -92,7 +92,7 @@ Download the latest release; for example, to download version v0.3.0 on linux, t
 ```
 curl -L {{#releaselink gomodule:"sigs.k8s.io/cluster-api" asset:"clusterctl-linux-amd64" version:"0.3.x"}} -o clusterctl
 ```
-Make the clusterctl binary executable.
+Make the kubectl binary executable.
 ```
 chmod +x ./clusterctl
 ```
@@ -113,7 +113,7 @@ Download the latest release; for example, to download version v0.3.0 on macOS, t
 ```
 curl -L {{#releaselink gomodule:"sigs.k8s.io/cluster-api" asset:"clusterctl-darwin-amd64" version:"0.3.x"}} -o clusterctl
 ```
-Make the clusterctl binary executable.
+Make the kubectl binary executable.
 ```
 chmod +x ./clusterctl
 ```
@@ -374,17 +374,9 @@ See the [AWS provider prerequisites] document for more details.
 {{#/tab }}
 {{#tab Azure}}
 
-<aside class="note warning">
-
-<h1>Warning</h1>
-
-Make sure you choose a VM size which is available in the desired location for your subscription. To see available SKUs, use `az vm list-skus -l <your_location> -r virtualMachines -o table` 
-
-</aside>
-
 ```bash
-# Name of the Azure datacenter location. Change this value to your desired location.
-export AZURE_LOCATION="centralus" 
+# Name of the Azure datacenter location.
+export AZURE_LOCATION="centralus"
 
 # Select VM types.
 export AZURE_CONTROL_PLANE_MACHINE_TYPE="Standard_D2s_v3"
@@ -565,7 +557,7 @@ For the purpose of this tutorial, we'll name our cluster capi-quickstart.
 
 ```bash
 clusterctl config cluster capi-quickstart \
-  --kubernetes-version v1.19.7 \
+  --kubernetes-version v1.19.1 \
   --control-plane-machine-count=3 \
   --worker-machine-count=3 \
   > capi-quickstart.yaml
@@ -584,7 +576,7 @@ The Docker provider is not designed for production use and is intended for devel
 
 ```bash
 clusterctl config cluster capi-quickstart --flavor development \
-  --kubernetes-version v1.19.7 \
+  --kubernetes-version v1.19.1 \
   --control-plane-machine-count=3 \
   --worker-machine-count=3 \
   > capi-quickstart.yaml
@@ -628,12 +620,6 @@ The cluster will now start provisioning. You can check status with:
 kubectl get cluster --all-namespaces
 ```
 
-You can also get an "at glance" view of the cluster and its resources by running:
-
-```bash
-clusterctl describe cluster capi-quickstart
-```
-
 To verify the first control plane is up:
 
 ```bash
@@ -643,8 +629,8 @@ kubectl get kubeadmcontrolplane --all-namespaces
 You should see an output is similar to this:
 
 ```bash
-NAME                            INITIALIZED   API SERVER AVAILABLE   VERSION   REPLICAS   READY   UPDATED   UNAVAILABLE
-capi-quickstart-control-plane   true                                 v1.19.7   3                  3         3
+NAME                              READY   INITIALIZED   REPLICAS   READY REPLICAS   UPDATED REPLICAS   UNAVAILABLE REPLICAS
+capi-quickstart-control-plane             true          3                           3                  3
 ```
 
 <aside class="note warning">
diff --git a/docs/developer/releasing.md b/docs/developer/releasing.md
index 31fe0eb74..55dcf7a1a 100644
--- a/docs/developer/releasing.md
+++ b/docs/developer/releasing.md
@@ -37,10 +37,10 @@ export DOCKER_CLI_EXPERIMENTAL=enabled
 
 ### Artifact locations
 
-1. The container image is found in the registry `k8s.gcr.io/cluster-api/` with an image
+1. The container image is found in the registry `us.gcr.io/k8s-artifacts-prod/cluster-api/` with an image
    name of `cluster-api-controller` and a tag that matches the release version. For
    example, in the `v0.1.5` release, the container image location is
-   `k8s.gcr.io/cluster-api/cluster-api-controller:v0.1.5`
+   `us.gcr.io/k8s-artifacts-prod/cluster-api/cluster-api-controller:v0.1.5`
 
 2. Prior to the `v0.1.5` release, the container image is found in the registry
    `gcr.io/k8s-cluster-api` with an image name of `cluster-api-controller` and a tag
@@ -61,7 +61,7 @@ For version v0.x.y:
 1. Push the tag to the GitHub repository `git push origin v0.x.y`
     1. NB: `origin` should be the name of the remote pointing to `github.com/kubernetes-sigs/cluster-api`
 1. Run `make release` to build artifacts (the image is automatically built by CI)
-1. Follow the [Image Promotion process](https://git.k8s.io/k8s.io/k8s.gcr.io#image-promoter) to promote the image from the staging repo to `k8s.gcr.io/cluster-api`
+1. Follow the [Image Promotion process](https://git.k8s.io/k8s.io/k8s.gcr.io#image-promoter) to promote the image from the staging repo to `us.gcr.io/k8s-artifacts-prod/cluster-api`
 1. Create a release in GitHub based on the tag created above
 1. Release notes can be created by running `make release-notes`, which will generate an output that can be copied to the drafted release in GitHub.
    Pay close attention to the `## :question: Sort these by hand` section, as it contains items that need to be manually sorted.
diff --git a/exp/api/v1alpha3/conversion.go b/exp/api/v1alpha3/conversion.go
index 865a30e16..8c8847d04 100644
--- a/exp/api/v1alpha3/conversion.go
+++ b/exp/api/v1alpha3/conversion.go
@@ -24,4 +24,4 @@ import (
 // Convert_v1alpha3_MachinePoolSpec_To_v1alpha4_MachinePoolSpec is an autogenerated conversion function.
 func Convert_v1alpha3_MachinePoolSpec_To_v1alpha4_MachinePoolSpec(in *MachinePoolSpec, out *v1alpha4.MachinePoolSpec, s conversion.Scope) error {
 	return autoConvert_v1alpha3_MachinePoolSpec_To_v1alpha4_MachinePoolSpec(in, out, s)
-}
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index f8d493d87..c6ae7d06a 100644
--- a/go.mod
+++ b/go.mod
@@ -38,7 +38,7 @@ require (
 	k8s.io/klog v1.0.0
 	k8s.io/kubectl v0.20.2
 	k8s.io/utils v0.0.0-20210111153108-fddb29f9d009
-	sigs.k8s.io/controller-runtime v0.8.2
+	sigs.k8s.io/controller-runtime v0.8.1
 	sigs.k8s.io/kind v0.9.0
 	sigs.k8s.io/yaml v1.2.0
 )
diff --git a/go.sum b/go.sum
index d68bc70b7..b6bcba36b 100644
--- a/go.sum
+++ b/go.sum
@@ -914,8 +914,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/controller-runtime v0.8.2 h1:SBWmI0b3uzMIUD/BIXWNegrCeZmPJ503pOtwxY0LPHM=
-sigs.k8s.io/controller-runtime v0.8.2/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU=
+sigs.k8s.io/controller-runtime v0.8.1 h1:O0K2CJ2JavK8/Tf4LfcpAwRxOFBhv8DjyrbmE6Qw59s=
+sigs.k8s.io/controller-runtime v0.8.1/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU=
 sigs.k8s.io/kind v0.9.0 h1:SoDlXq6pEc7dGagHULNRCCBYrLH6xOi7lqXTRXeAlg4=
 sigs.k8s.io/kind v0.9.0/go.mod h1:cxKQWwmbtRDzQ+RNKnR6gZG6fjbeTtItp5cGf+ww+1Y=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
diff --git a/main.go b/main.go
index 1fd8edea0..40727ec07 100644
--- a/main.go
+++ b/main.go
@@ -70,7 +70,6 @@ var (
 	machineHealthCheckConcurrency int
 	syncPeriod                    time.Duration
 	webhookPort                   int
-	webhookCertDir                string
 	healthAddr                    string
 )
 
@@ -138,9 +137,6 @@ func InitFlags(fs *pflag.FlagSet) {
 	fs.IntVar(&webhookPort, "webhook-port", 9443,
 		"Webhook Server port")
 
-	fs.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/",
-		"Webhook cert dir, only used when webhook-port is specified.")
-
 	fs.StringVar(&healthAddr, "health-addr", ":9440",
 		"The address the health endpoint binds to.")
 
@@ -178,7 +174,6 @@ func main() {
 			&corev1.Secret{},
 		},
 		Port:                   webhookPort,
-		CertDir:                webhookCertDir,
 		HealthProbeBindAddress: healthAddr,
 	})
 	if err != nil {
diff --git a/metadata.yaml b/metadata.yaml
deleted file mode 100644
index 69306d3be..000000000
--- a/metadata.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-# maps release series of major.minor to cluster-api contract version
-# the contract version may change between minor or major versions, but *not*
-# between patch versions.
-#
-# update this file only when a new major or minor version is released
-apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
-releaseSeries:
-  - major: 0
-    minor: 4
-    contract: v1alpha4
-  - major: 0
-    minor: 3
-    contract: v1alpha3
diff --git a/util/annotations/helpers.go b/util/annotations/helpers.go
index 71b96b330..7674c3fcd 100644
--- a/util/annotations/helpers.go
+++ b/util/annotations/helpers.go
@@ -33,12 +33,12 @@ func IsPaused(cluster *clusterv1.Cluster, o metav1.Object) bool {
 
 // HasPausedAnnotation returns true if the object has the `paused` annotation.
 func HasPausedAnnotation(o metav1.Object) bool {
-	return hasAnnotation(o, clusterv1.PausedAnnotation)
-}
-
-// HasSkipRemediationAnnotation returns true if the object has the `skip-remediation` annotation.
-func HasSkipRemediationAnnotation(o metav1.Object) bool {
-	return hasAnnotation(o, clusterv1.MachineSkipRemediationAnnotation)
+	annotations := o.GetAnnotations()
+	if annotations == nil {
+		return false
+	}
+	_, ok := annotations[clusterv1.PausedAnnotation]
+	return ok
 }
 
 func HasWithPrefix(prefix string, annotations map[string]string) bool {
@@ -65,13 +65,3 @@ func AddAnnotations(o metav1.Object, desired map[string]string) bool {
 	}
 	return hasChanged
 }
-
-// hasAnnotation returns true if the object has the specified annotation
-func hasAnnotation(o metav1.Object, annotation string) bool {
-	annotations := o.GetAnnotations()
-	if annotations == nil {
-		return false
-	}
-	_, ok := annotations[annotation]
-	return ok
-}
diff --git a/util/collections/machine_filters.go b/util/collections/machine_filters.go
deleted file mode 100644
index 6c42152a5..000000000
--- a/util/collections/machine_filters.go
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package collections
-
-import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/selection"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
-	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-type Func func(machine *clusterv1.Machine) bool
-
-// And returns a filter that returns true if all of the given filters returns true.
-func And(filters ...Func) Func {
-	return func(machine *clusterv1.Machine) bool {
-		for _, f := range filters {
-			if !f(machine) {
-				return false
-			}
-		}
-		return true
-	}
-}
-
-// Or returns a filter that returns true if any of the given filters returns true.
-func Or(filters ...Func) Func {
-	return func(machine *clusterv1.Machine) bool {
-		for _, f := range filters {
-			if f(machine) {
-				return true
-			}
-		}
-		return false
-	}
-}
-
-// Not returns a filter that returns the opposite of the given filter.
-func Not(mf Func) Func {
-	return func(machine *clusterv1.Machine) bool {
-		return !mf(machine)
-	}
-}
-
-// HasControllerRef is a filter that returns true if the machine has a controller ref
-func HasControllerRef(machine *clusterv1.Machine) bool {
-	if machine == nil {
-		return false
-	}
-	return metav1.GetControllerOf(machine) != nil
-}
-
-// InFailureDomains returns a filter to find all machines
-// in any of the given failure domains
-func InFailureDomains(failureDomains ...*string) Func {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		for i := range failureDomains {
-			fd := failureDomains[i]
-			if fd == nil {
-				if fd == machine.Spec.FailureDomain {
-					return true
-				}
-				continue
-			}
-			if machine.Spec.FailureDomain == nil {
-				continue
-			}
-			if *fd == *machine.Spec.FailureDomain {
-				return true
-			}
-		}
-		return false
-	}
-}
-
-// OwnedMachines returns a filter to find all owned control plane machines.
-// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, machinefilters.OwnedMachines(controlPlane))
-func OwnedMachines(owner client.Object) func(machine *clusterv1.Machine) bool {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		return util.IsOwnedByObject(machine, owner)
-	}
-}
-
-// ControlPlaneMachines returns a filter to find all control plane machines for a cluster, regardless of ownership.
-// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, machinefilters.ControlPlaneMachines(cluster.Name))
-func ControlPlaneMachines(clusterName string) func(machine *clusterv1.Machine) bool {
-	selector := ControlPlaneSelectorForCluster(clusterName)
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		return selector.Matches(labels.Set(machine.Labels))
-	}
-}
-
-// AdoptableControlPlaneMachines returns a filter to find all un-controlled control plane machines.
-// Usage: managementCluster.GetMachinesForCluster(ctx, cluster, AdoptableControlPlaneMachines(cluster.Name, controlPlane))
-func AdoptableControlPlaneMachines(clusterName string) func(machine *clusterv1.Machine) bool {
-	return And(
-		ControlPlaneMachines(clusterName),
-		Not(HasControllerRef),
-	)
-}
-
-// HasDeletionTimestamp returns a filter to find all machines that have a deletion timestamp.
-func HasDeletionTimestamp(machine *clusterv1.Machine) bool {
-	if machine == nil {
-		return false
-	}
-	return !machine.DeletionTimestamp.IsZero()
-}
-
-// HasUnhealthyCondition returns a filter to find all machines that have a MachineHealthCheckSucceeded condition set to False,
-// indicating a problem was detected on the machine, and the MachineOwnerRemediated condition set, indicating that KCP is
-// responsible of performing remediation as owner of the machine.
-func HasUnhealthyCondition(machine *clusterv1.Machine) bool {
-	if machine == nil {
-		return false
-	}
-	return conditions.IsFalse(machine, clusterv1.MachineHealthCheckSuccededCondition) && conditions.IsFalse(machine, clusterv1.MachineOwnerRemediatedCondition)
-}
-
-// IsReady returns a filter to find all machines with the ReadyCondition equals to True.
-func IsReady() Func {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		return conditions.IsTrue(machine, clusterv1.ReadyCondition)
-	}
-}
-
-// ShouldRolloutAfter returns a filter to find all machines where
-// CreationTimestamp < rolloutAfter < reconciliationTIme
-func ShouldRolloutAfter(reconciliationTime, rolloutAfter *metav1.Time) Func {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		return machine.CreationTimestamp.Before(rolloutAfter) && rolloutAfter.Before(reconciliationTime)
-	}
-}
-
-// HasAnnotationKey returns a filter to find all machines that have the
-// specified Annotation key present
-func HasAnnotationKey(key string) Func {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil || machine.Annotations == nil {
-			return false
-		}
-		if _, ok := machine.Annotations[key]; ok {
-			return true
-		}
-		return false
-	}
-}
-
-// ControlPlaneSelectorForCluster returns the label selector necessary to get control plane machines for a given cluster.
-func ControlPlaneSelectorForCluster(clusterName string) labels.Selector {
-	must := func(r *labels.Requirement, err error) labels.Requirement {
-		if err != nil {
-			panic(err)
-		}
-		return *r
-	}
-	return labels.NewSelector().Add(
-		must(labels.NewRequirement(clusterv1.ClusterLabelName, selection.Equals, []string{clusterName})),
-		must(labels.NewRequirement(clusterv1.MachineControlPlaneLabelName, selection.Exists, []string{})),
-	)
-}
-
-// MatchesKubernetesVersion returns a filter to find all machines that match a given Kubernetes version.
-func MatchesKubernetesVersion(kubernetesVersion string) Func {
-	return func(machine *clusterv1.Machine) bool {
-		if machine == nil {
-			return false
-		}
-		if machine.Spec.Version == nil {
-			return false
-		}
-		return *machine.Spec.Version == kubernetesVersion
-	}
-}
diff --git a/util/conversion/conversion.go b/util/conversion/conversion.go
index a5ffdaee9..7e57139ab 100644
--- a/util/conversion/conversion.go
+++ b/util/conversion/conversion.go
@@ -141,27 +141,33 @@ func GetFuzzer(scheme *runtime.Scheme, funcs ...fuzzer.FuzzerFuncs) *fuzz.Fuzzer
 func FuzzTestFunc(scheme *runtime.Scheme, hub conversion.Hub, dst conversion.Convertible, funcs ...fuzzer.FuzzerFuncs) func(*testing.T) {
 	return func(t *testing.T) {
 		t.Run("spoke-hub-spoke", func(t *testing.T) {
+			t.Skip("skipping this because it should account for data loss when there are api changes between versions, needs rework ")
 			g := gomega.NewWithT(t)
 			fuzzer := GetFuzzer(scheme, funcs...)
 
 			for i := 0; i < 10000; i++ {
-				// Create the spoke and fuzz it
-				spokeBefore := dst.DeepCopyObject().(conversion.Convertible)
-				fuzzer.Fuzz(spokeBefore)
+				// Create hub object
+				hubExisting := hub.DeepCopyObject().(conversion.Hub)
+				fuzzer.Fuzz(hubExisting)
 
-				// First convert spoke to hub
-				hubCopy := hub.DeepCopyObject().(conversion.Hub)
-				g.Expect(spokeBefore.ConvertTo(hubCopy)).To(gomega.Succeed())
+				// Convert hub object to spoke
+				spokeFirstGet := dst.DeepCopyObject().(conversion.Convertible)
+				g.Expect(spokeFirstGet.ConvertFrom(hubExisting)).To(gomega.Succeed())
+
+				// Do changes in the spoke
+				fuzzer.Fuzz(spokeFirstGet)
+				// Fuzz() might delete the annotation containing the hub serialized. So, re-add it.
+				MarshalData(hubExisting.(metav1.Object), spokeFirstGet.(metav1.Object))
 
-				// Convert hub back to spoke and check if the resulting spoke is equal to the spoke before the round trip
-				spokeAfter := dst.DeepCopyObject().(conversion.Convertible)
-				g.Expect(spokeAfter.ConvertFrom(hubCopy)).To(gomega.Succeed())
+				// Convert the changed spoke to hub
+				hubUpdated := hub.DeepCopyObject().(conversion.Hub)
+				g.Expect(spokeFirstGet.ConvertTo(hubUpdated)).To(gomega.Succeed())
 
-				// Remove data annotation eventually added by ConvertFrom for avoiding data loss in hub-spoke-hub round trips
-				metaAfter := spokeAfter.(metav1.Object)
-				delete(metaAfter.GetAnnotations(), DataAnnotation)
+				// Convert hub back to spoke and check if the changed spoke is still the same after spoke --> hub --> spoke conversion
+				spokeSecondGet := dst.DeepCopyObject().(conversion.Convertible)
+				g.Expect(spokeSecondGet.ConvertFrom(hubUpdated)).To(gomega.Succeed())
 
-				g.Expect(apiequality.Semantic.DeepEqual(spokeBefore, spokeAfter)).To(gomega.BeTrue(), cmp.Diff(spokeBefore, spokeAfter))
+				g.Expect(apiequality.Semantic.DeepEqual(spokeFirstGet, spokeSecondGet)).To(gomega.BeTrue(), cmp.Diff(spokeFirstGet, spokeSecondGet))
 			}
 		})
 		t.Run("hub-spoke-hub", func(t *testing.T) {
@@ -169,19 +175,22 @@ func FuzzTestFunc(scheme *runtime.Scheme, hub conversion.Hub, dst conversion.Con
 			fuzzer := GetFuzzer(scheme, funcs...)
 
 			for i := 0; i < 10000; i++ {
-				// Create the hub and fuzz it
-				hubBefore := hub.DeepCopyObject().(conversion.Hub)
-				fuzzer.Fuzz(hubBefore)
-
-				// First convert hub to spoke
+				// Make copies of both objects, to avoid changing or re-using the ones passed in.
+				hubCopy := hub.DeepCopyObject().(conversion.Hub)
 				dstCopy := dst.DeepCopyObject().(conversion.Convertible)
-				g.Expect(dstCopy.ConvertFrom(hubBefore)).To(gomega.Succeed())
 
-				// Convert spoke back to hub and check if the resulting hub is equal to the hub before the round trip
-				hubAfter := hub.DeepCopyObject().(conversion.Hub)
-				g.Expect(dstCopy.ConvertTo(hubAfter)).To(gomega.Succeed())
+				// Run the fuzzer on the Hub version copy.
+				fuzzer.Fuzz(hubCopy)
+
+				// Use the hub to convert into the convertible object.
+				g.Expect(dstCopy.ConvertFrom(hubCopy)).To(gomega.Succeed())
+
+				// Make another copy of hub and convert the convertible object back to the hub version.
+				after := hub.DeepCopyObject().(conversion.Hub)
+				g.Expect(dstCopy.ConvertTo(after)).To(gomega.Succeed())
 
-				g.Expect(apiequality.Semantic.DeepEqual(hubBefore, hubAfter)).To(gomega.BeTrue(), cmp.Diff(hubBefore, hubAfter))
+				// Make sure that the hub before the conversions and after are the same, include a diff if not.
+				g.Expect(apiequality.Semantic.DeepEqual(hubCopy, after)).To(gomega.BeTrue(), cmp.Diff(hubCopy, after))
 			}
 		})
 	}
diff --git a/util/patch/patch.go b/util/patch/patch.go
index fd0586b15..3f0e261a5 100644
--- a/util/patch/patch.go
+++ b/util/patch/patch.go
@@ -26,19 +26,16 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	kerrors "k8s.io/apimachinery/pkg/util/errors"
 	"k8s.io/apimachinery/pkg/util/wait"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 )
 
 // Helper is a utility for ensuring the proper patching of objects.
 type Helper struct {
 	client       client.Client
-	gvk          schema.GroupVersionKind
 	beforeObject client.Object
 	before       *unstructured.Unstructured
 	after        *unstructured.Unstructured
@@ -50,15 +47,10 @@ type Helper struct {
 // NewHelper returns an initialized Helper
 func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
 	// Return early if the object is nil.
-	if err := checkNilObject(obj); err != nil {
-		return nil, err
-	}
-
-	// Get the GroupVersionKind of the object,
-	// used to validate against later on.
-	gvk, err := apiutil.GVKForObject(obj, crClient.Scheme())
-	if err != nil {
-		return nil, err
+	// If you're wondering why we need reflection to do this check, see https://golang.org/doc/faq#nil_error.
+	// TODO(vincepri): Remove this check and let it panic if used improperly in a future minor release.
+	if obj == nil || (reflect.ValueOf(obj).IsValid() && reflect.ValueOf(obj).IsNil()) {
+		return nil, errors.Errorf("expected non-nil object")
 	}
 
 	// Convert the object to unstructured to compare against our before copy.
@@ -72,7 +64,6 @@ func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
 
 	return &Helper{
 		client:             crClient,
-		gvk:                gvk,
 		before:             unstructuredObj,
 		beforeObject:       obj.DeepCopyObject().(client.Object),
 		isConditionsSetter: canInterfaceConditions,
@@ -81,18 +72,8 @@ func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
 
 // Patch will attempt to patch the given object, including its status.
 func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) error {
-	// Return early if the object is nil.
-	if err := checkNilObject(obj); err != nil {
-		return err
-	}
-
-	// Get the GroupVersionKind of the object that we want to patch.
-	gvk, err := apiutil.GVKForObject(obj, h.client.Scheme())
-	if err != nil {
-		return err
-	}
-	if gvk != h.gvk {
-		return errors.Errorf("unmatched GroupVersionKind, expected %q got %q", h.gvk, gvk)
+	if obj == nil {
+		return errors.Errorf("expected non-nil object")
 	}
 
 	// Calculate the options.
@@ -102,6 +83,7 @@ func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) e
 	}
 
 	// Convert the object to unstructured to compare against our before copy.
+	var err error
 	h.after, err = toUnstructured(obj)
 	if err != nil {
 		return err
@@ -297,12 +279,3 @@ func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error)
 	}
 	return res, nil
 }
-
-func checkNilObject(obj client.Object) error {
-	// If you're wondering why we need reflection to do this check, see https://golang.org/doc/faq#nil_error.
-	// TODO(vincepri): Remove this check and let it panic if used improperly in a future minor release.
-	if obj == nil || (reflect.ValueOf(obj).IsValid() && reflect.ValueOf(obj).IsNil()) {
-		return errors.Errorf("expected non-nil object")
-	}
-	return nil
-}
diff --git a/util/util.go b/util/util.go
index be402af71..3bd63668c 100644
--- a/util/util.go
+++ b/util/util.go
@@ -22,6 +22,8 @@ import (
 	"fmt"
 	"math"
 	"math/rand"
+	"regexp"
+	"strconv"
 	"strings"
 	"time"
 
@@ -36,7 +38,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	k8sversion "k8s.io/apimachinery/pkg/version"
+	"k8s.io/apimachinery/pkg/version"
 	"k8s.io/client-go/metadata"
 	"k8s.io/client-go/rest"
 	"k8s.io/klog/klogr"
@@ -44,7 +46,6 @@ import (
 	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/container"
 	"sigs.k8s.io/cluster-api/util/predicates"
-	"sigs.k8s.io/cluster-api/util/version"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
@@ -66,8 +67,35 @@ var (
 	rnd                          = rand.New(rand.NewSource(time.Now().UnixNano()))
 	ErrNoCluster                 = fmt.Errorf("no %q label present", clusterv1.ClusterLabelName)
 	ErrUnstructuredFieldNotFound = fmt.Errorf("field not found")
+	kubeSemver                   = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
 )
 
+// ParseMajorMinorPatch returns a semver.Version from the string provided
+// by looking only at major.minor.patch and stripping everything else out.
+func ParseMajorMinorPatch(version string) (semver.Version, error) {
+	groups := kubeSemver.FindStringSubmatch(version)
+	if len(groups) < 4 {
+		return semver.Version{}, errors.Errorf("failed to parse major.minor.patch from %q", version)
+	}
+	major, err := strconv.ParseUint(groups[1], 10, 64)
+	if err != nil {
+		return semver.Version{}, errors.Wrapf(err, "failed to parse major version from %q", version)
+	}
+	minor, err := strconv.ParseUint(groups[2], 10, 64)
+	if err != nil {
+		return semver.Version{}, errors.Wrapf(err, "failed to parse minor version from %q", version)
+	}
+	patch, err := strconv.ParseUint(groups[3], 10, 64)
+	if err != nil {
+		return semver.Version{}, errors.Wrapf(err, "failed to parse patch version from %q", version)
+	}
+	return semver.Version{
+		Major: major,
+		Minor: minor,
+		Patch: patch,
+	}, nil
+}
+
 // RandomString returns a random alphanumeric string.
 func RandomString(n int) string {
 	result := make([]byte, n)
@@ -100,13 +128,6 @@ func Ordinalize(n int) string {
 	return fmt.Sprintf("%d%s", n, m[an%10])
 }
 
-// ParseMajorMinorPatch returns a semver.Version from the string provided
-// by looking only at major.minor.patch and stripping everything else out.
-// Deprecated: Please use the function in util/version
-func ParseMajorMinorPatch(v string) (semver.Version, error) {
-	return version.ParseMajorMinorPatchTolerant(v)
-}
-
 // ModifyImageRepository takes an imageName (e.g., repository/image:tag), and returns an image name with updated repository
 // Deprecated: Please use the functions in util/container
 func ModifyImageRepository(imageName, repositoryName string) (string, error) {
@@ -576,7 +597,7 @@ type KubeAwareAPIVersions []string
 func (k KubeAwareAPIVersions) Len() int      { return len(k) }
 func (k KubeAwareAPIVersions) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
 func (k KubeAwareAPIVersions) Less(i, j int) bool {
-	return k8sversion.CompareKubeAwareVersionStrings(k[i], k[j]) < 0
+	return version.CompareKubeAwareVersionStrings(k[i], k[j]) < 0
 }
 
 // MachinesByCreationTimestamp sorts a list of Machine by creation timestamp, using their names as a tie breaker.
diff --git a/util/version/version.go b/util/version/version.go
deleted file mode 100644
index 6e8d9c79a..000000000
--- a/util/version/version.go
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package version
-
-import (
-	"github.com/blang/semver"
-	"github.com/pkg/errors"
-	"regexp"
-	"strconv"
-)
-
-var (
-	// KubeSemver is the regex for Kubernetes versions. It requires the "v" prefix.
-	KubeSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
-	// KubeSemverTolerant is the regex for Kubernetes versions with an optional "v" prefix.
-	KubeSemverTolerant = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`)
-)
-
-// ParseMajorMinorPatch returns a semver.Version from the string provided
-// by looking only at major.minor.patch and stripping everything else out.
-// It requires the version to have a "v" prefix.
-func ParseMajorMinorPatch(version string) (semver.Version, error) {
-	return parseMajorMinorPatch(version, false)
-}
-
-// ParseMajorMinorPatchTolerant returns a semver.Version from the string provided
-// by looking only at major.minor.patch and stripping everything else out.
-// It does not require the version to have a "v" prefix.
-func ParseMajorMinorPatchTolerant(version string) (semver.Version, error) {
-	return parseMajorMinorPatch(version, true)
-}
-
-// parseMajorMinorPatch returns a semver.Version from the string provided
-// by looking only at major.minor.patch and stripping everything else out.
-func parseMajorMinorPatch(version string, tolerant bool) (semver.Version, error) {
-	groups := KubeSemver.FindStringSubmatch(version)
-	if tolerant {
-		groups = KubeSemverTolerant.FindStringSubmatch(version)
-	}
-	if len(groups) < 4 {
-		return semver.Version{}, errors.Errorf("failed to parse major.minor.patch from %q", version)
-	}
-	major, err := strconv.ParseUint(groups[1], 10, 64)
-	if err != nil {
-		return semver.Version{}, errors.Wrapf(err, "failed to parse major version from %q", version)
-	}
-	minor, err := strconv.ParseUint(groups[2], 10, 64)
-	if err != nil {
-		return semver.Version{}, errors.Wrapf(err, "failed to parse minor version from %q", version)
-	}
-	patch, err := strconv.ParseUint(groups[3], 10, 64)
-	if err != nil {
-		return semver.Version{}, errors.Wrapf(err, "failed to parse patch version from %q", version)
-	}
-	return semver.Version{
-		Major: major,
-		Minor: minor,
-		Patch: patch,
-	}, nil
-}


Test patch:
diff --git a/api/v1alpha3/conversion_test.go b/api/v1alpha3/conversion_test.go
index 67fac5681..1e0c2de39 100644
--- a/api/v1alpha3/conversion_test.go
+++ b/api/v1alpha3/conversion_test.go
@@ -19,11 +19,9 @@ package v1alpha3
 import (
 	"testing"
 
-	fuzz "github.com/google/gofuzz"
 	. "github.com/onsi/gomega"
 
 	"k8s.io/apimachinery/pkg/runtime"
-	runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
 	"sigs.k8s.io/cluster-api/api/v1alpha4"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 )
@@ -35,20 +33,7 @@ func TestFuzzyConversion(t *testing.T) {
 	g.Expect(v1alpha4.AddToScheme(scheme)).To(Succeed())
 
 	t.Run("for Cluster", utilconversion.FuzzTestFunc(scheme, &v1alpha4.Cluster{}, &Cluster{}))
-	t.Run("for Machine", utilconversion.FuzzTestFunc(scheme, &v1alpha4.Machine{}, &Machine{}, BootstrapFuzzFuncs))
-	t.Run("for MachineSet", utilconversion.FuzzTestFunc(scheme, &v1alpha4.MachineSet{}, &MachineSet{}, BootstrapFuzzFuncs))
-	t.Run("for MachineDeployment", utilconversion.FuzzTestFunc(scheme, &v1alpha4.MachineDeployment{}, &MachineDeployment{}, BootstrapFuzzFuncs))
-}
-
-func BootstrapFuzzFuncs(_ runtimeserializer.CodecFactory) []interface{} {
-	return []interface{}{
-		BootstrapFuzzer,
-	}
-}
-
-func BootstrapFuzzer(obj *Bootstrap, c fuzz.Continue) {
-	c.FuzzNoCustom(obj)
-
-	// Bootstrap.Data has been removed in v1alpha4, so setting it to nil in order to avoid v1alpha3 --> v1alpha4 --> v1alpha3 round trip errors.
-	obj.Data = nil
+	t.Run("for Machine", utilconversion.FuzzTestFunc(scheme, &v1alpha4.Machine{}, &Machine{}))
+	t.Run("for MachineSet", utilconversion.FuzzTestFunc(scheme, &v1alpha4.MachineSet{}, &MachineSet{}))
+	t.Run("for MachineDeployment", utilconversion.FuzzTestFunc(scheme, &v1alpha4.MachineDeployment{}, &MachineDeployment{}))
 }
diff --git a/api/v1alpha4/machinedeployment_webhook_test.go b/api/v1alpha4/machinedeployment_webhook_test.go
index c0dc40283..f4c114694 100644
--- a/api/v1alpha4/machinedeployment_webhook_test.go
+++ b/api/v1alpha4/machinedeployment_webhook_test.go
@@ -36,6 +36,7 @@ func TestMachineDeploymentDefault(t *testing.T) {
 	md.Default()
 
 	g.Expect(md.Labels[ClusterLabelName]).To(Equal(md.Spec.ClusterName))
+	g.Expect(md.Spec.Replicas).To(Equal(pointer.Int32Ptr(1)))
 	g.Expect(md.Spec.MinReadySeconds).To(Equal(pointer.Int32Ptr(0)))
 	g.Expect(md.Spec.RevisionHistoryLimit).To(Equal(pointer.Int32Ptr(1)))
 	g.Expect(md.Spec.ProgressDeadlineSeconds).To(Equal(pointer.Int32Ptr(600)))
diff --git a/api/v1alpha4/machineset_webhook_test.go b/api/v1alpha4/machineset_webhook_test.go
index 7297199fb..466a86e53 100644
--- a/api/v1alpha4/machineset_webhook_test.go
+++ b/api/v1alpha4/machineset_webhook_test.go
@@ -22,6 +22,7 @@ import (
 	. "github.com/onsi/gomega"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
 )
 
 func TestMachineSetDefault(t *testing.T) {
@@ -35,6 +36,7 @@ func TestMachineSetDefault(t *testing.T) {
 	md.Default()
 
 	g.Expect(md.Labels[ClusterLabelName]).To(Equal(md.Spec.ClusterName))
+	g.Expect(md.Spec.Replicas).To(Equal(pointer.Int32Ptr(1)))
 	g.Expect(md.Spec.DeletePolicy).To(Equal(string(RandomMachineSetDeletePolicy)))
 	g.Expect(md.Spec.Selector.MatchLabels).To(HaveKeyWithValue(MachineSetLabelName, "test-ms"))
 	g.Expect(md.Spec.Template.Labels).To(HaveKeyWithValue(MachineSetLabelName, "test-ms"))
diff --git a/bootstrap/kubeadm/api/v1alpha3/conversion_test.go b/bootstrap/kubeadm/api/v1alpha3/conversion_test.go
index 300b9fd50..589c077d1 100644
--- a/bootstrap/kubeadm/api/v1alpha3/conversion_test.go
+++ b/bootstrap/kubeadm/api/v1alpha3/conversion_test.go
@@ -19,9 +19,7 @@ package v1alpha3
 import (
 	"testing"
 
-	fuzz "github.com/google/gofuzz"
 	. "github.com/onsi/gomega"
-	runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha4"
@@ -34,19 +32,6 @@ func TestFuzzyConversion(t *testing.T) {
 	g.Expect(AddToScheme(scheme)).To(Succeed())
 	g.Expect(v1alpha4.AddToScheme(scheme)).To(Succeed())
 
-	t.Run("for KubeadmConfig", utilconversion.FuzzTestFunc(scheme, &v1alpha4.KubeadmConfig{}, &KubeadmConfig{}, KubeadmConfigStatusFuzzFuncs))
+	t.Run("for KubeadmConfig", utilconversion.FuzzTestFunc(scheme, &v1alpha4.KubeadmConfig{}, &KubeadmConfig{}))
 	t.Run("for KubeadmConfigTemplate", utilconversion.FuzzTestFunc(scheme, &v1alpha4.KubeadmConfigTemplate{}, &KubeadmConfigTemplate{}))
 }
-
-func KubeadmConfigStatusFuzzFuncs(_ runtimeserializer.CodecFactory) []interface{} {
-	return []interface{}{
-		KubeadmConfigStatusFuzzer,
-	}
-}
-
-func KubeadmConfigStatusFuzzer(obj *KubeadmConfigStatus, c fuzz.Continue) {
-	c.FuzzNoCustom(obj)
-
-	// KubeadmConfigStatus.BootstrapData has been removed in v1alpha4, so setting it to nil in order to avoid v1alpha3 --> v1alpha4 --> v1alpha3 round trip errors.
-	obj.BootstrapData = nil
-}
diff --git a/cmd/clusterctl/client/cluster/installer_test.go b/cmd/clusterctl/client/cluster/installer_test.go
index 828fb7d9a..c34c307f5 100644
--- a/cmd/clusterctl/client/cluster/installer_test.go
+++ b/cmd/clusterctl/client/cluster/installer_test.go
@@ -36,42 +36,61 @@ func Test_providerInstaller_Validate(t *testing.T) {
 
 	repositoryMap := map[string]repository.Repository{
 		"cluster-api": test.NewFakeRepository().
-			WithVersions("v1.0.0", "v1.0.1").
+			WithVersions("v0.9.0", "v1.0.0", "v1.0.1", "v2.0.0").
+			WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+				ReleaseSeries: []clusterctlv1.ReleaseSeries{
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				},
+			}).
 			WithMetadata("v1.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
 				},
 			}).
 			WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
-					{Major: 2, Minor: 0, Contract: "v1alpha4"},
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
+					{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
 				},
 			}),
 		"infrastructure-infra1": test.NewFakeRepository().
-			WithVersions("v1.0.0", "v1.0.1").
+			WithVersions("v0.9.0", "v1.0.0", "v1.0.1", "v2.0.0").
+			WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+				ReleaseSeries: []clusterctlv1.ReleaseSeries{
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				},
+			}).
 			WithMetadata("v1.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
 				},
 			}).
 			WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
-					{Major: 2, Minor: 0, Contract: "v1alpha4"},
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
+					{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
 				},
 			}),
 		"infrastructure-infra2": test.NewFakeRepository().
-			WithVersions("v1.0.0", "v1.0.1").
+			WithVersions("v0.9.0", "v1.0.0", "v1.0.1", "v2.0.0").
+			WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+				ReleaseSeries: []clusterctlv1.ReleaseSeries{
+					{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				},
+			}).
 			WithMetadata("v1.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
 				},
 			}).
 			WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 				ReleaseSeries: []clusterctlv1.ReleaseSeries{
-					{Major: 1, Minor: 0, Contract: "v1alpha3"},
-					{Major: 2, Minor: 0, Contract: "v1alpha4"},
+					{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
+					{Major: 2, Minor: 0, Contract: test.NextCAPIContractNotSupported},
 				},
 			}),
 	}
@@ -86,10 +105,10 @@ func Test_providerInstaller_Validate(t *testing.T) {
 		wantErr bool
 	}{
 		{
-			name: "install core + infra1 on an empty cluster",
+			name: "install core/current contract + infra1/current contract on an empty cluster",
 			fields: fields{
 				proxy: test.NewFakeProxy(), //empty cluster
-				installQueue: []repository.Components{ // install core + infra1, v1alpha3 contract
+				installQueue: []repository.Components{
 					newFakeComponents("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", ""),
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
 				},
@@ -97,82 +116,126 @@ func Test_providerInstaller_Validate(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "install infra2 on a cluster already initialized with core + infra1",
+			name: "install infra2/current contract on a cluster already initialized with core/current contract + infra1/current contract",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with core + infra1, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
-								WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
-				installQueue: []repository.Components{ // install infra2, v1alpha3 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
+					WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
+				installQueue: []repository.Components{
 					newFakeComponents("infra2", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra2-system", ""),
 				},
 			},
 			wantErr: false,
 		},
 		{
-			name: "install another instance of infra1 on a cluster already initialized with core + infra1, no overlaps",
+			name: "install another instance of infra1/current contract on a cluster already initialized with core/current contract + infra1/current contract, no overlaps",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with core + infra1, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
-								WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns1", "ns1"),
-				installQueue: []repository.Components{ // install infra2, v1alpha3 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
+					WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns1", "ns1"),
+				installQueue: []repository.Components{
 					newFakeComponents("infra2", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns2", "ns2"),
 				},
 			},
 			wantErr: false,
 		},
 		{
-			name: "install another instance of infra1 on a cluster already initialized with core + infra1, same namespace of the existing infra1",
+			name: "install another instance of infra1/current contract on a cluster already initialized with core/current contract + infra1/current contract, same namespace of the existing infra1",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with core + infra1, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
-								WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "n1", ""),
-				installQueue: []repository.Components{ // install infra1, v1alpha3 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
+					WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "n1", ""),
+				installQueue: []repository.Components{
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "n1", ""),
 				},
 			},
 			wantErr: true,
 		},
 		{
-			name: "install another instance of infra1 on a cluster already initialized with core + infra1, watching overlap with the existing infra1",
+			name: "install another instance of infra1/current contract on a cluster already initialized with core/current contract + infra1/current contract, watching overlap with the existing infra1",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with core + infra1, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
-								WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
-				installQueue: []repository.Components{ // install infra1, v1alpha3 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", "").
+					WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
+				installQueue: []repository.Components{
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra2-system", ""),
 				},
 			},
 			wantErr: true,
 		},
 		{
-			name: "install another instance of infra1 on a cluster already initialized with core + infra1, not part of the existing management group",
+			name: "install another instance of infra1/current contract on a cluster already initialized with core/current contract + infra1/current contract, not part of the existing management group",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with core + infra1, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1").
-								WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns1", "ns1"),
-				installQueue: []repository.Components{ // install infra1, v1alpha3 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1").
+					WithProviderInventory("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns1", "ns1"),
+				installQueue: []repository.Components{
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "ns2", "ns2"),
 				},
 			},
 			wantErr: true,
 		},
 		{
-			name: "install an instance of infra1 on a cluster already initialized with two core, but it is part of two management group",
+			name: "install an instance of infra1/current contract on a cluster already initialized with two core/current contract, but it is part of two management group",
 			fields: fields{
 				proxy: test.NewFakeProxy(). // cluster with two core (two management groups)
 								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1").
 								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns2", "ns2"),
-				installQueue: []repository.Components{ // install infra1, v1alpha3 contract
+				installQueue: []repository.Components{
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
 				},
 			},
 			wantErr: true,
 		},
 		{
-			name: "install core@v1alpha3 + infra1@v1alpha4 on an empty cluster",
+			name: "install core/previous contract + infra1/previous contract on an empty cluster (not supported)",
+			fields: fields{
+				proxy: test.NewFakeProxy(), //empty cluster
+				installQueue: []repository.Components{
+					newFakeComponents("cluster-api", clusterctlv1.CoreProviderType, "v0.9.0", "cluster-api-system", ""),
+					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v0.9.0", "infra1-system", ""),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "install core/previous contract + infra1/current contract on an empty cluster (not supported)",
+			fields: fields{
+				proxy: test.NewFakeProxy(), //empty cluster
+				installQueue: []repository.Components{
+					newFakeComponents("cluster-api", clusterctlv1.CoreProviderType, "v0.9.0", "cluster-api-system", ""),
+					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v1.0.0", "infra1-system", ""),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "install infra1/previous contract (not supported) on a cluster already initialized with core/current contract",
+			fields: fields{
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1"),
+				installQueue: []repository.Components{
+					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v0.9.0", "infra1-system", ""),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "install core/next contract + infra1/next contract on an empty cluster (not supported)",
+			fields: fields{
+				proxy: test.NewFakeProxy(), //empty cluster
+				installQueue: []repository.Components{
+					newFakeComponents("cluster-api", clusterctlv1.CoreProviderType, "v2.0.0", "cluster-api-system", ""),
+					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra1-system", ""),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "install core/current contract + infra1/next contract on an empty cluster (not supported)",
 			fields: fields{
 				proxy: test.NewFakeProxy(), //empty cluster
-				installQueue: []repository.Components{ // install core + infra1, v1alpha3 contract
+				installQueue: []repository.Components{
 					newFakeComponents("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system", ""),
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra1-system", ""),
 				},
@@ -180,11 +243,11 @@ func Test_providerInstaller_Validate(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "install infra1@v1alpha4 on a cluster already initialized with core@v1alpha3 +",
+			name: "install infra1/next contract (not supported) on a cluster already initialized with core/current contract",
 			fields: fields{
-				proxy: test.NewFakeProxy(). // cluster with one core, v1alpha3 contract
-								WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1"),
-				installQueue: []repository.Components{ // install infra1, v1alpha4 contract
+				proxy: test.NewFakeProxy().
+					WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "ns1", "ns1"),
+				installQueue: []repository.Components{
 					newFakeComponents("infra1", clusterctlv1.InfrastructureProviderType, "v2.0.0", "infra1-system", ""),
 				},
 			},
diff --git a/cmd/clusterctl/client/init_test.go b/cmd/clusterctl/client/init_test.go
index 49f875e6e..f78975c4e 100644
--- a/cmd/clusterctl/client/init_test.go
+++ b/cmd/clusterctl/client/init_test.go
@@ -81,7 +81,7 @@ func Test_clusterctlClient_InitImages(t *testing.T) {
 			},
 			expectedImages: []string{
 				"gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0",
-				"k8s.gcr.io/cluster-api-aws/cluster-api-aws-controller:v0.5.3",
+				"us.gcr.io/k8s-artifacts-prod/cluster-api-aws/cluster-api-aws-controller:v0.5.3",
 			},
 			wantErr: false,
 		},
@@ -245,7 +245,7 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name: "Init (with an empty cluster) with default provider versions",
+			name: "Init (with an empty cluster) with default provider versions/current contract",
 			field: field{
 				client: fakeEmptyCluster(), // clusterctl client for an empty management cluster (with repository setup for capi, bootstrap, control plane and infra provider)
 				hasCRD: false,
@@ -287,7 +287,7 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "Init (with an empty cluster) opting out from automatic install of providers",
+			name: "Init (with an empty cluster) opting out from automatic install of providers/current contract",
 			field: field{
 				client: fakeEmptyCluster(), // clusterctl client for an empty management cluster (with repository setup for capi, bootstrap, control plane and infra provider)
 				hasCRD: false,
@@ -317,7 +317,7 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "Init (with an empty cluster) with custom provider versions",
+			name: "Init (with an empty cluster) with custom provider versions/current contract",
 			field: field{
 				client: fakeEmptyCluster(), // clusterctl client for an empty management cluster (with repository setup for capi, bootstrap, control plane and infra provider)
 				hasCRD: false,
@@ -359,7 +359,7 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "Init (with an empty cluster) with target namespace",
+			name: "Init (with an empty cluster) with target namespace/current contract",
 			field: field{
 				client: fakeEmptyCluster(), // clusterctl client for an empty management cluster (with repository setup for capi, bootstrap, control plane and infra provider)
 				hasCRD: false,
@@ -400,7 +400,7 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "Init (with a NOT empty cluster) adds a provider",
+			name: "Init (with a NOT empty cluster) adds a provider/current contract",
 			field: field{
 				client: fakeInitializedCluster(), // clusterctl client for an management cluster with capi installed (with repository setup for capi, bootstrap, control plane and infra provider)
 				hasCRD: true,
@@ -508,6 +508,37 @@ func Test_clusterctlClient_Init(t *testing.T) {
 			want:    nil,
 			wantErr: true,
 		},
+		{
+			name: "Init (with an empty cluster) with custom provider versions/previous contract, not supported",
+			field: field{
+				client: fakeEmptyCluster(), // clusterctl client for an empty management cluster (with repository setup for capi, bootstrap, control plane and infra provider)
+				hasCRD: false,
+			},
+			args: args{
+				coreProvider:           fmt.Sprintf("%s:v0.9.0", config.ClusterAPIProviderName),
+				bootstrapProvider:      []string{fmt.Sprintf("%s:v0.9.0", config.KubeadmBootstrapProviderName)},
+				controlPlaneProvider:   []string{fmt.Sprintf("%s:v0.9.0", config.KubeadmControlPlaneProviderName)},
+				infrastructureProvider: []string{"infra:v0.9.0"},
+				targetNameSpace:        "",
+				watchingNamespace:      "",
+			},
+			wantErr: true,
+		},
+		{
+			name: "Init (with a NOT empty cluster) adds a provider/previous contract, not supported",
+			field: field{
+				client: fakeInitializedCluster(), // clusterctl client for an management cluster with capi installed (with repository setup for capi, bootstrap, control plane and infra provider)
+				hasCRD: true,
+			},
+			args: args{
+				coreProvider:           "", // with a NOT empty cluster, a core provider should NOT be added automatically
+				bootstrapProvider:      []string{fmt.Sprintf("%s:v0.9.0", config.KubeadmBootstrapProviderName)},
+				infrastructureProvider: []string{"infra:v0.9.0"},
+				targetNameSpace:        "",
+				watchingNamespace:      "",
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -617,61 +648,93 @@ func fakeRepositories(config *fakeConfigClient, providers []Provider) []*fakeRep
 	repository1 := newFakeRepository(capiProviderConfig, config).
 		WithPaths("root", "components.yaml").
 		WithDefaultVersion("v1.0.0").
+		WithFile("v0.9.0", "components.yaml", componentsYAML("ns1")).
+		WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+			ReleaseSeries: []clusterctlv1.ReleaseSeries{
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+			},
+		}).
 		WithFile("v1.0.0", "components.yaml", componentsYAML("ns1")).
 		WithMetadata("v1.0.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 1, Minor: 0, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
 			},
 		}).
 		WithFile("v1.1.0", "components.yaml", componentsYAML("ns1")).
 		WithMetadata("v1.1.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 1, Minor: 1, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 1, Minor: 1, Contract: test.CurrentCAPIContract},
 			},
 		})
 	repository2 := newFakeRepository(bootstrapProviderConfig, config).
 		WithPaths("root", "components.yaml").
 		WithDefaultVersion("v2.0.0").
+		WithFile("v0.9.0", "components.yaml", componentsYAML("ns1")).
+		WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+			ReleaseSeries: []clusterctlv1.ReleaseSeries{
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+			},
+		}).
 		WithFile("v2.0.0", "components.yaml", componentsYAML("ns2")).
 		WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 2, Minor: 0, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
 			},
 		}).
 		WithFile("v2.1.0", "components.yaml", componentsYAML("ns2")).
 		WithMetadata("v2.1.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 2, Minor: 1, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 2, Minor: 1, Contract: test.CurrentCAPIContract},
 			},
 		})
 	repository3 := newFakeRepository(controlPlaneProviderConfig, config).
 		WithPaths("root", "components.yaml").
 		WithDefaultVersion("v2.0.0").
+		WithFile("v0.9.0", "components.yaml", componentsYAML("ns1")).
+		WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+			ReleaseSeries: []clusterctlv1.ReleaseSeries{
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+			},
+		}).
 		WithFile("v2.0.0", "components.yaml", componentsYAML("ns3")).
 		WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 2, Minor: 0, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
 			},
 		}).
 		WithFile("v2.1.0", "components.yaml", componentsYAML("ns3")).
 		WithMetadata("v2.1.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 2, Minor: 1, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 2, Minor: 1, Contract: test.CurrentCAPIContract},
 			},
 		})
 	repository4 := newFakeRepository(infraProviderConfig, config).
 		WithPaths("root", "components.yaml").
 		WithDefaultVersion("v3.0.0").
+		WithFile("v0.9.0", "components.yaml", componentsYAML("ns1")).
+		WithMetadata("v0.9.0", &clusterctlv1.Metadata{
+			ReleaseSeries: []clusterctlv1.ReleaseSeries{
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+			},
+		}).
 		WithFile("v3.0.0", "components.yaml", infraComponentsYAML("ns4")).
 		WithMetadata("v3.0.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 3, Minor: 0, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 3, Minor: 0, Contract: test.CurrentCAPIContract},
 			},
 		}).
 		WithFile("v3.1.0", "components.yaml", infraComponentsYAML("ns4")).
 		WithMetadata("v3.1.0", &clusterctlv1.Metadata{
 			ReleaseSeries: []clusterctlv1.ReleaseSeries{
-				{Major: 3, Minor: 1, Contract: "v1alpha3"},
+				{Major: 0, Minor: 9, Contract: test.PreviousCAPIContractNotSupported},
+				{Major: 3, Minor: 1, Contract: test.CurrentCAPIContract},
 			},
 		}).
 		WithFile("v3.0.0", "cluster-template.yaml", templateYAML("ns4", "test"))
@@ -686,7 +749,7 @@ func fakeRepositories(config *fakeConfigClient, providers []Provider) []*fakeRep
 				WithFile("v2.0.0", "components.yaml", componentsYAML("ns2")).
 				WithMetadata("v2.0.0", &clusterctlv1.Metadata{
 					ReleaseSeries: []clusterctlv1.ReleaseSeries{
-						{Major: 2, Minor: 0, Contract: "v1alpha3"},
+						{Major: 2, Minor: 0, Contract: test.CurrentCAPIContract},
 					},
 				}))
 	}
@@ -767,7 +830,7 @@ spec:
       containers:
       - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0
         name: kube-rbac-proxy
-      - image: k8s.gcr.io/cluster-api-aws/cluster-api-aws-controller:v0.5.3
+      - image: us.gcr.io/k8s-artifacts-prod/cluster-api-aws/cluster-api-aws-controller:v0.5.3
         name: manager
         volumeMounts:
         - mountPath: /home/.aws
diff --git a/cmd/clusterctl/internal/test/contracts.go b/cmd/clusterctl/internal/test/contracts.go
new file mode 100644
index 000000000..a5ef55b85
--- /dev/null
+++ b/cmd/clusterctl/internal/test/contracts.go
@@ -0,0 +1,28 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package test
+
+import clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
+
+// PreviousCAPIContractNotSupported define the previous Cluster API contract, not supported by this release of clusterctl.
+const PreviousCAPIContractNotSupported = "v1alpha3"
+
+// CurrentCAPIContract define the current Cluster API contract.
+var CurrentCAPIContract = clusterv1.GroupVersion.Version
+
+// NextCAPIContractNotSupported define the next Cluster API contract, not supported by this release of clusterctl.
+const NextCAPIContractNotSupported = "v1alpha5"
diff --git a/controllers/external/tracker_test.go b/controllers/external/tracker_test.go
deleted file mode 100644
index 9d3ef6c24..000000000
--- a/controllers/external/tracker_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package external
-
-import (
-	"testing"
-
-	. "github.com/onsi/gomega"
-	"github.com/pkg/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
-	"sigs.k8s.io/controller-runtime/pkg/controller"
-	"sigs.k8s.io/controller-runtime/pkg/handler"
-	"sigs.k8s.io/controller-runtime/pkg/log"
-	"sigs.k8s.io/controller-runtime/pkg/predicate"
-	"sigs.k8s.io/controller-runtime/pkg/source"
-)
-
-var (
-	logger = log.NullLogger{}
-)
-
-type fakeController struct {
-	controller.Controller
-}
-
-type watchCountController struct {
-	// can not directly embed an interface when a pointer receiver is
-	// used in any of the overriding methods.
-	*fakeController
-	// no.of times Watch was called
-	count      int
-	raiseError bool
-}
-
-func newWatchCountController(raiseError bool) *watchCountController {
-	return &watchCountController{
-		raiseError: raiseError,
-	}
-}
-
-func (c *watchCountController) Watch(_ source.Source, _ handler.EventHandler, _ ...predicate.Predicate) error {
-	c.count++
-	if c.raiseError {
-		return errors.New("injected failure")
-	}
-	return nil
-}
-
-func TestRetryWatch(t *testing.T) {
-	g := NewWithT(t)
-	ctrl := newWatchCountController(true)
-	tracker := ObjectTracker{Controller: ctrl}
-
-	err := tracker.Watch(logger, &clusterv1.Cluster{}, nil)
-	g.Expect(err).To(HaveOccurred())
-	g.Expect(ctrl.count).Should(Equal(1))
-	// Calling Watch on same Object kind that failed earlier should be retryable.
-	err = tracker.Watch(logger, &clusterv1.Cluster{}, nil)
-	g.Expect(err).To(HaveOccurred())
-	g.Expect(ctrl.count).Should(Equal(2))
-}
-
-func TestWatchMultipleTimes(t *testing.T) {
-	g := NewWithT(t)
-	ctrl := &watchCountController{}
-	tracker := ObjectTracker{Controller: ctrl}
-
-	obj := &clusterv1.Cluster{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Cluster",
-			APIVersion: clusterv1.GroupVersion.Version,
-		},
-	}
-	err := tracker.Watch(logger, obj, nil)
-	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(ctrl.count).Should(Equal(1))
-	// Calling Watch on same Object kind should not register watch again.
-	err = tracker.Watch(logger, obj, nil)
-	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(ctrl.count).Should(Equal(1))
-}
diff --git a/controllers/machinehealthcheck_targets_test.go b/controllers/machinehealthcheck_targets_test.go
index a0a8cb9d0..caea281d0 100644
--- a/controllers/machinehealthcheck_targets_test.go
+++ b/controllers/machinehealthcheck_targets_test.go
@@ -75,14 +75,6 @@ func TestGetTargetsFromMHC(t *testing.T) {
 	testNode4 := newTestNode("node4")
 	testMachine4 := newTestMachine("machine4", namespace, "other-cluster", testNode4.Name, mhcSelector)
 
-	// machines for skip remediation
-	testNode5 := newTestNode("node5")
-	testMachine5 := newTestMachine("machine5", namespace, clusterName, testNode5.Name, mhcSelector)
-	testMachine5.Annotations = map[string]string{"cluster.x-k8s.io/skip-remediation": ""}
-	testNode6 := newTestNode("node6")
-	testMachine6 := newTestMachine("machine6", namespace, clusterName, testNode6.Name, mhcSelector)
-	testMachine6.Annotations = map[string]string{"cluster.x-k8s.io/paused": ""}
-
 	testCases := []struct {
 		desc            string
 		toCreate        []client.Object
@@ -132,17 +124,6 @@ func TestGetTargetsFromMHC(t *testing.T) {
 				},
 			},
 		},
-		{
-			desc:     "with machines having skip-remediation or paused annotation",
-			toCreate: append(baseObjects, testNode1, testMachine1, testMachine5, testMachine6),
-			expectedTargets: []healthCheckTarget{
-				{
-					Machine: testMachine1,
-					MHC:     testMHC,
-					Node:    testNode1,
-				},
-			},
-		},
 	}
 
 	for _, tc := range testCases {
@@ -162,7 +143,7 @@ func TestGetTargetsFromMHC(t *testing.T) {
 				t.patchHelper = patchHelper
 			}
 
-			targets, err := reconciler.getTargetsFromMHC(ctx, ctrl.LoggerFrom(ctx), k8sClient, testMHC)
+			targets, err := reconciler.getTargetsFromMHC(ctx, k8sClient, testMHC)
 			gs.Expect(err).ToNot(HaveOccurred())
 
 			gs.Expect(len(targets)).To(Equal(len(tc.expectedTargets)))
diff --git a/controllers/schema_test.go b/controllers/schema_test.go
deleted file mode 100644
index 9f121b48c..000000000
--- a/controllers/schema_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controllers
-
-import (
-	"testing"
-
-	. "github.com/onsi/gomega"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/utils/pointer"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-func TestMachineSetScheme(t *testing.T) {
-	g := NewWithT(t)
-	ns, err := testEnv.CreateNamespace(ctx, "schema-test")
-	g.Expect(err).ToNot(HaveOccurred())
-
-	testMachineSet := &clusterv1.MachineSet{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: ns.Name,
-			Name:      "test-machineset",
-		},
-		Spec: clusterv1.MachineSetSpec{
-			ClusterName: "test",
-			Template: clusterv1.MachineTemplateSpec{
-				Spec: clusterv1.MachineSpec{
-					ClusterName: "test",
-				},
-			},
-		},
-	}
-
-	g.Expect(testEnv.Create(ctx, testMachineSet)).To(Succeed())
-
-	defer func(do ...client.Object) {
-		g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed())
-	}(ns, testMachineSet)
-
-	g.Expect(testMachineSet.Spec.Replicas).To(Equal(pointer.Int32Ptr(1)))
-}
-
-func TestMachineDeploymentScheme(t *testing.T) {
-	g := NewWithT(t)
-	ns, err := testEnv.CreateNamespace(ctx, "schema-test")
-	g.Expect(err).ToNot(HaveOccurred())
-
-	testMachineDeployment := &clusterv1.MachineDeployment{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: ns.Name,
-			Name:      "test-machinedeployment",
-		},
-		Spec: clusterv1.MachineDeploymentSpec{
-			ClusterName: "test",
-			Template: clusterv1.MachineTemplateSpec{
-				Spec: clusterv1.MachineSpec{
-					ClusterName: "test",
-				},
-			},
-		},
-	}
-
-	g.Expect(testEnv.Create(ctx, testMachineDeployment)).To(Succeed())
-
-	defer func(do ...client.Object) {
-		g.Expect(testEnv.Cleanup(ctx, do...)).To(Succeed())
-	}(ns, testMachineDeployment)
-
-	g.Expect(testMachineDeployment.Spec.Replicas).To(Equal(pointer.Int32Ptr(1)))
-}
diff --git a/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook_test.go b/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook_test.go
index 0c349ab62..d8238b772 100644
--- a/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook_test.go
+++ b/controlplane/kubeadm/api/v1alpha4/kubeadm_control_plane_webhook_test.go
@@ -133,7 +133,7 @@ func TestKubeadmControlPlaneValidateCreate(t *testing.T) {
 			kcp:       validVersion,
 		},
 		{
-			name:      "should error when given a valid semantic version without 'v'",
+			name:      "should succeed when given a valid semantic version without 'v'",
 			expectErr: true,
 			kcp:       invalidVersion2,
 		},
@@ -457,11 +457,6 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) {
 	withoutClusterConfiguration := before.DeepCopy()
 	withoutClusterConfiguration.Spec.KubeadmConfigSpec.ClusterConfiguration = nil
 
-	afterEtcdLocalDirAddition := before.DeepCopy()
-	afterEtcdLocalDirAddition.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &kubeadmv1beta1.LocalEtcd{
-		DataDir: "/data",
-	}
-
 	disallowedUpgrade118Prev := prevKCPWithVersion("v1.18.8")
 	disallowedUpgrade119Version := before.DeepCopy()
 	disallowedUpgrade119Version.Spec.Version = "v1.19.0"
@@ -706,12 +701,6 @@ func TestKubeadmControlPlaneValidateUpdate(t *testing.T) {
 			before:    withoutClusterConfiguration,
 			kcp:       withoutClusterConfiguration,
 		},
-		{
-			name:      "should fail if etcd local dir is changed from missing ClusterConfiguration",
-			expectErr: true,
-			before:    withoutClusterConfiguration,
-			kcp:       afterEtcdLocalDirAddition,
-		},
 		{
 			name:      "should fail when skipping control plane minor versions",
 			expectErr: true,
diff --git a/controlplane/kubeadm/controllers/controller_test.go b/controlplane/kubeadm/controllers/controller_test.go
index 2f20ace55..673f780c4 100644
--- a/controlplane/kubeadm/controllers/controller_test.go
+++ b/controlplane/kubeadm/controllers/controller_test.go
@@ -19,7 +19,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"sync"
 	"testing"
 	"time"
@@ -361,7 +360,7 @@ func TestKubeadmControlPlaneReconciler_adoption(t *testing.T) {
 		kcp.Spec.Version = version
 
 		fmc := &fakeManagementCluster{
-			Machines: collections.Machines{},
+			Machines: internal.FilterableMachineCollection{},
 			Workload: fakeWorkloadCluster{},
 		}
 		objs := []client.Object{cluster.DeepCopy(), kcp.DeepCopy(), tmpl.DeepCopy()}
@@ -425,7 +424,7 @@ func TestKubeadmControlPlaneReconciler_adoption(t *testing.T) {
 		kcp.Spec.Version = version
 
 		fmc := &fakeManagementCluster{
-			Machines: collections.Machines{},
+			Machines: internal.FilterableMachineCollection{},
 			Workload: fakeWorkloadCluster{},
 		}
 		objs := []client.Object{cluster.DeepCopy(), kcp.DeepCopy(), tmpl.DeepCopy()}
@@ -535,7 +534,7 @@ func TestKubeadmControlPlaneReconciler_adoption(t *testing.T) {
 		kcp.DeletionTimestamp = &now
 
 		fmc := &fakeManagementCluster{
-			Machines: collections.Machines{},
+			Machines: internal.FilterableMachineCollection{},
 			Workload: fakeWorkloadCluster{},
 		}
 		objs := []client.Object{cluster.DeepCopy(), kcp.DeepCopy(), tmpl.DeepCopy()}
@@ -597,7 +596,7 @@ func TestKubeadmControlPlaneReconciler_adoption(t *testing.T) {
 		kcp.Spec.Version = "v1.17.0"
 
 		fmc := &fakeManagementCluster{
-			Machines: collections.Machines{
+			Machines: internal.FilterableMachineCollection{
 				"test0": &clusterv1.Machine{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace: cluster.Namespace,
diff --git a/controlplane/kubeadm/controllers/fakes_test.go b/controlplane/kubeadm/controllers/fakes_test.go
index 74592865f..945f50e97 100644
--- a/controlplane/kubeadm/controllers/fakes_test.go
+++ b/controlplane/kubeadm/controllers/fakes_test.go
@@ -18,17 +18,18 @@ package controllers
 
 import (
 	"context"
+
 	"github.com/blang/semver"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
-	"sigs.k8s.io/cluster-api/util/collections"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 type fakeManagementCluster struct {
 	// TODO: once all client interactions are moved to the Management cluster this can go away
 	Management *internal.Management
-	Machines   collections.Machines
+	Machines   internal.FilterableMachineCollection
 	Workload   fakeWorkloadCluster
 	Reader     client.Reader
 }
@@ -45,7 +46,7 @@ func (f *fakeManagementCluster) GetWorkloadCluster(_ context.Context, _ client.O
 	return f.Workload, nil
 }
 
-func (f *fakeManagementCluster) GetMachinesForCluster(c context.Context, n client.ObjectKey, filters ...collections.Func) (collections.Machines, error) {
+func (f *fakeManagementCluster) GetMachinesForCluster(c context.Context, n client.ObjectKey, filters ...machinefilters.Func) (internal.FilterableMachineCollection, error) {
 	if f.Management != nil {
 		return f.Management.GetMachinesForCluster(c, n, filters...)
 	}
diff --git a/controlplane/kubeadm/controllers/remediation_test.go b/controlplane/kubeadm/controllers/remediation_test.go
index 3e0fbdfda..5cc1e3841 100644
--- a/controlplane/kubeadm/controllers/remediation_test.go
+++ b/controlplane/kubeadm/controllers/remediation_test.go
@@ -18,7 +18,6 @@ package controllers
 
 import (
 	"context"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -51,7 +50,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 		controlPlane := &internal.ControlPlane{
 			KCP:      &controlplanev1.KubeadmControlPlane{},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.New(),
+			Machines: internal.NewFilterableMachineCollection(),
 		}
 		ret, err := r.reconcileUnhealthyMachines(context.TODO(), controlPlane)
 
@@ -67,7 +66,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 		controlPlane := &internal.ControlPlane{
 			KCP:      &controlplanev1.KubeadmControlPlane{},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m),
+			Machines: internal.NewFilterableMachineCollection(m),
 		}
 		ret, err := r.reconcileUnhealthyMachines(context.TODO(), controlPlane)
 
@@ -83,7 +82,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(1),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m),
+			Machines: internal.NewFilterableMachineCollection(m),
 		}
 		ret, err := r.reconcileUnhealthyMachines(context.TODO(), controlPlane)
 
@@ -102,7 +101,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(3),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m),
+			Machines: internal.NewFilterableMachineCollection(m),
 		}
 		ret, err := r.reconcileUnhealthyMachines(context.TODO(), controlPlane)
 
@@ -123,7 +122,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(3),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3),
 		}
 		ret, err := r.reconcileUnhealthyMachines(context.TODO(), controlPlane)
 
@@ -145,7 +144,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(3),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -180,7 +179,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3, m4, m5),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -218,7 +217,7 @@ func TestReconcileUnhealthyMachines(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(3),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -270,7 +269,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(1),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1),
+			Machines: internal.NewFilterableMachineCollection(m1),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -300,7 +299,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(2),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2),
+			Machines: internal.NewFilterableMachineCollection(m1, m2),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -331,7 +330,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -362,7 +361,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -395,7 +394,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3, m4, m5),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -428,7 +427,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3, m4, m5),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -463,7 +462,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3, m4, m5, m6, m7),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5, m6, m7),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
@@ -498,7 +497,7 @@ func TestCanSafelyRemoveEtcdMember(t *testing.T) {
 				Replicas: utilpointer.Int32Ptr(5),
 			}},
 			Cluster:  &clusterv1.Cluster{},
-			Machines: collections.FromMachines(m1, m2, m3, m4, m5, m6, m7),
+			Machines: internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5, m6, m7),
 		}
 
 		r := &KubeadmControlPlaneReconciler{
diff --git a/controlplane/kubeadm/controllers/scale_test.go b/controlplane/kubeadm/controllers/scale_test.go
index 394cb952d..20bbe6a9b 100644
--- a/controlplane/kubeadm/controllers/scale_test.go
+++ b/controlplane/kubeadm/controllers/scale_test.go
@@ -19,7 +19,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 	"time"
 
@@ -88,7 +87,7 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 		initObjs := []client.Object{cluster.DeepCopy(), kcp.DeepCopy(), genericMachineTemplate.DeepCopy()}
 
 		fmc := &fakeManagementCluster{
-			Machines: collections.New(),
+			Machines: internal.NewFilterableMachineCollection(),
 			Workload: fakeWorkloadCluster{},
 		}
 
@@ -127,7 +126,7 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 		cluster.Spec.ControlPlaneEndpoint.Host = "nodomain.example.com"
 		cluster.Spec.ControlPlaneEndpoint.Port = 6443
 
-		beforeMachines := collections.New()
+		beforeMachines := internal.NewFilterableMachineCollection()
 		for i := 0; i < 2; i++ {
 			m, _ := createMachineNodePair(fmt.Sprintf("test-%d", i), cluster.DeepCopy(), kcp.DeepCopy(), true)
 			beforeMachines.Insert(m)
@@ -158,7 +157,7 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 		g.Expect(fakeClient.List(context.Background(), controlPlaneMachines)).To(Succeed())
 		g.Expect(controlPlaneMachines.Items).To(HaveLen(len(beforeMachines)))
 
-		endMachines := collections.FromMachineList(controlPlaneMachines)
+		endMachines := internal.NewFilterableMachineCollectionFromMachineList(controlPlaneMachines)
 		for _, m := range endMachines {
 			bm, ok := beforeMachines[m.Name]
 			bm.SetResourceVersion("1")
@@ -291,8 +290,8 @@ func TestSelectMachineForScaleDown(t *testing.T) {
 	m7 := machine("machine-7", withFailureDomain("two"), withTimestamp(startDate.Add(-5*time.Hour)), withAnnotation("cluster.x-k8s.io/delete-machine"))
 	m8 := machine("machine-8", withFailureDomain("two"), withTimestamp(startDate.Add(-6*time.Hour)), withAnnotation("cluster.x-k8s.io/delete-machine"))
 
-	mc3 := collections.FromMachines(m1, m2, m3, m4, m5)
-	mc6 := collections.FromMachines(m6, m7, m8)
+	mc3 := internal.NewFilterableMachineCollection(m1, m2, m3, m4, m5)
+	mc6 := internal.NewFilterableMachineCollection(m6, m7, m8)
 	fd := clusterv1.FailureDomains{
 		"one": failureDomain(true),
 		"two": failureDomain(true),
@@ -319,56 +318,56 @@ func TestSelectMachineForScaleDown(t *testing.T) {
 	testCases := []struct {
 		name             string
 		cp               *internal.ControlPlane
-		outDatedMachines collections.Machines
+		outDatedMachines internal.FilterableMachineCollection
 		expectErr        bool
 		expectedMachine  clusterv1.Machine
 	}{
 		{
 			name:             "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade",
 			cp:               needsUpgradeControlPlane,
-			outDatedMachines: collections.FromMachines(m5),
+			outDatedMachines: internal.NewFilterableMachineCollection(m5),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-5"}},
 		},
 		{
 			name:             "when there are no outdated machines, it returns the oldest machine in the largest failure domain",
 			cp:               upToDateControlPlane,
-			outDatedMachines: collections.New(),
+			outDatedMachines: internal.NewFilterableMachineCollection(),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}},
 		},
 		{
 			name:             "when there is a single machine marked with delete annotation key in machine collection, it returns only that marked machine",
 			cp:               annotatedControlPlane,
-			outDatedMachines: collections.FromMachines(m6, m7),
+			outDatedMachines: internal.NewFilterableMachineCollection(m6, m7),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}},
 		},
 		{
 			name:             "when there are machines marked with delete annotation key in machine collection, it returns the oldest marked machine first",
 			cp:               annotatedControlPlane,
-			outDatedMachines: collections.FromMachines(m7, m8),
+			outDatedMachines: internal.NewFilterableMachineCollection(m7, m8),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
 		},
 		{
 			name:             "when there are annotated machines which are part of the annotatedControlPlane but not in outdatedMachines, it returns the oldest marked machine first",
 			cp:               annotatedControlPlane,
-			outDatedMachines: collections.New(),
+			outDatedMachines: internal.NewFilterableMachineCollection(),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
 		},
 		{
 			name:             "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade",
 			cp:               needsUpgradeControlPlane,
-			outDatedMachines: collections.FromMachines(m7, m3),
+			outDatedMachines: internal.NewFilterableMachineCollection(m7, m3),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}},
 		},
 		{
 			name:             "when there is an up to date machine with delete annotation, while there are any outdated machines without annotatio that still exist, it returns oldest marked machine first",
 			cp:               upToDateControlPlane,
-			outDatedMachines: collections.FromMachines(m5, m3, m8, m7, m6, m1, m2),
+			outDatedMachines: internal.NewFilterableMachineCollection(m5, m3, m8, m7, m6, m1, m2),
 			expectErr:        false,
 			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
 		},
@@ -472,7 +471,7 @@ func TestPreflightChecks(t *testing.T) {
 			controlPlane := &internal.ControlPlane{
 				Cluster:  &clusterv1.Cluster{},
 				KCP:      tt.kcp,
-				Machines: collections.FromMachines(tt.machines...),
+				Machines: internal.NewFilterableMachineCollection(tt.machines...),
 			}
 			result, err := r.preflightChecks(context.TODO(), controlPlane)
 			g.Expect(err).NotTo(HaveOccurred())
diff --git a/controlplane/kubeadm/controllers/upgrade_test.go b/controlplane/kubeadm/controllers/upgrade_test.go
index b073e3529..c0310d675 100644
--- a/controlplane/kubeadm/controllers/upgrade_test.go
+++ b/controlplane/kubeadm/controllers/upgrade_test.go
@@ -18,7 +18,6 @@ package controllers
 
 import (
 	"context"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -83,7 +82,7 @@ func TestKubeadmControlPlaneReconciler_upgradeControlPlane(t *testing.T) {
 	kcp.Spec.Version = "v1.17.4"
 
 	// run upgrade the first time, expect we scale up
-	needingUpgrade := collections.FromMachineList(initialMachine)
+	needingUpgrade := internal.NewFilterableMachineCollectionFromMachineList(initialMachine)
 	controlPlane.Machines = needingUpgrade
 	result, err = r.upgradeControlPlane(ctx, cluster, kcp, controlPlane, needingUpgrade)
 	g.Expect(result).To(Equal(ctrl.Result{Requeue: true}))
@@ -106,7 +105,7 @@ func TestKubeadmControlPlaneReconciler_upgradeControlPlane(t *testing.T) {
 	for i := range bothMachines.Items {
 		setMachineHealthy(&bothMachines.Items[i])
 	}
-	controlPlane.Machines = collections.FromMachineList(bothMachines)
+	controlPlane.Machines = internal.NewFilterableMachineCollectionFromMachineList(bothMachines)
 
 	// run upgrade the second time, expect we scale down
 	result, err = r.upgradeControlPlane(ctx, cluster, kcp, controlPlane, controlPlane.Machines)
diff --git a/controlplane/kubeadm/internal/cluster_test.go b/controlplane/kubeadm/internal/cluster_test.go
index 16ef5b0da..86c7afaf8 100644
--- a/controlplane/kubeadm/internal/cluster_test.go
+++ b/controlplane/kubeadm/internal/cluster_test.go
@@ -24,7 +24,6 @@ import (
 	"crypto/x509/pkix"
 	"fmt"
 	"math/big"
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 	"time"
 
@@ -39,6 +38,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
 	"sigs.k8s.io/cluster-api/controllers/remote"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	"sigs.k8s.io/cluster-api/util/certs"
 	"sigs.k8s.io/cluster-api/util/kubeconfig"
 	"sigs.k8s.io/cluster-api/util/secret"
@@ -60,7 +60,7 @@ func TestGetMachinesForCluster(t *testing.T) {
 	g.Expect(machines).To(HaveLen(3))
 
 	// Test the ControlPlaneMachines works
-	machines, err = m.GetMachinesForCluster(ctx, clusterKey, collections.ControlPlaneMachines("my-cluster"))
+	machines, err = m.GetMachinesForCluster(ctx, clusterKey, machinefilters.ControlPlaneMachines("my-cluster"))
 	g.Expect(err).NotTo(HaveOccurred())
 	g.Expect(machines).To(HaveLen(1))
 
@@ -68,7 +68,7 @@ func TestGetMachinesForCluster(t *testing.T) {
 	nameFilter := func(cluster *clusterv1.Machine) bool {
 		return cluster.Name == "first-machine"
 	}
-	machines, err = m.GetMachinesForCluster(ctx, clusterKey, collections.ControlPlaneMachines("my-cluster"), nameFilter)
+	machines, err = m.GetMachinesForCluster(ctx, clusterKey, machinefilters.ControlPlaneMachines("my-cluster"), nameFilter)
 	g.Expect(err).NotTo(HaveOccurred())
 	g.Expect(machines).To(HaveLen(1))
 }
diff --git a/controlplane/kubeadm/internal/control_plane_test.go b/controlplane/kubeadm/internal/control_plane_test.go
index 167f0d7f5..82c3512d4 100644
--- a/controlplane/kubeadm/internal/control_plane_test.go
+++ b/controlplane/kubeadm/internal/control_plane_test.go
@@ -17,7 +17,6 @@ limitations under the License.
 package internal
 
 import (
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -48,7 +47,7 @@ func TestControlPlane(t *testing.T) {
 					},
 				},
 			},
-			Machines: collections.Machines{
+			Machines: FilterableMachineCollection{
 				"machine-1": machine("machine-1", withFailureDomain("one")),
 				"machine-2": machine("machine-2", withFailureDomain("two")),
 				"machine-3": machine("machine-3", withFailureDomain("two")),
@@ -94,6 +93,18 @@ func TestControlPlane(t *testing.T) {
 	})
 }
 
+func failureDomain(controlPlane bool) clusterv1.FailureDomainSpec {
+	return clusterv1.FailureDomainSpec{
+		ControlPlane: controlPlane,
+	}
+}
+
+func withFailureDomain(fd string) machineOpt {
+	return func(m *clusterv1.Machine) {
+		m.Spec.FailureDomain = &fd
+	}
+}
+
 func TestHasUnhealthyMachine(t *testing.T) {
 	// healthy machine (without MachineHealthCheckSucceded condition)
 	healthyMachine1 := &clusterv1.Machine{}
@@ -109,7 +120,7 @@ func TestHasUnhealthyMachine(t *testing.T) {
 	conditions.MarkFalse(unhealthyMachineOwnerRemediated, clusterv1.MachineOwnerRemediatedCondition, clusterv1.WaitingForRemediationReason, clusterv1.ConditionSeverityWarning, "")
 
 	c := ControlPlane{
-		Machines: collections.FromMachines(
+		Machines: NewFilterableMachineCollection(
 			healthyMachine1,
 			healthyMachine2,
 			unhealthyMachineNOTOwnerRemediated,
@@ -120,29 +131,3 @@ func TestHasUnhealthyMachine(t *testing.T) {
 	g := NewWithT(t)
 	g.Expect(c.HasUnhealthyMachine()).To(BeTrue())
 }
-
-type machineOpt func(*clusterv1.Machine)
-
-func failureDomain(controlPlane bool) clusterv1.FailureDomainSpec {
-	return clusterv1.FailureDomainSpec{
-		ControlPlane: controlPlane,
-	}
-}
-
-func withFailureDomain(fd string) machineOpt {
-	return func(m *clusterv1.Machine) {
-		m.Spec.FailureDomain = &fd
-	}
-}
-
-func machine(name string, opts ...machineOpt) *clusterv1.Machine {
-	m := &clusterv1.Machine{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
-		},
-	}
-	for _, opt := range opts {
-		opt(m)
-	}
-	return m
-}
diff --git a/util/failuredomains/failure_domains_test.go b/controlplane/kubeadm/internal/failure_domain_test.go
similarity index 85%
rename from util/failuredomains/failure_domains_test.go
rename to controlplane/kubeadm/internal/failure_domain_test.go
index e0bce8cac..3fe4aba89 100644
--- a/util/failuredomains/failure_domains_test.go
+++ b/controlplane/kubeadm/internal/failure_domain_test.go
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package failuredomains
+package internal
 
 import (
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -42,7 +41,7 @@ func TestNewFailureDomainPicker(t *testing.T) {
 	testcases := []struct {
 		name     string
 		fds      clusterv1.FailureDomains
-		machines collections.Machines
+		machines FilterableMachineCollection
 		expected []*string
 	}{
 		{
@@ -59,7 +58,7 @@ func TestNewFailureDomainPicker(t *testing.T) {
 		{
 			name:     "one machine in a failure domain",
 			fds:      fds,
-			machines: collections.FromMachines(machinea.DeepCopy()),
+			machines: NewFilterableMachineCollection(machinea.DeepCopy()),
 			expected: []*string{b},
 		},
 		{
@@ -67,7 +66,7 @@ func TestNewFailureDomainPicker(t *testing.T) {
 			fds: clusterv1.FailureDomains{
 				*a: clusterv1.FailureDomainSpec{},
 			},
-			machines: collections.FromMachines(machinenil.DeepCopy()),
+			machines: NewFilterableMachineCollection(machinenil.DeepCopy()),
 			expected: []*string{a},
 		},
 		{
@@ -75,7 +74,7 @@ func TestNewFailureDomainPicker(t *testing.T) {
 			fds: clusterv1.FailureDomains{
 				*a: clusterv1.FailureDomainSpec{},
 			},
-			machines: collections.FromMachines(machineb.DeepCopy()),
+			machines: NewFilterableMachineCollection(machineb.DeepCopy()),
 			expected: []*string{a},
 		},
 		{
@@ -113,7 +112,7 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 	testcases := []struct {
 		name     string
 		fds      clusterv1.FailureDomains
-		machines collections.Machines
+		machines FilterableMachineCollection
 		expected []*string
 	}{
 		{
@@ -130,7 +129,7 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 		{
 			name:     "one machine in a failure domain",
 			fds:      fds,
-			machines: collections.FromMachines(machinea.DeepCopy()),
+			machines: NewFilterableMachineCollection(machinea.DeepCopy()),
 			expected: []*string{a},
 		},
 		{
@@ -138,7 +137,7 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 			fds: clusterv1.FailureDomains{
 				*a: clusterv1.FailureDomainSpec{ControlPlane: true},
 			},
-			machines: collections.FromMachines(machinenil.DeepCopy()),
+			machines: NewFilterableMachineCollection(machinenil.DeepCopy()),
 			expected: nil,
 		},
 		{
@@ -146,7 +145,7 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 			fds: clusterv1.FailureDomains{
 				*a: clusterv1.FailureDomainSpec{ControlPlane: true},
 			},
-			machines: collections.FromMachines(machineb.DeepCopy()),
+			machines: NewFilterableMachineCollection(machineb.DeepCopy()),
 			expected: nil,
 		},
 		{
@@ -156,7 +155,7 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 		},
 		{
 			name:     "nil failure domains with machines",
-			machines: collections.FromMachines(machineb.DeepCopy()),
+			machines: NewFilterableMachineCollection(machineb.DeepCopy()),
 			expected: nil,
 		},
 		{
@@ -168,7 +167,8 @@ func TestNewFailureDomainPickMost(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			g := NewWithT(t)
 
-			fd := PickMost(tc.fds, tc.machines, tc.machines)
+			fd := PickMost(&ControlPlane{Machines: tc.machines,
+				Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: tc.fds}}}, tc.machines)
 			if tc.expected == nil {
 				g.Expect(fd).To(BeNil())
 			} else {
diff --git a/util/collections/machine_collection_test.go b/controlplane/kubeadm/internal/machine_collection_test.go
similarity index 90%
rename from util/collections/machine_collection_test.go
rename to controlplane/kubeadm/internal/machine_collection_test.go
index e996d1c27..375938e0d 100644
--- a/util/collections/machine_collection_test.go
+++ b/controlplane/kubeadm/internal/machine_collection_test.go
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package collections_test
+package internal
 
 import (
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 	"time"
 
@@ -33,10 +32,10 @@ func TestMachineCollection(t *testing.T) {
 }
 
 var _ = Describe("Machine Collection", func() {
-	Describe("Machines", func() {
-		var collection collections.Machines
+	Describe("FilterableMachineCollection", func() {
+		var collection FilterableMachineCollection
 		BeforeEach(func() {
-			collection = collections.Machines{
+			collection = FilterableMachineCollection{
 				"machine-4": machine("machine-4", withCreationTimestamp(metav1.Time{Time: time.Date(2018, 04, 02, 03, 04, 05, 06, time.UTC)})),
 				"machine-5": machine("machine-5", withCreationTimestamp(metav1.Time{Time: time.Date(2018, 05, 02, 03, 04, 05, 06, time.UTC)})),
 				"machine-2": machine("machine-2", withCreationTimestamp(metav1.Time{Time: time.Date(2018, 02, 02, 03, 04, 05, 06, time.UTC)})),
@@ -67,8 +66,8 @@ var _ = Describe("Machine Collection", func() {
 
 		Describe("Names", func() {
 			It("returns a slice of names of each machine in the collection", func() {
-				Expect(collections.New().Names()).To(BeEmpty())
-				Expect(collections.FromMachines(machine("1"), machine("2")).Names()).To(ConsistOf("1", "2"))
+				Expect(NewFilterableMachineCollection().Names()).To(BeEmpty())
+				Expect(NewFilterableMachineCollection(machine("1"), machine("2")).Names()).To(ConsistOf("1", "2"))
 			})
 		})
 	})
diff --git a/util/collections/machine_filters_test.go b/controlplane/kubeadm/internal/machinefilters/machine_filters_test.go
similarity index 56%
rename from util/collections/machine_filters_test.go
rename to controlplane/kubeadm/internal/machinefilters/machine_filters_test.go
index 623752d99..c3019629b 100644
--- a/util/collections/machine_filters_test.go
+++ b/controlplane/kubeadm/internal/machinefilters/machine_filters_test.go
@@ -14,19 +14,23 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package collections_test
+package machinefilters_test
 
 import (
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 	"time"
 
 	. "github.com/onsi/gomega"
 	"sigs.k8s.io/cluster-api/util/conditions"
 
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/utils/pointer"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
+	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha4"
+	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha4"
+	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 )
 
 func falseFilter(_ *clusterv1.Machine) bool {
@@ -41,12 +45,12 @@ func TestNot(t *testing.T) {
 	t.Run("returns false given a machine filter that returns true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.Not(trueFilter)(m)).To(BeFalse())
+		g.Expect(machinefilters.Not(trueFilter)(m)).To(BeFalse())
 	})
 	t.Run("returns true given a machine filter that returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.Not(falseFilter)(m)).To(BeTrue())
+		g.Expect(machinefilters.Not(falseFilter)(m)).To(BeTrue())
 	})
 }
 
@@ -54,12 +58,12 @@ func TestAnd(t *testing.T) {
 	t.Run("returns true if both given machine filters return true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.And(trueFilter, trueFilter)(m)).To(BeTrue())
+		g.Expect(machinefilters.And(trueFilter, trueFilter)(m)).To(BeTrue())
 	})
 	t.Run("returns false if either given machine filter returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.And(trueFilter, falseFilter)(m)).To(BeFalse())
+		g.Expect(machinefilters.And(trueFilter, falseFilter)(m)).To(BeFalse())
 	})
 }
 
@@ -67,12 +71,12 @@ func TestOr(t *testing.T) {
 	t.Run("returns true if either given machine filters return true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.Or(trueFilter, falseFilter)(m)).To(BeTrue())
+		g.Expect(machinefilters.Or(trueFilter, falseFilter)(m)).To(BeTrue())
 	})
 	t.Run("returns false if both given machine filter returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.Or(falseFilter, falseFilter)(m)).To(BeFalse())
+		g.Expect(machinefilters.Or(falseFilter, falseFilter)(m)).To(BeFalse())
 	})
 }
 
@@ -80,26 +84,26 @@ func TestHasUnhealthyCondition(t *testing.T) {
 	t.Run("healthy machine (without HealthCheckSucceeded condition) should return false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.HasUnhealthyCondition(m)).To(BeFalse())
+		g.Expect(machinefilters.HasUnhealthyCondition(m)).To(BeFalse())
 	})
 	t.Run("healthy machine (with HealthCheckSucceeded condition == True) should return false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		conditions.MarkTrue(m, clusterv1.MachineHealthCheckSuccededCondition)
-		g.Expect(collections.HasUnhealthyCondition(m)).To(BeFalse())
+		g.Expect(machinefilters.HasUnhealthyCondition(m)).To(BeFalse())
 	})
 	t.Run("unhealthy machine NOT eligible for KCP remediation (with withHealthCheckSucceeded condition == False but without OwnerRemediated) should return false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		conditions.MarkFalse(m, clusterv1.MachineHealthCheckSuccededCondition, clusterv1.MachineHasFailureReason, clusterv1.ConditionSeverityWarning, "")
-		g.Expect(collections.HasUnhealthyCondition(m)).To(BeFalse())
+		g.Expect(machinefilters.HasUnhealthyCondition(m)).To(BeFalse())
 	})
 	t.Run("unhealthy machine eligible for KCP (with HealthCheckSucceeded condition == False and with OwnerRemediated) should return true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		conditions.MarkFalse(m, clusterv1.MachineHealthCheckSuccededCondition, clusterv1.MachineHasFailureReason, clusterv1.ConditionSeverityWarning, "")
 		conditions.MarkFalse(m, clusterv1.MachineOwnerRemediatedCondition, clusterv1.WaitingForRemediationReason, clusterv1.ConditionSeverityWarning, "")
-		g.Expect(collections.HasUnhealthyCondition(m)).To(BeTrue())
+		g.Expect(machinefilters.HasUnhealthyCondition(m)).To(BeTrue())
 	})
 }
 
@@ -109,19 +113,19 @@ func TestHasDeletionTimestamp(t *testing.T) {
 		m := &clusterv1.Machine{}
 		now := metav1.Now()
 		m.SetDeletionTimestamp(&now)
-		g.Expect(collections.HasDeletionTimestamp(m)).To(BeTrue())
+		g.Expect(machinefilters.HasDeletionTimestamp(m)).To(BeTrue())
 	})
 	t.Run("machine with nil deletion timestamp returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.HasDeletionTimestamp(m)).To(BeFalse())
+		g.Expect(machinefilters.HasDeletionTimestamp(m)).To(BeFalse())
 	})
 	t.Run("machine with zero deletion timestamp returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		zero := metav1.NewTime(time.Time{})
 		m.SetDeletionTimestamp(&zero)
-		g.Expect(collections.HasDeletionTimestamp(m)).To(BeFalse())
+		g.Expect(machinefilters.HasDeletionTimestamp(m)).To(BeFalse())
 	})
 }
 
@@ -129,37 +133,37 @@ func TestShouldRolloutAfter(t *testing.T) {
 	reconciliationTime := metav1.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
 	t.Run("if the machine is nil it returns false", func(t *testing.T) {
 		g := NewWithT(t)
-		g.Expect(collections.ShouldRolloutAfter(&reconciliationTime, &reconciliationTime)(nil)).To(BeFalse())
+		g.Expect(machinefilters.ShouldRolloutAfter(&reconciliationTime, &reconciliationTime)(nil)).To(BeFalse())
 	})
 	t.Run("if the reconciliationTime is nil it returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.ShouldRolloutAfter(nil, &reconciliationTime)(m)).To(BeFalse())
+		g.Expect(machinefilters.ShouldRolloutAfter(nil, &reconciliationTime)(m)).To(BeFalse())
 	})
 	t.Run("if the rolloutAfter is nil it returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.ShouldRolloutAfter(&reconciliationTime, nil)(m)).To(BeFalse())
+		g.Expect(machinefilters.ShouldRolloutAfter(&reconciliationTime, nil)(m)).To(BeFalse())
 	})
 	t.Run("if rolloutAfter is after the reconciliation time, return false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		rolloutAfter := metav1.NewTime(reconciliationTime.Add(+1 * time.Hour))
-		g.Expect(collections.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeFalse())
+		g.Expect(machinefilters.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeFalse())
 	})
 	t.Run("if rolloutAfter is before the reconciliation time and the machine was created before rolloutAfter, return true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		m.SetCreationTimestamp(metav1.NewTime(reconciliationTime.Add(-2 * time.Hour)))
 		rolloutAfter := metav1.NewTime(reconciliationTime.Add(-1 * time.Hour))
-		g.Expect(collections.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeTrue())
+		g.Expect(machinefilters.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeTrue())
 	})
 	t.Run("if rolloutAfter is before the reconciliation time and the machine was created after rolloutAfter, return false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		m.SetCreationTimestamp(metav1.NewTime(reconciliationTime.Add(+1 * time.Hour)))
 		rolloutAfter := metav1.NewTime(reconciliationTime.Add(-1 * time.Hour))
-		g.Expect(collections.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeFalse())
+		g.Expect(machinefilters.ShouldRolloutAfter(&reconciliationTime, &rolloutAfter)(m)).To(BeFalse())
 	})
 }
 
@@ -168,35 +172,35 @@ func TestHashAnnotationKey(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		m.SetAnnotations(map[string]string{"test": ""})
-		g.Expect(collections.HasAnnotationKey("test")(m)).To(BeTrue())
+		g.Expect(machinefilters.HasAnnotationKey("test")(m)).To(BeTrue())
 	})
 	t.Run("machine with specified annotation with non-empty value returns true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
 		m.SetAnnotations(map[string]string{"test": "blue"})
-		g.Expect(collections.HasAnnotationKey("test")(m)).To(BeTrue())
+		g.Expect(machinefilters.HasAnnotationKey("test")(m)).To(BeTrue())
 	})
 	t.Run("machine without specified annotation returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.HasAnnotationKey("foo")(m)).To(BeFalse())
+		g.Expect(machinefilters.HasAnnotationKey("foo")(m)).To(BeFalse())
 	})
 }
 
 func TestInFailureDomain(t *testing.T) {
 	t.Run("nil machine returns false", func(t *testing.T) {
 		g := NewWithT(t)
-		g.Expect(collections.InFailureDomains(pointer.StringPtr("test"))(nil)).To(BeFalse())
+		g.Expect(machinefilters.InFailureDomains(pointer.StringPtr("test"))(nil)).To(BeFalse())
 	})
 	t.Run("machine with given failure domain returns true", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{Spec: clusterv1.MachineSpec{FailureDomain: pointer.StringPtr("test")}}
-		g.Expect(collections.InFailureDomains(pointer.StringPtr("test"))(m)).To(BeTrue())
+		g.Expect(machinefilters.InFailureDomains(pointer.StringPtr("test"))(m)).To(BeTrue())
 	})
 	t.Run("machine with a different failure domain returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{Spec: clusterv1.MachineSpec{FailureDomain: pointer.StringPtr("notTest")}}
-		g.Expect(collections.InFailureDomains(
+		g.Expect(machinefilters.InFailureDomains(
 			pointer.StringPtr("test"),
 			pointer.StringPtr("test2"),
 			pointer.StringPtr("test3"),
@@ -206,24 +210,24 @@ func TestInFailureDomain(t *testing.T) {
 	t.Run("machine without failure domain returns false", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.InFailureDomains(pointer.StringPtr("test"))(m)).To(BeFalse())
+		g.Expect(machinefilters.InFailureDomains(pointer.StringPtr("test"))(m)).To(BeFalse())
 	})
 	t.Run("machine without failure domain returns true, when nil used for failure domain", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{}
-		g.Expect(collections.InFailureDomains(nil)(m)).To(BeTrue())
+		g.Expect(machinefilters.InFailureDomains(nil)(m)).To(BeTrue())
 	})
 	t.Run("machine with failure domain returns true, when one of multiple failure domains match", func(t *testing.T) {
 		g := NewWithT(t)
 		m := &clusterv1.Machine{Spec: clusterv1.MachineSpec{FailureDomain: pointer.StringPtr("test")}}
-		g.Expect(collections.InFailureDomains(pointer.StringPtr("foo"), pointer.StringPtr("test"))(m)).To(BeTrue())
+		g.Expect(machinefilters.InFailureDomains(pointer.StringPtr("foo"), pointer.StringPtr("test"))(m)).To(BeTrue())
 	})
 }
 
 func TestMatchesKubernetesVersion(t *testing.T) {
 	t.Run("nil machine returns false", func(t *testing.T) {
 		g := NewWithT(t)
-		g.Expect(collections.MatchesKubernetesVersion("some_ver")(nil)).To(BeFalse())
+		g.Expect(machinefilters.MatchesKubernetesVersion("some_ver")(nil)).To(BeFalse())
 	})
 
 	t.Run("nil machine.Spec.Version returns false", func(t *testing.T) {
@@ -233,7 +237,7 @@ func TestMatchesKubernetesVersion(t *testing.T) {
 				Version: nil,
 			},
 		}
-		g.Expect(collections.MatchesKubernetesVersion("some_ver")(machine)).To(BeFalse())
+		g.Expect(machinefilters.MatchesKubernetesVersion("some_ver")(machine)).To(BeFalse())
 	})
 
 	t.Run("machine.Spec.Version returns true if matches", func(t *testing.T) {
@@ -244,7 +248,7 @@ func TestMatchesKubernetesVersion(t *testing.T) {
 				Version: &kversion,
 			},
 		}
-		g.Expect(collections.MatchesKubernetesVersion("some_ver")(machine)).To(BeTrue())
+		g.Expect(machinefilters.MatchesKubernetesVersion("some_ver")(machine)).To(BeTrue())
 	})
 
 	t.Run("machine.Spec.Version returns false if does not match", func(t *testing.T) {
@@ -255,6 +259,116 @@ func TestMatchesKubernetesVersion(t *testing.T) {
 				Version: &kversion,
 			},
 		}
-		g.Expect(collections.MatchesKubernetesVersion("some_ver")(machine)).To(BeFalse())
+		g.Expect(machinefilters.MatchesKubernetesVersion("some_ver")(machine)).To(BeFalse())
 	})
 }
+
+func TestMatchesTemplateClonedFrom(t *testing.T) {
+	t.Run("nil machine returns false", func(t *testing.T) {
+		g := NewWithT(t)
+		g.Expect(
+			machinefilters.MatchesTemplateClonedFrom(nil, nil)(nil),
+		).To(BeFalse())
+	})
+
+	t.Run("returns true if machine not found", func(t *testing.T) {
+		g := NewWithT(t)
+		kcp := &controlplanev1.KubeadmControlPlane{}
+		machine := &clusterv1.Machine{
+			Spec: clusterv1.MachineSpec{
+				InfrastructureRef: corev1.ObjectReference{
+					Kind:       "KubeadmConfig",
+					Namespace:  "default",
+					Name:       "test",
+					APIVersion: bootstrapv1.GroupVersion.String(),
+				},
+			},
+		}
+		g.Expect(
+			machinefilters.MatchesTemplateClonedFrom(map[string]*unstructured.Unstructured{}, kcp)(machine),
+		).To(BeTrue())
+	})
+}
+
+func TestMatchesTemplateClonedFrom_WithClonedFromAnnotations(t *testing.T) {
+	kcp := &controlplanev1.KubeadmControlPlane{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+		},
+		Spec: controlplanev1.KubeadmControlPlaneSpec{
+			InfrastructureTemplate: corev1.ObjectReference{
+				Kind:       "GenericMachineTemplate",
+				Namespace:  "default",
+				Name:       "infra-foo",
+				APIVersion: "generic.io/v1",
+			},
+		},
+	}
+	machine := &clusterv1.Machine{
+		Spec: clusterv1.MachineSpec{
+			InfrastructureRef: corev1.ObjectReference{
+				APIVersion: "infrastructure.cluster.x-k8s.io/v1alpha4",
+				Kind:       "InfrastructureMachine",
+				Name:       "infra-config1",
+				Namespace:  "default",
+			},
+		},
+	}
+	tests := []struct {
+		name        string
+		annotations map[string]interface{}
+		expectMatch bool
+	}{
+		{
+			name:        "returns true if annotations don't exist",
+			annotations: map[string]interface{}{},
+			expectMatch: true,
+		},
+		{
+			name: "returns false if annotations don't match anything",
+			annotations: map[string]interface{}{
+				clusterv1.TemplateClonedFromNameAnnotation:      "barfoo1",
+				clusterv1.TemplateClonedFromGroupKindAnnotation: "barfoo2",
+			},
+			expectMatch: false,
+		},
+		{
+			name: "returns false if TemplateClonedFromNameAnnotation matches but TemplateClonedFromGroupKindAnnotation doesn't",
+			annotations: map[string]interface{}{
+				clusterv1.TemplateClonedFromNameAnnotation:      "infra-foo",
+				clusterv1.TemplateClonedFromGroupKindAnnotation: "barfoo2",
+			},
+			expectMatch: false,
+		},
+		{
+			name: "returns true if both annotations match",
+			annotations: map[string]interface{}{
+				clusterv1.TemplateClonedFromNameAnnotation:      "infra-foo",
+				clusterv1.TemplateClonedFromGroupKindAnnotation: "GenericMachineTemplate.generic.io",
+			},
+			expectMatch: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+			infraConfigs := map[string]*unstructured.Unstructured{
+				machine.Name: {
+					Object: map[string]interface{}{
+						"kind":       "InfrastructureMachine",
+						"apiVersion": "infrastructure.cluster.x-k8s.io/v1alpha4",
+						"metadata": map[string]interface{}{
+							"name":        "infra-config1",
+							"namespace":   "default",
+							"annotations": tt.annotations,
+						},
+					},
+				},
+			}
+			g.Expect(
+				machinefilters.MatchesTemplateClonedFrom(infraConfigs, kcp)(machine),
+			).To(Equal(tt.expectMatch))
+		})
+	}
+}
diff --git a/controlplane/kubeadm/internal/filters_test.go b/controlplane/kubeadm/internal/machinefilters/util_test.go
similarity index 81%
rename from controlplane/kubeadm/internal/filters_test.go
rename to controlplane/kubeadm/internal/machinefilters/util_test.go
index 1d5ebd7d2..a5f63c8a8 100644
--- a/controlplane/kubeadm/internal/filters_test.go
+++ b/controlplane/kubeadm/internal/machinefilters/util_test.go
@@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package internal
+package machinefilters
 
 import (
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"testing"
 
-	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
@@ -31,13 +30,13 @@ import (
 
 func TestMatchClusterConfiguration(t *testing.T) {
 	t.Run("machine without the ClusterConfiguration annotation should match (not enough information to make a decision)", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{}
 		m := &clusterv1.Machine{}
-		g.Expect(matchClusterConfiguration(kcp, m)).To(BeTrue())
+		g.Expect(matchClusterConfiguration(kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("machine without an invalid ClusterConfiguration annotation should not match (only solution is to rollout)", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{}
 		m := &clusterv1.Machine{
 			ObjectMeta: metav1.ObjectMeta{
@@ -46,10 +45,10 @@ func TestMatchClusterConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchClusterConfiguration(kcp, m)).To(BeFalse())
+		g.Expect(matchClusterConfiguration(kcp, m)).To(gomega.BeFalse())
 	})
 	t.Run("Return true if cluster configuration matches", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -66,10 +65,10 @@ func TestMatchClusterConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchClusterConfiguration(kcp, m)).To(BeTrue())
+		g.Expect(matchClusterConfiguration(kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("Return false if cluster configuration does not match", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -86,10 +85,10 @@ func TestMatchClusterConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchClusterConfiguration(kcp, m)).To(BeFalse())
+		g.Expect(matchClusterConfiguration(kcp, m)).To(gomega.BeFalse())
 	})
 	t.Run("Return true if cluster configuration is nil (special case)", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{},
@@ -102,13 +101,13 @@ func TestMatchClusterConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchClusterConfiguration(kcp, m)).To(BeTrue())
+		g.Expect(matchClusterConfiguration(kcp, m)).To(gomega.BeTrue())
 	})
 }
 
 func TestGetAdjustedKcpConfig(t *testing.T) {
 	t.Run("if the machine is the first control plane, kcp config should get InitConfiguration", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -123,11 +122,11 @@ func TestGetAdjustedKcpConfig(t *testing.T) {
 			},
 		}
 		kcpConfig := getAdjustedKcpConfig(kcp, machineConfig)
-		g.Expect(kcpConfig.InitConfiguration).ToNot(BeNil())
-		g.Expect(kcpConfig.JoinConfiguration).To(BeNil())
+		g.Expect(kcpConfig.InitConfiguration).ToNot(gomega.BeNil())
+		g.Expect(kcpConfig.JoinConfiguration).To(gomega.BeNil())
 	})
 	t.Run("if the machine is a joining control plane, kcp config should get JoinConfiguration", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -142,14 +141,14 @@ func TestGetAdjustedKcpConfig(t *testing.T) {
 			},
 		}
 		kcpConfig := getAdjustedKcpConfig(kcp, machineConfig)
-		g.Expect(kcpConfig.InitConfiguration).To(BeNil())
-		g.Expect(kcpConfig.JoinConfiguration).ToNot(BeNil())
+		g.Expect(kcpConfig.InitConfiguration).To(gomega.BeNil())
+		g.Expect(kcpConfig.JoinConfiguration).ToNot(gomega.BeNil())
 	})
 }
 
 func TestCleanupConfigFields(t *testing.T) {
 	t.Run("ClusterConfiguration gets removed from KcpConfig and MachineConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			ClusterConfiguration: &kubeadmv1beta1.ClusterConfiguration{},
 		}
@@ -159,11 +158,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.ClusterConfiguration).To(BeNil())
-		g.Expect(machineConfig.Spec.ClusterConfiguration).To(BeNil())
+		g.Expect(kcpConfig.ClusterConfiguration).To(gomega.BeNil())
+		g.Expect(machineConfig.Spec.ClusterConfiguration).To(gomega.BeNil())
 	})
 	t.Run("JoinConfiguration gets removed from MachineConfig if it was not derived by KCPConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			JoinConfiguration: nil, // KCP not providing a JoinConfiguration
 		}
@@ -173,11 +172,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.JoinConfiguration).To(BeNil())
-		g.Expect(machineConfig.Spec.JoinConfiguration).To(BeNil())
+		g.Expect(kcpConfig.JoinConfiguration).To(gomega.BeNil())
+		g.Expect(machineConfig.Spec.JoinConfiguration).To(gomega.BeNil())
 	})
 	t.Run("JoinConfiguration.Discovery gets removed because it is not relevant for compare", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
 				Discovery: kubeadmv1beta1.Discovery{TLSBootstrapToken: "aaa"},
@@ -191,11 +190,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.JoinConfiguration.Discovery).To(Equal(kubeadmv1beta1.Discovery{}))
-		g.Expect(machineConfig.Spec.JoinConfiguration.Discovery).To(Equal(kubeadmv1beta1.Discovery{}))
+		g.Expect(kcpConfig.JoinConfiguration.Discovery).To(gomega.Equal(kubeadmv1beta1.Discovery{}))
+		g.Expect(machineConfig.Spec.JoinConfiguration.Discovery).To(gomega.Equal(kubeadmv1beta1.Discovery{}))
 	})
 	t.Run("JoinConfiguration.ControlPlane gets removed from MachineConfig if it was not derived by KCPConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
 				ControlPlane: nil, // Control plane configuration missing in KCP
@@ -209,11 +208,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.JoinConfiguration).ToNot(BeNil())
-		g.Expect(machineConfig.Spec.JoinConfiguration.ControlPlane).To(BeNil())
+		g.Expect(kcpConfig.JoinConfiguration).ToNot(gomega.BeNil())
+		g.Expect(machineConfig.Spec.JoinConfiguration.ControlPlane).To(gomega.BeNil())
 	})
 	t.Run("JoinConfiguration.NodeRegistrationOptions gets removed from MachineConfig if it was not derived by KCPConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{
 				NodeRegistration: kubeadmv1beta1.NodeRegistrationOptions{}, // NodeRegistrationOptions configuration missing in KCP
@@ -227,11 +226,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.JoinConfiguration).ToNot(BeNil())
-		g.Expect(machineConfig.Spec.JoinConfiguration.NodeRegistration).To(Equal(kubeadmv1beta1.NodeRegistrationOptions{}))
+		g.Expect(kcpConfig.JoinConfiguration).ToNot(gomega.BeNil())
+		g.Expect(machineConfig.Spec.JoinConfiguration.NodeRegistration).To(gomega.Equal(kubeadmv1beta1.NodeRegistrationOptions{}))
 	})
 	t.Run("InitConfiguration.TypeMeta gets removed from MachineConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			InitConfiguration: &kubeadmv1beta1.InitConfiguration{},
 		}
@@ -246,11 +245,11 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.InitConfiguration).ToNot(BeNil())
-		g.Expect(machineConfig.Spec.InitConfiguration.TypeMeta).To(Equal(metav1.TypeMeta{}))
+		g.Expect(kcpConfig.InitConfiguration).ToNot(gomega.BeNil())
+		g.Expect(machineConfig.Spec.InitConfiguration.TypeMeta).To(gomega.Equal(metav1.TypeMeta{}))
 	})
 	t.Run("JoinConfiguration.TypeMeta gets removed from MachineConfig", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcpConfig := &bootstrapv1.KubeadmConfigSpec{
 			JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{},
 		}
@@ -265,20 +264,20 @@ func TestCleanupConfigFields(t *testing.T) {
 			},
 		}
 		cleanupConfigFields(kcpConfig, machineConfig)
-		g.Expect(kcpConfig.JoinConfiguration).ToNot(BeNil())
-		g.Expect(machineConfig.Spec.JoinConfiguration.TypeMeta).To(Equal(metav1.TypeMeta{}))
+		g.Expect(kcpConfig.JoinConfiguration).ToNot(gomega.BeNil())
+		g.Expect(machineConfig.Spec.JoinConfiguration.TypeMeta).To(gomega.Equal(metav1.TypeMeta{}))
 	})
 }
 
 func TestMatchInitOrJoinConfiguration(t *testing.T) {
 	t.Run("returns true if the machine does not have a bootstrap config", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{}
 		m := &clusterv1.Machine{}
-		g.Expect(matchInitOrJoinConfiguration(nil, kcp, m)).To(BeTrue())
+		g.Expect(matchInitOrJoinConfiguration(nil, kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("returns true if the there are problems reading the bootstrap config", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{}
 		m := &clusterv1.Machine{
 			Spec: clusterv1.MachineSpec{
@@ -287,10 +286,10 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(nil, kcp, m)).To(BeTrue())
+		g.Expect(matchInitOrJoinConfiguration(nil, kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("returns true if InitConfiguration is equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -335,10 +334,10 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(BeTrue())
+		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("returns false if InitConfiguration is NOT equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -387,10 +386,10 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(BeFalse())
+		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(gomega.BeFalse())
 	})
 	t.Run("returns true if JoinConfiguration is equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -435,10 +434,10 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(BeTrue())
+		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(gomega.BeTrue())
 	})
 	t.Run("returns false if JoinConfiguration is NOT equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -487,10 +486,10 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(BeFalse())
+		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(gomega.BeFalse())
 	})
 	t.Run("returns false if some other configurations are not equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -536,13 +535,13 @@ func TestMatchInitOrJoinConfiguration(t *testing.T) {
 				},
 			},
 		}
-		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(BeFalse())
+		g.Expect(matchInitOrJoinConfiguration(machineConfigs, kcp, m)).To(gomega.BeFalse())
 	})
 }
 
 func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 	t.Run("returns true if ClusterConfiguration is equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -563,10 +562,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			m.Name: {},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeTrue())
+		g.Expect(f(m)).To(gomega.BeTrue())
 	})
 	t.Run("returns false if ClusterConfiguration is NOT equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -587,10 +586,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			m.Name: {},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeFalse())
+		g.Expect(f(m)).To(gomega.BeFalse())
 	})
 	t.Run("returns true if InitConfiguration is equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -636,10 +635,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeTrue())
+		g.Expect(f(m)).To(gomega.BeTrue())
 	})
 	t.Run("returns false if InitConfiguration is NOT equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -689,10 +688,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeFalse())
+		g.Expect(f(m)).To(gomega.BeFalse())
 	})
 	t.Run("returns true if JoinConfiguration is equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -738,10 +737,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeTrue())
+		g.Expect(f(m)).To(gomega.BeTrue())
 	})
 	t.Run("returns false if JoinConfiguration is NOT equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -791,10 +790,10 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeFalse())
+		g.Expect(f(m)).To(gomega.BeFalse())
 	})
 	t.Run("returns false if some other configurations are not equal", func(t *testing.T) {
-		g := NewWithT(t)
+		g := gomega.NewWithT(t)
 		kcp := &controlplanev1.KubeadmControlPlane{
 			Spec: controlplanev1.KubeadmControlPlaneSpec{
 				KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{
@@ -841,116 +840,6 @@ func TestMatchesKubeadmBootstrapConfig(t *testing.T) {
 			},
 		}
 		f := MatchesKubeadmBootstrapConfig(machineConfigs, kcp)
-		g.Expect(f(m)).To(BeFalse())
+		g.Expect(f(m)).To(gomega.BeFalse())
 	})
 }
-
-func TestMatchesTemplateClonedFrom(t *testing.T) {
-	t.Run("nil machine returns false", func(t *testing.T) {
-		g := NewWithT(t)
-		g.Expect(
-			MatchesTemplateClonedFrom(nil, nil)(nil),
-		).To(BeFalse())
-	})
-
-	t.Run("returns true if machine not found", func(t *testing.T) {
-		g := NewWithT(t)
-		kcp := &controlplanev1.KubeadmControlPlane{}
-		machine := &clusterv1.Machine{
-			Spec: clusterv1.MachineSpec{
-				InfrastructureRef: corev1.ObjectReference{
-					Kind:       "KubeadmConfig",
-					Namespace:  "default",
-					Name:       "test",
-					APIVersion: bootstrapv1.GroupVersion.String(),
-				},
-			},
-		}
-		g.Expect(
-			MatchesTemplateClonedFrom(map[string]*unstructured.Unstructured{}, kcp)(machine),
-		).To(BeTrue())
-	})
-}
-
-func TestMatchesTemplateClonedFrom_WithClonedFromAnnotations(t *testing.T) {
-	kcp := &controlplanev1.KubeadmControlPlane{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "default",
-		},
-		Spec: controlplanev1.KubeadmControlPlaneSpec{
-			InfrastructureTemplate: corev1.ObjectReference{
-				Kind:       "GenericMachineTemplate",
-				Namespace:  "default",
-				Name:       "infra-foo",
-				APIVersion: "generic.io/v1",
-			},
-		},
-	}
-	machine := &clusterv1.Machine{
-		Spec: clusterv1.MachineSpec{
-			InfrastructureRef: corev1.ObjectReference{
-				APIVersion: "infrastructure.cluster.x-k8s.io/v1alpha4",
-				Kind:       "InfrastructureMachine",
-				Name:       "infra-config1",
-				Namespace:  "default",
-			},
-		},
-	}
-	tests := []struct {
-		name        string
-		annotations map[string]interface{}
-		expectMatch bool
-	}{
-		{
-			name:        "returns true if annotations don't exist",
-			annotations: map[string]interface{}{},
-			expectMatch: true,
-		},
-		{
-			name: "returns false if annotations don't match anything",
-			annotations: map[string]interface{}{
-				clusterv1.TemplateClonedFromNameAnnotation:      "barfoo1",
-				clusterv1.TemplateClonedFromGroupKindAnnotation: "barfoo2",
-			},
-			expectMatch: false,
-		},
-		{
-			name: "returns false if TemplateClonedFromNameAnnotation matches but TemplateClonedFromGroupKindAnnotation doesn't",
-			annotations: map[string]interface{}{
-				clusterv1.TemplateClonedFromNameAnnotation:      "infra-foo",
-				clusterv1.TemplateClonedFromGroupKindAnnotation: "barfoo2",
-			},
-			expectMatch: false,
-		},
-		{
-			name: "returns true if both annotations match",
-			annotations: map[string]interface{}{
-				clusterv1.TemplateClonedFromNameAnnotation:      "infra-foo",
-				clusterv1.TemplateClonedFromGroupKindAnnotation: "GenericMachineTemplate.generic.io",
-			},
-			expectMatch: true,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-			infraConfigs := map[string]*unstructured.Unstructured{
-				machine.Name: {
-					Object: map[string]interface{}{
-						"kind":       "InfrastructureMachine",
-						"apiVersion": "infrastructure.cluster.x-k8s.io/v1alpha4",
-						"metadata": map[string]interface{}{
-							"name":        "infra-config1",
-							"namespace":   "default",
-							"annotations": tt.annotations,
-						},
-					},
-				},
-			}
-			g.Expect(
-				MatchesTemplateClonedFrom(infraConfigs, kcp)(machine),
-			).To(Equal(tt.expectMatch))
-		})
-	}
-}
diff --git a/controlplane/kubeadm/internal/workload_cluster_conditions_test.go b/controlplane/kubeadm/internal/workload_cluster_conditions_test.go
index ed0ba4edc..7384e33a3 100644
--- a/controlplane/kubeadm/internal/workload_cluster_conditions_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_conditions_test.go
@@ -17,7 +17,6 @@ limitations under the License.
 package internal
 
 import (
-	"sigs.k8s.io/cluster-api/util/collections"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -475,7 +474,7 @@ func TestUpdateEtcdConditions(t *testing.T) {
 			}
 			controlPane := &ControlPlane{
 				KCP:      tt.kcp,
-				Machines: collections.FromMachines(tt.machines...),
+				Machines: NewFilterableMachineCollection(tt.machines...),
 			}
 			w.UpdateEtcdConditions(ctx, controlPane)
 
@@ -747,7 +746,7 @@ func TestUpdateStaticPodConditions(t *testing.T) {
 			}
 			controlPane := &ControlPlane{
 				KCP:      tt.kcp,
-				Machines: collections.FromMachines(tt.machines...),
+				Machines: NewFilterableMachineCollection(tt.machines...),
 			}
 			w.UpdateStaticPodConditions(ctx, controlPane)
 
diff --git a/scripts/ci-test.sh b/scripts/ci-test.sh
index 45105bf10..1909363f6 100755
--- a/scripts/ci-test.sh
+++ b/scripts/ci-test.sh
@@ -25,9 +25,9 @@ cd "${REPO_ROOT}" || exit 1
 source "${REPO_ROOT}/hack/ensure-go.sh"
 
 echo "*** Testing Cluster API ***"
-make test-verbose
+make test
 
 echo -e "\n*** Testing Cluster API Provider Docker ***\n"
 # Docker provider
 cd test/infrastructure/docker
-make test-verbose
+make test
diff --git a/test/e2e/config/docker.yaml b/test/e2e/config/docker.yaml
index 0896ab908..a1f4ddc4f 100644
--- a/test/e2e/config/docker.yaml
+++ b/test/e2e/config/docker.yaml
@@ -29,7 +29,7 @@ providers:
 - name: cluster-api
   type: CoreProvider
   versions:
-  - name: v0.3.0
+  - name: v0.4.0
   # Use manifest from source files
     value: ../../../config/default
     replacements:
@@ -41,7 +41,7 @@ providers:
 - name: kubeadm
   type: BootstrapProvider
   versions:
-  - name: v0.3.0
+  - name: v0.4.0
   # Use manifest from source files
     value: ../../../bootstrap/kubeadm/config/default
     replacements:
@@ -53,7 +53,7 @@ providers:
 - name: kubeadm
   type: ControlPlaneProvider
   versions:
-  - name: v0.3.0
+  - name: v0.4.0
   # Use manifest from source files
     value: ../../../controlplane/kubeadm/config/default
     replacements:
@@ -65,7 +65,7 @@ providers:
 - name: docker
   type: InfrastructureProvider
   versions:
-  - name: v0.3.0
+  - name: v0.4.0
   # Use manifest from source files
     value: ../../../test/infrastructure/docker/config/default
     replacements:
diff --git a/test/e2e/data/shared/v1alpha4/metadata.yaml b/test/e2e/data/shared/v1alpha4/metadata.yaml
index f3b4c3ef6..318ea96c6 100644
--- a/test/e2e/data/shared/v1alpha4/metadata.yaml
+++ b/test/e2e/data/shared/v1alpha4/metadata.yaml
@@ -1,6 +1,9 @@
 apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
 kind: Metadata
 releaseSeries:
+  - major: 0
+    minor: 4
+    contract: v1alpha4
   - major: 0
     minor: 3
     contract: v1alpha3
diff --git a/test/framework/kubetest/run.go b/test/framework/kubetest/run.go
index 2ab741a2f..a936adf54 100644
--- a/test/framework/kubetest/run.go
+++ b/test/framework/kubetest/run.go
@@ -35,7 +35,7 @@ import (
 )
 
 const (
-	standardImage   = "k8s.gcr.io/conformance"
+	standardImage   = "us.gcr.io/k8s-artifacts-prod/conformance"
 	ciArtifactImage = "gcr.io/k8s-staging-ci-images/conformance"
 )
 
diff --git a/test/infrastructure/docker/Makefile b/test/infrastructure/docker/Makefile
index c8bd027d8..4a595377c 100644
--- a/test/infrastructure/docker/Makefile
+++ b/test/infrastructure/docker/Makefile
@@ -69,12 +69,8 @@ help:  ## Display this help
 ## --------------------------------------
 
 .PHONY: test
-test: ## Run tests.
-	go test ./... $(TEST_ARGS)
-
-.PHONY: test-verbose
-test-verbose: ## Run tests with verbose settings.
-	TEST_ARGS="$(TEST_ARGS) -v" $(MAKE) test
+test: ## Run tests
+	go test -v ./...
 
 ## --------------------------------------
 ## Binaries
diff --git a/test/infrastructure/docker/exp/docker/nodepool.go b/test/infrastructure/docker/exp/docker/nodepool.go
index ad97726b6..ab545a2e2 100644
--- a/test/infrastructure/docker/exp/docker/nodepool.go
+++ b/test/infrastructure/docker/exp/docker/nodepool.go
@@ -214,15 +214,8 @@ func (np *NodePool) refresh() error {
 func (np *NodePool) reconcileMachine(ctx context.Context, machine *docker.Machine) (ctrl.Result, error) {
 	log := ctrl.LoggerFrom(ctx)
 
-	var machineStatus infrav1exp.DockerMachinePoolInstanceStatus
-	isFound := false
-	for _, instanceStatus := range np.dockerMachinePool.Status.Instances {
-		if instanceStatus.InstanceName == machine.Name() {
-			machineStatus = instanceStatus
-			isFound = true
-		}
-	}
-	if !isFound {
+	machineStatus, err := getInstanceStatusByMachineName(np.dockerMachinePool, machine.Name())
+	if err != nil {
 		log.Info("Creating instance record", "instance", machine.Name())
 		machineStatus = infrav1exp.DockerMachinePoolInstanceStatus{
 			InstanceName: machine.Name(),
@@ -233,14 +226,6 @@ func (np *NodePool) reconcileMachine(ctx context.Context, machine *docker.Machin
 		return ctrl.Result{Requeue: true}, nil
 	}
 
-	defer func() {
-		for i, instanceStatus := range np.dockerMachinePool.Status.Instances {
-			if instanceStatus.InstanceName == machine.Name() {
-				np.dockerMachinePool.Status.Instances[i] = machineStatus
-			}
-		}
-	}()
-
 	externalMachine, err := docker.NewMachine(np.cluster.Name, machine.Name(), np.dockerMachinePool.Spec.Template.CustomImage, np.labelFilters)
 	if err != nil {
 		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalMachine named %s", machine.Name())
@@ -337,3 +322,14 @@ func getBootstrapData(ctx context.Context, kClient client.Client, machinePool *c
 
 	return base64.StdEncoding.EncodeToString(value), nil
 }
+
+// getInstanceStatusByMachineName returns the instance status for a given machine by name or error if it doesn't exist
+func getInstanceStatusByMachineName(dockerMachinePool *infrav1exp.DockerMachinePool, machineName string) (infrav1exp.DockerMachinePoolInstanceStatus, error) {
+	var machine infrav1exp.DockerMachinePoolInstanceStatus
+	for _, machine = range dockerMachinePool.Status.Instances {
+		if machine.InstanceName == machineName {
+			return machine, nil
+		}
+	}
+	return machine, errors.Errorf("no machine found with name %s", machineName)
+}
diff --git a/test/infrastructure/docker/go.mod b/test/infrastructure/docker/go.mod
index fc9dba23b..0d20b8560 100644
--- a/test/infrastructure/docker/go.mod
+++ b/test/infrastructure/docker/go.mod
@@ -13,7 +13,7 @@ require (
 	k8s.io/klog v1.0.0
 	k8s.io/utils v0.0.0-20210111153108-fddb29f9d009
 	sigs.k8s.io/cluster-api v0.3.3
-	sigs.k8s.io/controller-runtime v0.8.2
+	sigs.k8s.io/controller-runtime v0.8.1
 	sigs.k8s.io/kind v0.9.0
 	sigs.k8s.io/yaml v1.2.0
 )
diff --git a/test/infrastructure/docker/go.sum b/test/infrastructure/docker/go.sum
index ca99e002d..e59a62a08 100644
--- a/test/infrastructure/docker/go.sum
+++ b/test/infrastructure/docker/go.sum
@@ -849,8 +849,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/controller-runtime v0.8.2 h1:SBWmI0b3uzMIUD/BIXWNegrCeZmPJ503pOtwxY0LPHM=
-sigs.k8s.io/controller-runtime v0.8.2/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU=
+sigs.k8s.io/controller-runtime v0.8.1 h1:O0K2CJ2JavK8/Tf4LfcpAwRxOFBhv8DjyrbmE6Qw59s=
+sigs.k8s.io/controller-runtime v0.8.1/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU=
 sigs.k8s.io/kind v0.9.0 h1:SoDlXq6pEc7dGagHULNRCCBYrLH6xOi7lqXTRXeAlg4=
 sigs.k8s.io/kind v0.9.0/go.mod h1:cxKQWwmbtRDzQ+RNKnR6gZG6fjbeTtItp5cGf+ww+1Y=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
diff --git a/test/infrastructure/docker/main.go b/test/infrastructure/docker/main.go
index b46531857..eb9ad6791 100644
--- a/test/infrastructure/docker/main.go
+++ b/test/infrastructure/docker/main.go
@@ -54,7 +54,6 @@ var (
 	concurrency          int
 	healthAddr           string
 	webhookPort          int
-	webhookCertDir       string
 )
 
 func init() {
@@ -68,25 +67,6 @@ func init() {
 	// +kubebuilder:scaffold:scheme
 }
 
-func initFlags(fs *pflag.FlagSet) {
-	fs.StringVar(&metricsBindAddr, "metrics-bind-addr", ":8080",
-		"The address the metric endpoint binds to.")
-	fs.IntVar(&concurrency, "concurrency", 10,
-		"The number of docker machines to process simultaneously")
-	fs.BoolVar(&enableLeaderElection, "leader-elect", false,
-		"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
-	fs.DurationVar(&syncPeriod, "sync-period", 10*time.Minute,
-		"The minimum interval at which watched resources are reconciled (e.g. 15m)")
-	fs.StringVar(&healthAddr, "health-addr", ":9440",
-		"The address the health endpoint binds to.")
-	fs.IntVar(&webhookPort, "webhook-port", 9443,
-		"Webhook Server port")
-	fs.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/",
-		"Webhook cert dir, only used when webhook-port is specified.")
-
-	feature.MutableGates.AddFlag(fs)
-}
-
 func main() {
 	rand.Seed(time.Now().UnixNano())
 
@@ -104,7 +84,6 @@ func main() {
 		SyncPeriod:             &syncPeriod,
 		HealthProbeBindAddress: healthAddr,
 		Port:                   webhookPort,
-		CertDir:                webhookCertDir,
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")
@@ -126,6 +105,23 @@ func main() {
 	}
 }
 
+func initFlags(fs *pflag.FlagSet) {
+	fs.StringVar(&metricsBindAddr, "metrics-bind-addr", ":8080",
+		"The address the metric endpoint binds to.")
+	fs.IntVar(&concurrency, "concurrency", 10,
+		"The number of docker machines to process simultaneously")
+	fs.BoolVar(&enableLeaderElection, "leader-elect", false,
+		"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
+	fs.DurationVar(&syncPeriod, "sync-period", 10*time.Minute,
+		"The minimum interval at which watched resources are reconciled (e.g. 15m)")
+	fs.StringVar(&healthAddr, "health-addr", ":9440",
+		"The address the health endpoint binds to.")
+	fs.IntVar(&webhookPort, "webhook-port", 9443,
+		"Webhook Server port")
+
+	feature.MutableGates.AddFlag(fs)
+}
+
 func setupChecks(mgr ctrl.Manager) {
 	if err := mgr.AddReadyzCheck("ping", healthz.Ping); err != nil {
 		setupLog.Error(err, "unable to create ready check")
diff --git a/util/patch/patch_test.go b/util/patch/patch_test.go
index a719f63ab..78008c99c 100644
--- a/util/patch/patch_test.go
+++ b/util/patch/patch_test.go
@@ -865,38 +865,6 @@ var _ = Describe("Patch Helper", func() {
 			}, timeout).Should(BeTrue())
 		})
 	})
-
-	It("Should error if the object isn't the same", func() {
-		cluster := &clusterv1.Cluster{
-			ObjectMeta: metav1.ObjectMeta{
-				GenerateName: "test-",
-				Namespace:    "default",
-			},
-		}
-
-		machineSet := &clusterv1.MachineSet{
-			ObjectMeta: metav1.ObjectMeta{
-				GenerateName: "test-ms",
-				Namespace:    "default",
-			},
-			Spec: clusterv1.MachineSetSpec{
-				ClusterName: "test1",
-				Template: clusterv1.MachineTemplateSpec{
-					Spec: clusterv1.MachineSpec{
-						ClusterName: "test1",
-					},
-				},
-			},
-		}
-
-		Expect(testEnv.Create(ctx, cluster)).To(Succeed())
-		Expect(testEnv.Create(ctx, machineSet)).To(Succeed())
-
-		patcher, err := NewHelper(cluster, testEnv)
-		Expect(err).ToNot(HaveOccurred())
-
-		Expect(patcher.Patch(ctx, machineSet)).ToNot(Succeed())
-	})
 })
 
 func TestNewHelperNil(t *testing.T) {
diff --git a/util/util_test.go b/util/util_test.go
index fd389597b..5b0e33776 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -39,6 +39,54 @@ var (
 	ctx = ctrl.SetupSignalHandler()
 )
 
+func TestParseMajorMinorPatch(t *testing.T) {
+	g := NewWithT(t)
+
+	var testcases = []struct {
+		name        string
+		input       string
+		output      semver.Version
+		expectError bool
+	}{
+		{
+			name:  "should parse an OCI compliant string",
+			input: "v1.2.16_foo-1",
+			output: semver.Version{
+				Major: 1,
+				Minor: 2,
+				Patch: 16,
+			},
+		},
+		{
+			name:  "should parse a valid semver",
+			input: "v1.16.6+foobar-0",
+			output: semver.Version{
+				Major: 1,
+				Minor: 16,
+				Patch: 6,
+			},
+		},
+		{
+			name:        "should error if there is no patch version",
+			input:       "v1.16+foobar-0",
+			expectError: true,
+		},
+		{
+			name:        "should error if there is no minor and patch",
+			input:       "v1+foobar-0",
+			expectError: true,
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			out, err := ParseMajorMinorPatch(tc.input)
+			g.Expect(err != nil).To(Equal(tc.expectError))
+			g.Expect(out).To(Equal(tc.output))
+		})
+	}
+}
+
 func TestMachineToInfrastructureMapFunc(t *testing.T) {
 	g := NewWithT(t)
 
diff --git a/util/version/version_test.go b/util/version/version_test.go
deleted file mode 100644
index 40d5a163a..000000000
--- a/util/version/version_test.go
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
-Copyright 2021 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package version
-
-import (
-	"github.com/blang/semver"
-	. "github.com/onsi/gomega"
-	"testing"
-)
-
-func TestParseMajorMinorPatch(t *testing.T) {
-	g := NewWithT(t)
-
-	var testcases = []struct {
-		name        string
-		input       string
-		output      semver.Version
-		expectError bool
-	}{
-		{
-			name:  "should parse an OCI compliant string",
-			input: "v1.2.16_foo-1",
-			output: semver.Version{
-				Major: 1,
-				Minor: 2,
-				Patch: 16,
-			},
-		},
-		{
-			name:  "should parse a valid semver",
-			input: "v1.16.6+foobar-0",
-			output: semver.Version{
-				Major: 1,
-				Minor: 16,
-				Patch: 6,
-			},
-		},
-		{
-			name:        "should error if there is no patch version",
-			input:       "v1.16+foobar-0",
-			expectError: true,
-		},
-		{
-			name:        "should error if there is no minor and patch",
-			input:       "v1+foobar-0",
-			expectError: true,
-		},
-		{
-			name:        "should error if there is no v prefix",
-			input:       "1.4.7",
-			expectError: true,
-		},
-	}
-
-	for _, tc := range testcases {
-		t.Run(tc.name, func(t *testing.T) {
-			out, err := ParseMajorMinorPatch(tc.input)
-			g.Expect(err != nil).To(Equal(tc.expectError))
-			g.Expect(out).To(Equal(tc.output))
-		})
-	}
-}
-
-func TestParseMajorMinorPatchTolerant(t *testing.T) {
-	g := NewWithT(t)
-
-	var testcases = []struct {
-		name        string
-		input       string
-		output      semver.Version
-		expectError bool
-	}{
-		{
-			name:  "should parse an OCI compliant string",
-			input: "v1.2.16_foo-1",
-			output: semver.Version{
-				Major: 1,
-				Minor: 2,
-				Patch: 16,
-			},
-		},
-		{
-			name:  "should parse a valid semver with no v prefix",
-			input: "1.16.6+foobar-0",
-			output: semver.Version{
-				Major: 1,
-				Minor: 16,
-				Patch: 6,
-			},
-		},
-		{
-			name:        "should error if there is no patch version",
-			input:       "1.16+foobar-0",
-			expectError: true,
-		},
-		{
-			name:        "should error if there is no minor and patch",
-			input:       "1+foobar-0",
-			expectError: true,
-		},
-	}
-
-	for _, tc := range testcases {
-		t.Run(tc.name, func(t *testing.T) {
-			out, err := ParseMajorMinorPatchTolerant(tc.input)
-			g.Expect(err != nil).To(Equal(tc.expectError))
-			g.Expect(out).To(Equal(tc.output))
-		})
-	}
-}