Repository: kubernetes-sigs/cluster-api
Pull request #2995 (2020-05-01 19:43:33)

Add helper wrapping envtest
**What this PR does / why we need it**:
Trying to make envtest it a simpler API, less copypasta. I think we should be able to keep it up to date with the full set of our CRDs.

I only updated the `controllers` package tests, but others can be updated as needed.

Split out from #2975

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api/pull/2995#" title="Author self-approved">benmoss</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **fabriziopandini**
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"]} -->
k8s-ci-robot: @benmoss: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-apidiff | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256308314262736896) | `/test pull-cluster-api-apidiff`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-apidiff | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256308314262736896) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256308314266931201) | `/test pull-cluster-api-make`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-apidiff | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256308314262736896) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256308314266931201) | `/test pull-cluster-api-make`
pull-cluster-api-test | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256308314266931204) | `/test pull-cluster-api-test`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-make | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256308314266931201) | `/test pull-cluster-api-make`
pull-cluster-api-test | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256308314266931204) | `/test pull-cluster-api-test`
pull-cluster-api-apidiff | 06a108221f1d51c76165ef843e73f444c30b6e1d | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256311081710653440) | `/test pull-cluster-api-apidiff`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-test | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256308314266931204) | `/test pull-cluster-api-test`
pull-cluster-api-apidiff | 06a108221f1d51c76165ef843e73f444c30b6e1d | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256311081710653440) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 06a108221f1d51c76165ef843e73f444c30b6e1d | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256311081706459139) | `/test pull-cluster-api-make`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-test | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256308314266931204) | `/test pull-cluster-api-test`
pull-cluster-api-make | 06a108221f1d51c76165ef843e73f444c30b6e1d | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256311081706459139) | `/test pull-cluster-api-make`
pull-cluster-api-apidiff | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256312340211568642) | `/test pull-cluster-api-apidiff`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-test | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256308314266931204) | `/test pull-cluster-api-test`
pull-cluster-api-apidiff | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256312340211568642) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256312340211568646) | `/test pull-cluster-api-make`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-verify | 614f0d517682e8680bbdb7dd5bfbea8e50a6a968 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-verify/1256308314266931206) | `/test pull-cluster-api-verify`
pull-cluster-api-apidiff | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256312340211568642) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256312340211568646) | `/test pull-cluster-api-make`
pull-cluster-api-test | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256312340211568641) | `/test pull-cluster-api-test`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-make | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-make/1256312340211568646) | `/test pull-cluster-api-make`
pull-cluster-api-test | 1450fc97cc597ef3513df3179cd20a272b182abf | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1256312340211568641) | `/test pull-cluster-api-test`
pull-cluster-api-apidiff | c200d4eab32d9da60570d56aabd41a8692212e62 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1256319889803579392) | `/test pull-cluster-api-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=2995). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3benmoss). 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 -->
benmoss: Not sure what to do about the apidiff failure

```
sigs.k8s.io/cluster-api/util/kubeconfig
  Incompatible changes:
  - CreateEnvTestSecret: removed
```
k8s-ci-robot: @benmoss: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-apidiff | 6c4f482fe2b82a43dc50670196bb4325c0ae4491 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1257299179189833728) | `/test pull-cluster-api-apidiff`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-apidiff | 857f3cf2f22e3029c25f967bc3f8358a9dcff527 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-apidiff/1257299934734979076) | `/test pull-cluster-api-apidiff`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=2995). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3benmoss). 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 -->
benmoss: > I like the idea of having util for envtest, but I find a little bit confusing the fact that TestEnvironment is also a Manager and a Client.

I just did this as a convenience, to reduce the verbosity of using it. We could not embed those structs though, the main win here is to avoid our tests having 5 pieces of state, instead we just wrap up everything in the environment object.
k8s-ci-robot: @benmoss: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-test | b5c69beb0aacebefd27903be69dd89b674afaec3 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-test/1257329757742174214) | `/test pull-cluster-api-test`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-e2e | 21cfedd7de3b68f1e4e17888ef6a10e182899e11 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/2995/pull-cluster-api-e2e/1257338816272797696) | `/test pull-cluster-api-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=cluster-api&pr=2995). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3benmoss). 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 -->
benmoss: /test pull-cluster-api-e2e

Patch:
diff --git a/.dockerignore b/.dockerignore
index 5df157835..914ce2dca 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -15,5 +15,3 @@ Tiltfile
 test/infrastructure/docker/e2e/logs/**
 **/config/**/*.yaml
 _artifacts
-Makefile
-**/Makefile
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index b0e297c8b..2226acae9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -22,6 +22,4 @@ about: Tell us about a problem you are experiencing
 - Kubernetes version: (use `kubectl version`):
 - OS (e.g. from `/etc/os-release`):
 
-/kind bug
-[One or more /area label. See https://github.com/kubernetes-sigs/cluster-api/labels?q=area for the list of labels]
- 
+/kind bug
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 91f850da9..c757b577d 100644
--- a/Makefile
+++ b/Makefile
@@ -86,8 +86,6 @@ TAG ?= dev
 ARCH ?= amd64
 ALL_ARCH = amd64 arm arm64 ppc64le s390x
 
-GINKGO_NODES ?= 1
-
 # Allow overriding the imagePullPolicy
 PULL_POLICY ?= Always
 
@@ -134,22 +132,6 @@ test-capd-e2e: ## Rebuild the docker provider and run the capd-e2e tests
 	$(MAKE) -C test/infrastructure/docker docker-build REGISTRY=gcr.io/k8s-staging-capi-docker
 	$(MAKE) -C test/infrastructure/docker run-e2e
 
-E2E_FLAGS := REGISTRY=gcr.io/k8s-staging-cluster-api TAG=dev ARCH=amd64 PULL_POLICY=IfNotPresent GINKGO_FOCUS="$(GINKGO_FOCUS)" GINKGO_NODES=$(GINKGO_NODES) GINKGO_NOCOLOR=false E2E_CONF_FILE=config/docker-dev.yaml ARTIFACTS=$(abspath _artifacts) SKIP_RESOURCE_CLEANUP=false USE_EXISTING_CLUSTER=false
-
-.PHONY: e2e
-e2e: ## Run clusterctl e2e
-	mkdir -p _artifacts
-	$(E2E_FLAGS) $(MAKE) docker-build
-	$(E2E_FLAGS) $(MAKE) -C test/infrastructure/docker docker-build
-	docker pull quay.io/jetstack/cert-manager-cainjector:v0.11.0
-	docker pull quay.io/jetstack/cert-manager-webhook:v0.11.0
-	docker pull quay.io/jetstack/cert-manager-controller:v0.11.0
-	$(E2E_FLAGS) $(MAKE) -C test/e2e run
-
-.PHONY: clean-e2e
-clean-e2e: clean-manifests ## Clean-up after clusterctl e2e
-	rm -rf _artifacts
-
 ## --------------------------------------
 ## Binaries
 ## --------------------------------------
@@ -219,9 +201,6 @@ lint-full: $(GOLANGCI_LINT) ## Run slower linters to detect possible issues
 	cd $(E2E_FRAMEWORK_DIR); $(GOLANGCI_LINT) run -v --fast=false
 	cd $(CAPD_DIR); $(GOLANGCI_LINT) run -v --fast=false
 
-apidiff: $(GO_APIDIFF) ## Check for API differences
-	$(GO_APIDIFF) $(shell git rev-parse origin/master) --print-compatible
-
 ## --------------------------------------
 ## Generate / Manifests
 ## --------------------------------------
@@ -550,11 +529,6 @@ clean-book: ## Remove all generated GitBook files
 clean-bindata: ## Remove bindata generated folder
 	rm -rf $(GOBINDATA_CLUSTERCTL_DIR)/manifest
 
-.PHONY: clean-manifests ## Reset manifests in config directories back to master
-clean-manifests:
-	@read -p "WARNING: This will reset all config directories to local master. Press [ENTER] to continue."
-	git checkout master config bootstrap/kubeadm/config controlplane/kubeadm/config test/infrastructure/docker/config
-
 .PHONY: format-tiltfile
 format-tiltfile: ## Format Tiltfile
 	./hack/verify-starlark.sh fix
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index ef2b8e93f..7ca8e639c 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -5,7 +5,6 @@ aliases:
   - neolit123
   - justinsb
   - timothysc
-  - fabriziopandini
 
   # -----------------------------------------------------------
   # OWNER_ALIASES for Cluster API
diff --git a/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go b/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go
index a2f99eb47..fec09f452 100644
--- a/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go
+++ b/bootstrap/kubeadm/controllers/kubeadmconfig_controller.go
@@ -42,9 +42,7 @@ import (
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/feature"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	"sigs.k8s.io/cluster-api/util/secret"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -92,19 +90,24 @@ func (r *KubeadmConfigReconciler) SetupWithManager(mgr ctrl.Manager, option cont
 
 	r.scheme = mgr.GetScheme()
 
-	b := ctrl.NewControllerManagedBy(mgr).
+	builder := ctrl.NewControllerManagedBy(mgr).
 		For(&bootstrapv1.KubeadmConfig{}).
 		WithOptions(option).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Watches(
 			&source.Kind{Type: &clusterv1.Machine{}},
 			&handler.EnqueueRequestsFromMapFunc{
 				ToRequests: handler.ToRequestsFunc(r.MachineToBootstrapMapFunc),
 			},
+		).
+		Watches(
+			&source.Kind{Type: &clusterv1.Cluster{}},
+			&handler.EnqueueRequestsFromMapFunc{
+				ToRequests: handler.ToRequestsFunc(r.ClusterToKubeadmConfigs),
+			},
 		)
 
 	if feature.Gates.Enabled(feature.MachinePool) {
-		b = b.Watches(
+		builder = builder.Watches(
 			&source.Kind{Type: &expv1.MachinePool{}},
 			&handler.EnqueueRequestsFromMapFunc{
 				ToRequests: handler.ToRequestsFunc(r.MachinePoolToBootstrapMapFunc),
@@ -112,22 +115,10 @@ func (r *KubeadmConfigReconciler) SetupWithManager(mgr ctrl.Manager, option cont
 		)
 	}
 
-	c, err := b.Build(r)
-	if err != nil {
+	if err := builder.Complete(r); err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
 
-	err = c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: handler.ToRequestsFunc(r.ClusterToKubeadmConfigs),
-		},
-		predicates.ClusterUnpausedAndInfrastructureReady(r.Log),
-	)
-	if err != nil {
-		return errors.Wrap(err, "failed adding Watch for Clusters to controller manager")
-	}
-
 	return nil
 }
 
@@ -177,11 +168,6 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
 		return ctrl.Result{}, err
 	}
 
-	if annotations.IsPaused(cluster, config) {
-		log.Info("Reconciliation is paused for this object")
-		return ctrl.Result{}, nil
-	}
-
 	scope := &Scope{
 		Logger:      log,
 		Config:      config,
diff --git a/bootstrap/kubeadm/docs/external-etcd.md b/bootstrap/kubeadm/docs/external-etcd.md
index 8cdb7805e..8c33441f5 100644
--- a/bootstrap/kubeadm/docs/external-etcd.md
+++ b/bootstrap/kubeadm/docs/external-etcd.md
@@ -1,6 +1,6 @@
 # Support for external etcd
 
-Cluster API Bootstrap Provider Kubeadm supports using an external etcd cluster for your workload Kubernetes clusters.
+Cluster API Bootstrap Provider Kubeadm  supports using an external etcd cluster for your workload Kubernetes clusters.
 
 ## ⚠️ Warnings ⚠️
 
@@ -8,69 +8,42 @@ Before getting started you should be aware of the expectations that come with us
 
 * Cluster API is unable to manage any aspect of the external etcd cluster.
 * Depending on how you configure your etcd nodes you may incur additional cloud costs in data transfer.
-    * As an example, cross availability zone traffic can cost money on cloud providers. You don't have to deploy etcd across availability zones, but if you do please be aware of the costs.
+    * As an example, cross availability zone traffic can cost money on cloud providers. You don't have to deploy etcd
+    across availability zones, but if you do please be aware of the costs.
 
 ## Getting started
 
-To use this, you will need to create an etcd cluster and generate an apiserver-etcd-client certificate and private key. This behaviour can be tested using [`kubeadm`](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/) and [`etcdadm`](https://github.com/kubernetes-sigs/etcdadm).
+To use this, you will need to create an etcd cluster and generate an apiserver-etcd-client key/pair.
+This behaviour can be tested using [`kubeadm`](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/) and [`etcdadm`](https://github.com/kubernetes-sigs/etcdadm).
  
 ### Setting up etcd with kubeadm 
 
-CA certificates are required to setup etcd cluster. If you already have a CA then the CA's `crt` and `key` must be copied to `/etc/kubernetes/pki/etcd/ca.crt` and `/etc/kubernetes/pki/etcd/ca.key`. 
+CA certificates are required to setup etcd cluster.
+If you already have a CA then the CA's `key` and `crt` must be copied to `/etc/kubernetes/pki/etcd/ca.crt` and `/etc/kubernetes/pki/etcd/ca.key`. 
 
-If you do not already have a CA then run command `kubeadm init phase certs etcd-ca`. This creates two files:
+If you do not already have a CA then run command `kubeadm init phase certs etcd-ca`. This creates two files
 
-* `/etc/kubernetes/pki/etcd/ca.crt`
-* `/etc/kubernetes/pki/etcd/ca.key`  
+ * `/etc/kubernetes/pki/etcd/ca.crt`
+ * `/etc/kubernetes/pki/etcd/ca.key`  
 
-This certificate and private key are used to sign etcd server and peer certificates as well as other client certificates (like the apiserver-etcd-client certificate or the etcd-healthcheck-client certificate). More information on how to setup external etcd with kubeadm can be found [here](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/#setting-up-the-cluster).
+These key/pair are used to sign etcd server, peer certificates and eventually apiserver-etcd client. More information on how to setup external etcd with kubeadm can be found [`here`](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/setup-ha-etcd-with-kubeadm/#setting-up-the-cluster).
 
-Once the etcd cluster is setup, you will need the following files from the etcd cluster:
+You would require files `/etc/kubernetes/pki/apiserver-etcd-client.key`, `/etc/kubernetes/pki/apiserver-etcd-client.crt` and `/etc/kubernetes/pki/etcd/server.crt` to setup etcd cluster. These are put in 2 secrets.
 
-1. `/etc/kubernetes/pki/apiserver-etcd-client.crt` and `/etc/kubernetes/pki/apiserver-etcd-client.key`
-2. `/etc/kubernetes/pki/etcd/ca.crt`
-
-You'll use these files to create the necessary Secrets on the management cluster (see the "Creating the required Secrets" section).
-
-### Setting up etcd with etcdadm (Alpha)
-
-`etcdadm` creates the CA if one does not exist, uses it to sign its server and peer certificates, and finally to sign the API server etcd client certificate. The CA's `crt` and `key` generated using `etcdadm` are stored in `/etc/etcd/pki/ca.crt` and `/etc/etcd/pki/ca.key`. `etcdadm` also generates a certificate for the API server etcd client; the certificate and private key are found at `/etc/etcd/pki/apiserver-etcd-client.crt` and `/etc/etcd/pki/apiserver-etcd-client.key`, respectively.
-
-Once the etcd cluster has been bootstrapped using `etcdadm`, you will need the following files from the etcd cluster:
-
-1. `/etc/etcd/pki/apiserver-etcd-client.crt` and `/etc/etcd/pki/apiserver-etcd-client.key`
-2. `/etc/etcd/pki/etcd/ca.crt`
-
-You'll use these files in the next section to create the necessary Secrets on the management cluster.
-
-## Creating the required Secrets
-
-Regardless of the method used to bootstrap the etcd cluster, you will need to use the certificates copied from the etcd cluster to create some [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-using-kubectl-create-secret) on the management cluster.
-
-In the commands below to create the Secrets, substitute `$CLUSTER_NAME` with the name of the workload cluster to be created by CAPI, and substitute `$CLUSTER_NAMESPACE` with the name of the namespace where the workload cluster will be created. The namespace can be omitted if the workload cluster will be created in the default namespace.
-
-First, you will need to create a Secret containing the API server etcd client certificate and key. This command assumes the certificate and private key are in the current directory; adjust your command accordingly if they are not:
-
-```bash
-# Kubernetes API server etcd client certificate and key
+```
+# Kubernetes APIServer etcd client certificate
 $ kubectl create secret tls $CLUSTER_NAME-apiserver-etcd-client \
-  --cert apiserver-etcd-client.crt \
-  --key apiserver-etcd-client.key \
+  --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.crt \
   --namespace $CLUSTER_NAMESPACE
-```
 
-Next, create a Secret for the etcd cluster's CA certificate. The `kubectl create secret tls` command requires _both_ a certificate and a key, but the key isn't needed by CAPI. Instead, use the `kubectl create secret generic` command, and note that the file containing the CA certificate **must** be named `tls.crt`:
-
-```bash
 # Etcd's CA crt file to validate the generated client certificates
-$ kubectl create secret generic $CLUSTER_NAME-etcd \
-  --from-file tls.crt \
+$ kubectl create secret tls $CLUSTER_NAME-etcd --cert /etc/kubernetes/pki/etcd/server.crt \ 
   --namespace $CLUSTER_NAMESPACE
 ```
+**Note:** Above command has key/pair base64 encoded by default. 
 
-**Note:** The above commands will base64 encode the certificate/key files by default.
-
-Alternatively you can base64 encode the files and put them in two secrets. The secrets must be formatted as follows and the cert material must be base64 encoded:
+**Note:** Alternatively you can base64 encode the files and put them in two secrets. The secrets
+must be formatted as follows and the cert material must be base64 encoded:
 
 ```yaml
 # Kubernetes APIServer etcd client certificate
@@ -104,11 +77,15 @@ data:
     ...
 ```
 
-The Secrets must be created _before_ creating the workload cluster.
+### Setting up etcd with etcdadm (Alpha)
+`etcdadm` creates the CA if one does not exist, uses it to sign it's server and peer certificates, and finally to sign the apiserver etcd client certificate.
+CA's `key` and `crt` generated using `etcdadm` are stored in `/etc/etcd/pki/apiserver-etcd-client.crt`, `/etc/etcd/pki/apiserver-etcd-client.key` and `/etc/etcd/pki/server.crt` .
 
-## Configuring CABPK
 
-Once the Secrets are in place on the management cluster, the rest of the process leverages standard `kubeadm` configuration. Configure your ClusterConfiguration for the workload cluster as follows:
+Just like kubeadm, it is required to create 2 [`secrets`](https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-using-kubectl-create-secret) using these server and etcd client key/pair.
+
+## Configuring CABPK
+After that the rest is standard Kubeadm. Config your ClusterConfiguration as follows:
 
 ```yaml
 apiVersion: bootstrap.cluster.x-k8s.io/v1alpha3
@@ -129,8 +106,4 @@ spec:
     ... # other clusterConfiguration goes here
 ```
 
-Create your workload cluster as normal. The new workload cluster should use the configured external etcd nodes instead of creating co-located etcd Pods on the control plane nodes.
-
-## Additional Notes/Caveats
-
-* Depending on the provider, additional changes to the workload cluster's manifest may be necessary to ensure the new CAPI-managed nodes have connectivity to the existing etcd nodes. For example, on AWS you will need to leverage the `additionalSecurityGroups` field on the AWSMachine and/or AWSMachineTemplate objects to add the CAPI-managed nodes to a security group that has connectivity to the existing etcd cluster. Other mechanisms exist for other providers.
+Create your cluster as normal!
diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go
index 8ed2ab409..21e5dda11 100644
--- a/cmd/clusterctl/client/cluster/objectgraph.go
+++ b/cmd/clusterctl/client/cluster/objectgraph.go
@@ -17,6 +17,8 @@ limitations under the License.
 package cluster
 
 import (
+	"strings"
+
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -27,7 +29,6 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
 	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
-	secretutil "sigs.k8s.io/cluster-api/util/secret"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -338,15 +339,15 @@ func (o *objectGraph) setSoftOwnership() {
 			continue
 		}
 
-		// If the secret name is not a valid cluster secret name, ignore it.
-		secretClusterName, _, err := secretutil.ParseSecretName(secret.identity.Name)
-		if err != nil {
+		// If the secret name does not comply the naming convention {cluster-name}-{certificate-name/kubeconfig}, ignore it.
+		nameSplit := strings.Split(secret.identity.Name, "-")
+		if len(nameSplit) != 2 {
 			continue
 		}
 
-		// If the secret is linked to a cluster, then add the cluster to the list of the secrets's softOwners.
+		// If the secret is linked to a cluster by the naming convention, then add the cluster to the list of the secrets's softOwners.
 		for _, cluster := range clusters {
-			if secretClusterName == cluster.identity.Name && secret.identity.Namespace == cluster.identity.Namespace {
+			if nameSplit[0] == cluster.identity.Name && secret.identity.Namespace == cluster.identity.Namespace {
 				secret.addSoftOwner(cluster)
 			}
 		}
diff --git a/cmd/clusterctl/client/config/reader_viper.go b/cmd/clusterctl/client/config/reader_viper.go
index 1104fc147..c375dbc95 100644
--- a/cmd/clusterctl/client/config/reader_viper.go
+++ b/cmd/clusterctl/client/config/reader_viper.go
@@ -64,16 +64,6 @@ func newViperReader(opts ...viperReaderOption) Reader {
 func (v *viperReader) Init(path string) error {
 	log := logf.Log
 
-	// Configure viper for reading environment variables as well, and more specifically:
-	// AutomaticEnv force viper to check for an environment variable any time a viper.Get request is made.
-	// It will check for a environment variable with a name matching the key uppercased; in case name use the - delimiter,
-	// the SetEnvKeyReplacer forces matching to name use the _ delimiter instead (- is not allowed in linux env variable names).
-	replacer := strings.NewReplacer("-", "_")
-	viper.SetEnvKeyReplacer(replacer)
-	viper.AllowEmptyEnv(true)
-	viper.AutomaticEnv()
-
-	// Reads the clusterctl config file
 	if path != "" {
 		if _, err := os.Stat(path); err != nil {
 			return err
@@ -81,24 +71,32 @@ func (v *viperReader) Init(path string) error {
 		// Use path file from the flag.
 		viper.SetConfigFile(path)
 	} else {
-		// Checks if there is a default .cluster-api/clusterctl{.extension} file in home directory
+		// Configure for searching .cluster-api/clusterctl{.extension} in home directory
+		viper.SetConfigName(ConfigName)
+		for _, p := range v.configPaths {
+			viper.AddConfigPath(p)
+		}
 		if !v.checkDefaultConfig() {
 			// since there is no default config to read from, just skip
 			// reading in config
 			log.V(5).Info("No default config file available")
 			return nil
 		}
-		// Configure viper for reading .cluster-api/clusterctl{.extension} in home directory
-		viper.SetConfigName(ConfigName)
-		for _, p := range v.configPaths {
-			viper.AddConfigPath(p)
-		}
 	}
 
+	// Configure for reading environment variables as well, and more specifically:
+	// AutomaticEnv force viper to check for an environment variable any time a viper.Get request is made.
+	// It will check for a environment variable with a name matching the key uppercased; in case name use the - delimiter,
+	// the SetEnvKeyReplacer forces matching to name use the _ delimiter instead (- is not allowed in linux env variable names).
+	replacer := strings.NewReplacer("-", "_")
+	viper.SetEnvKeyReplacer(replacer)
+	viper.AllowEmptyEnv(true)
+	viper.AutomaticEnv()
+
 	if err := viper.ReadInConfig(); err != nil {
 		return err
 	}
-	log.V(5).Info("Using configuration", "File", viper.ConfigFileUsed())
+	log.V(5).Info("Reading configuration", "File", viper.ConfigFileUsed())
 	return nil
 }
 
@@ -123,7 +121,7 @@ func (v *viperReader) UnmarshalKey(key string, rawval interface{}) error {
 func (v *viperReader) checkDefaultConfig() bool {
 	for _, path := range v.configPaths {
 		for _, ext := range viper.SupportedExts {
-			f := filepath.Join(path, fmt.Sprintf("%s.%s", ConfigName, ext))
+			f := fmt.Sprintf("%s%s.%s", path, ConfigName, ext)
 			_, err := os.Stat(f)
 			if err == nil {
 				return true
diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go
index cd4541e4e..977bcc966 100644
--- a/controllers/cluster_controller.go
+++ b/controllers/cluster_controller.go
@@ -35,9 +35,7 @@ import (
 	"sigs.k8s.io/cluster-api/controllers/metrics"
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	"sigs.k8s.io/cluster-api/util/secret"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -79,7 +77,6 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager, options controlle
 			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.controlPlaneMachineToCluster)},
 		).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
 
 	if err != nil {
@@ -112,7 +109,7 @@ func (r *ClusterReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, reterr e
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, cluster) {
+	if util.IsPaused(cluster, cluster) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
diff --git a/controllers/cluster_controller_phases.go b/controllers/cluster_controller_phases.go
index 6f815fc52..7888f5ac7 100644
--- a/controllers/cluster_controller_phases.go
+++ b/controllers/cluster_controller_phases.go
@@ -29,7 +29,6 @@ import (
 	"sigs.k8s.io/cluster-api/controllers/external"
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 	"sigs.k8s.io/cluster-api/util/kubeconfig"
 	"sigs.k8s.io/cluster-api/util/patch"
@@ -79,7 +78,7 @@ func (r *ClusterReconciler) reconcileExternal(ctx context.Context, cluster *clus
 	}
 
 	// if external ref is paused, return error.
-	if annotations.IsPaused(cluster, obj) {
+	if util.IsPaused(cluster, obj) {
 		logger.V(3).Info("External object referenced is paused")
 		return external.ReconcileOutput{Paused: true}, nil
 	}
diff --git a/controllers/external/tracker.go b/controllers/external/tracker.go
index 29088fdac..bbc097c60 100644
--- a/controllers/external/tracker.go
+++ b/controllers/external/tracker.go
@@ -23,7 +23,6 @@ import (
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/source"
@@ -56,7 +55,6 @@ func (o *ObjectTracker) Watch(log logr.Logger, obj runtime.Object, handler handl
 	err := o.Controller.Watch(
 		&source.Kind{Type: u},
 		handler,
-		predicates.ResourceNotPaused(log),
 	)
 	if err != nil {
 		o.m.Delete(obj)
diff --git a/controllers/machine_controller.go b/controllers/machine_controller.go
index ab7feca5d..5461c7aa2 100644
--- a/controllers/machine_controller.go
+++ b/controllers/machine_controller.go
@@ -42,16 +42,13 @@ import (
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	kubedrain "sigs.k8s.io/cluster-api/third_party/kubernetes-drain"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
-	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
 var (
@@ -80,30 +77,22 @@ type MachineReconciler struct {
 }
 
 func (r *MachineReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-	clusterToMachines, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineList{}, mgr.GetScheme())
-	if err != nil {
-		return err
-	}
-
 	controller, err := ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.Machine{}).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
+
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
 
-	err = controller.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: clusterToMachines,
-		},
-		// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
-		predicates.ClusterUnpaused(r.Log),
-	)
+	// Add a watch on clusterv1.Cluster object for paused notifications.
+	clusterToMachines, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineList{}, mgr.GetScheme())
 	if err != nil {
-		return errors.Wrap(err, "failed to add Watch for Clusters to controller manager")
+		return err
+	}
+	if err := util.WatchOnClusterPaused(controller, clusterToMachines); err != nil {
+		return err
 	}
 
 	r.recorder = mgr.GetEventRecorderFor("machine-controller")
@@ -154,7 +143,7 @@ func (r *MachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, reterr e
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, m) {
+	if util.IsPaused(cluster, m) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
@@ -333,6 +322,10 @@ func (r *MachineReconciler) isDeleteNodeAllowed(ctx context.Context, cluster *cl
 		// Do not delete the NodeRef if there are no remaining members of
 		// the control plane.
 		return errNoControlPlaneNodes
+	case numControlPlaneMachines == 1 && util.IsControlPlaneMachine(machine):
+		// Do not delete the NodeRef if this is the last member of the
+		// control plane.
+		return errLastControlPlaneNode
 	default:
 		// Otherwise it is okay to delete the NodeRef.
 		return nil
diff --git a/controllers/machine_controller_phases.go b/controllers/machine_controller_phases.go
index 73cf303a9..1f6df26a8 100644
--- a/controllers/machine_controller_phases.go
+++ b/controllers/machine_controller_phases.go
@@ -28,7 +28,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/utils/pointer"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -103,7 +102,7 @@ func (r *MachineReconciler) reconcileExternal(ctx context.Context, cluster *clus
 	}
 
 	// if external ref is paused, return error.
-	if annotations.IsPaused(cluster, obj) {
+	if util.IsPaused(cluster, obj) {
 		logger.V(3).Info("External object referenced is paused")
 		return external.ReconcileOutput{Paused: true}, nil
 	}
diff --git a/controllers/machinedeployment_controller.go b/controllers/machinedeployment_controller.go
index 6404c3e80..b0ced4ecb 100644
--- a/controllers/machinedeployment_controller.go
+++ b/controllers/machinedeployment_controller.go
@@ -30,9 +30,7 @@ import (
 	"k8s.io/client-go/tools/record"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -60,12 +58,7 @@ type MachineDeploymentReconciler struct {
 }
 
 func (r *MachineDeploymentReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-	clusterToMachineDeployments, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineDeploymentList{}, mgr.GetScheme())
-	if err != nil {
-		return err
-	}
-
-	c, err := ctrl.NewControllerManagedBy(mgr).
+	controller, err := ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.MachineDeployment{}).
 		Owns(&clusterv1.MachineSet{}).
 		Watches(
@@ -73,22 +66,19 @@ func (r *MachineDeploymentReconciler) SetupWithManager(mgr ctrl.Manager, options
 			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.MachineSetToDeployments)},
 		).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
+
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
 
-	err = c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: clusterToMachineDeployments,
-		},
-		// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
-		predicates.ClusterUnpaused(r.Log),
-	)
+	// Add a watch on clusterv1.Cluster object for paused notifications.
+	clusterToMachineDeployments, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineDeploymentList{}, mgr.GetScheme())
 	if err != nil {
-		return errors.Wrap(err, "failed to add Watch for Clusters to controller manager")
+		return err
+	}
+	if err := util.WatchOnClusterPaused(controller, clusterToMachineDeployments); err != nil {
+		return err
 	}
 
 	r.recorder = mgr.GetEventRecorderFor("machinedeployment-controller")
@@ -117,7 +107,7 @@ func (r *MachineDeploymentReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, deployment) {
+	if util.IsPaused(cluster, deployment) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
diff --git a/controllers/machinehealthcheck_controller.go b/controllers/machinehealthcheck_controller.go
index f6de9b728..5bd74f2a8 100644
--- a/controllers/machinehealthcheck_controller.go
+++ b/controllers/machinehealthcheck_controller.go
@@ -35,9 +35,7 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -78,25 +76,20 @@ type MachineHealthCheckReconciler struct {
 func (r *MachineHealthCheckReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
 	controller, err := ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.MachineHealthCheck{}).
+		Watches(
+			&source.Kind{Type: &clusterv1.Cluster{}},
+			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.clusterToMachineHealthCheck)},
+		).
 		Watches(
 			&source.Kind{Type: &clusterv1.Machine{}},
 			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.machineToMachineHealthCheck)},
 		).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
+
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
-	err = controller.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.clusterToMachineHealthCheck)},
-		// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
-		predicates.ClusterUnpaused(r.Log),
-	)
-	if err != nil {
-		return errors.Wrap(err, "failed to add Watch for Clusters to controller manager")
-	}
 
 	// Add index to MachineHealthCheck for listing by Cluster Name
 	if err := mgr.GetCache().IndexField(&clusterv1.MachineHealthCheck{},
@@ -147,7 +140,7 @@ func (r *MachineHealthCheckReconciler) Reconcile(req ctrl.Request) (_ ctrl.Resul
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, m) {
+	if util.IsPaused(cluster, m) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
diff --git a/controllers/machineset_controller.go b/controllers/machineset_controller.go
index 2d3b16089..dedbe249a 100644
--- a/controllers/machineset_controller.go
+++ b/controllers/machineset_controller.go
@@ -38,10 +38,8 @@ import (
 	"sigs.k8s.io/cluster-api/controllers/noderefutil"
 	"sigs.k8s.io/cluster-api/controllers/remote"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -77,12 +75,7 @@ type MachineSetReconciler struct {
 }
 
 func (r *MachineSetReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-	clusterToMachineSets, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineSetList{}, mgr.GetScheme())
-	if err != nil {
-		return err
-	}
-
-	c, err := ctrl.NewControllerManagedBy(mgr).
+	controller, err := ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.MachineSet{}).
 		Owns(&clusterv1.Machine{}).
 		Watches(
@@ -90,20 +83,19 @@ func (r *MachineSetReconciler) SetupWithManager(mgr ctrl.Manager, options contro
 			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.MachineToMachineSets)},
 		).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
+
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
 
-	err = c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{ToRequests: clusterToMachineSets},
-		// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
-		predicates.ClusterUnpaused(r.Log),
-	)
+	// Add a watch on clusterv1.Cluster object for paused notifications.
+	clusterToMachineSets, err := util.ClusterToObjectsMapper(mgr.GetClient(), &clusterv1.MachineSetList{}, mgr.GetScheme())
 	if err != nil {
-		return errors.Wrap(err, "failed to add Watch for Clusters to controller manager")
+		return err
+	}
+	if err := util.WatchOnClusterPaused(controller, clusterToMachineSets); err != nil {
+		return err
 	}
 
 	r.recorder = mgr.GetEventRecorderFor("machineset-controller")
@@ -132,7 +124,7 @@ func (r *MachineSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error)
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, machineSet) {
+	if util.IsPaused(cluster, machineSet) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
diff --git a/controllers/remote/cluster.go b/controllers/remote/cluster.go
index 131bcf6a6..190098070 100644
--- a/controllers/remote/cluster.go
+++ b/controllers/remote/cluster.go
@@ -40,11 +40,12 @@ func NewClusterClient(ctx context.Context, c client.Client, cluster client.Objec
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to create client for Cluster %s/%s", cluster.Namespace, cluster.Name)
 	}
+
 	return ret, nil
 }
 
 // RESTConfig returns a configuration instance to be used with a Kubernetes client.
-func RESTConfig(ctx context.Context, c client.Reader, cluster client.ObjectKey) (*restclient.Config, error) {
+func RESTConfig(ctx context.Context, c client.Client, cluster client.ObjectKey) (*restclient.Config, error) {
 	kubeConfig, err := kcfg.FromSecret(ctx, c, cluster)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to retrieve kubeconfig secret for Cluster %s/%s", cluster.Namespace, cluster.Name)
diff --git a/controlplane/kubeadm/controllers/controller.go b/controlplane/kubeadm/controllers/controller.go
index a601526cb..4d56e78c0 100644
--- a/controlplane/kubeadm/controllers/controller.go
+++ b/controlplane/kubeadm/controllers/controller.go
@@ -36,9 +36,7 @@ import (
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	"sigs.k8s.io/cluster-api/util/secret"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -65,30 +63,22 @@ type KubeadmControlPlaneReconciler struct {
 	controller controller.Controller
 	recorder   record.EventRecorder
 
-	managementCluster         internal.ManagementCluster
-	managementClusterUncached internal.ManagementCluster
+	managementCluster internal.ManagementCluster
 }
 
 func (r *KubeadmControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
 	c, err := ctrl.NewControllerManagedBy(mgr).
 		For(&controlplanev1.KubeadmControlPlane{}).
 		Owns(&clusterv1.Machine{}).
+		Watches(
+			&source.Kind{Type: &clusterv1.Cluster{}},
+			&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.ClusterToKubeadmControlPlane)},
+		).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
-	if err != nil {
-		return errors.Wrap(err, "failed setting up with a controller manager")
-	}
 
-	err = c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: handler.ToRequestsFunc(r.ClusterToKubeadmControlPlane),
-		},
-		predicates.ClusterUnpausedAndInfrastructureReady(r.Log),
-	)
 	if err != nil {
-		return errors.Wrap(err, "failed adding Watch for Clusters to controller manager")
+		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
 
 	r.scheme = mgr.GetScheme()
@@ -98,9 +88,6 @@ func (r *KubeadmControlPlaneReconciler) SetupWithManager(mgr ctrl.Manager, optio
 	if r.managementCluster == nil {
 		r.managementCluster = &internal.Management{Client: r.Client}
 	}
-	if r.managementClusterUncached == nil {
-		r.managementClusterUncached = &internal.Management{Client: mgr.GetAPIReader()}
-	}
 
 	return nil
 }
@@ -130,7 +117,7 @@ func (r *KubeadmControlPlaneReconciler) Reconcile(req ctrl.Request) (res ctrl.Re
 	}
 	logger = logger.WithValues("cluster", cluster.Name)
 
-	if annotations.IsPaused(cluster, kcp) {
+	if util.IsPaused(cluster, kcp) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
@@ -267,11 +254,6 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, cluster *
 		return ctrl.Result{Requeue: true}, nil
 	}
 
-	// Ensure kubeadm role bindings for v1.18+
-	if err := workloadCluster.AllowBootstrapTokensToGetNodes(ctx); err != nil {
-		return ctrl.Result{}, errors.Wrap(err, "failed to set role and role binding for kubeadm")
-	}
-
 	// Update kube-proxy daemonset.
 	if err := workloadCluster.UpdateKubeProxyImageInfo(ctx, kcp); err != nil {
 		logger.Error(err, "failed to update kube-proxy daemonset")
@@ -350,7 +332,6 @@ func (r *KubeadmControlPlaneReconciler) ClusterToKubeadmControlPlane(o handler.M
 
 // reconcileHealth performs health checks for control plane components and etcd
 // It removes any etcd members that do not have a corresponding node.
-// Also, as a final step, checks if there is any machines that is being deleted.
 func (r *KubeadmControlPlaneReconciler) reconcileHealth(ctx context.Context, cluster *clusterv1.Cluster, kcp *controlplanev1.KubeadmControlPlane, controlPlane *internal.ControlPlane) error {
 	logger := controlPlane.Logger()
 
@@ -380,11 +361,5 @@ func (r *KubeadmControlPlaneReconciler) reconcileHealth(ctx context.Context, clu
 		return &capierrors.RequeueAfterError{RequeueAfter: healthCheckFailedRequeueAfter}
 	}
 
-	// We need this check for scale up as well as down to avoid scaling up when there is a machine being deleted.
-	// This should be at the end of this method as no need to wait for machine to be completely deleted to reconcile etcd.
-	// TODO: Revisit during machine remediation implementation which may need to cover other machine phases.
-	if controlPlane.HasDeletingMachine() {
-		return &capierrors.RequeueAfterError{RequeueAfter: deleteRequeueAfter}
-	}
 	return nil
 }
diff --git a/controlplane/kubeadm/controllers/scale.go b/controlplane/kubeadm/controllers/scale.go
index 8b2a4c95f..2a2915f76 100644
--- a/controlplane/kubeadm/controllers/scale.go
+++ b/controlplane/kubeadm/controllers/scale.go
@@ -25,7 +25,6 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
-	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/machinefilters"
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	"sigs.k8s.io/cluster-api/util"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -34,20 +33,6 @@ import (
 func (r *KubeadmControlPlaneReconciler) initializeControlPlane(ctx context.Context, cluster *clusterv1.Cluster, kcp *controlplanev1.KubeadmControlPlane, controlPlane *internal.ControlPlane) (ctrl.Result, error) {
 	logger := controlPlane.Logger()
 
-	// 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), machinefilters.OwnedControlPlaneMachines(kcp.Name))
-	if err != nil {
-		logger.Error(err, "failed to perform an uncached read of control plane machines for cluster")
-		return ctrl.Result{}, err
-	}
-	if len(ownedMachines) > 0 {
-		return ctrl.Result{}, errors.Errorf(
-			"control plane has already been initialized, found %d owned machine for cluster %s/%s: controller cache or management cluster is misbehaving",
-			len(ownedMachines), cluster.Namespace, cluster.Name,
-		)
-	}
-
 	bootstrapSpec := controlPlane.InitialControlPlaneConfig()
 	fd := controlPlane.FailureDomainWithFewestMachines()
 	if err := r.cloneConfigsAndGenerateMachine(ctx, cluster, kcp, bootstrapSpec, fd); err != nil {
@@ -63,7 +48,6 @@ func (r *KubeadmControlPlaneReconciler) initializeControlPlane(ctx context.Conte
 func (r *KubeadmControlPlaneReconciler) scaleUpControlPlane(ctx context.Context, cluster *clusterv1.Cluster, kcp *controlplanev1.KubeadmControlPlane, controlPlane *internal.ControlPlane) (ctrl.Result, error) {
 	logger := controlPlane.Logger()
 
-	// reconcileHealth returns err if there is a machine being delete which is a required condition to check before scaling up
 	if err := r.reconcileHealth(ctx, cluster, kcp, controlPlane); err != nil {
 		return ctrl.Result{}, &capierrors.RequeueAfterError{RequeueAfter: healthCheckFailedRequeueAfter}
 	}
@@ -89,16 +73,21 @@ func (r *KubeadmControlPlaneReconciler) scaleDownControlPlane(
 ) (ctrl.Result, error) {
 	logger := controlPlane.Logger()
 
-	if err := r.reconcileHealth(ctx, cluster, kcp, controlPlane); err != nil {
-		return ctrl.Result{}, &capierrors.RequeueAfterError{RequeueAfter: healthCheckFailedRequeueAfter}
-	}
-
 	workloadCluster, err := r.managementCluster.GetWorkloadCluster(ctx, util.ObjectKey(cluster))
 	if err != nil {
 		logger.Error(err, "Failed to create client to workload cluster")
 		return ctrl.Result{}, errors.Wrapf(err, "failed to create client to workload cluster")
 	}
 
+	// Wait for any delete in progress to complete before deleting another Machine
+	if controlPlane.HasDeletingMachine() {
+		return ctrl.Result{}, &capierrors.RequeueAfterError{RequeueAfter: deleteRequeueAfter}
+	}
+
+	if err := r.reconcileHealth(ctx, cluster, kcp, controlPlane); err != nil {
+		return ctrl.Result{}, &capierrors.RequeueAfterError{RequeueAfter: healthCheckFailedRequeueAfter}
+	}
+
 	machineToDelete, err := selectMachineForScaleDown(controlPlane)
 	if err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "failed to select machine for scale down")
diff --git a/controlplane/kubeadm/controllers/upgrade.go b/controlplane/kubeadm/controllers/upgrade.go
index 8b53ff58f..cca3fd980 100644
--- a/controlplane/kubeadm/controllers/upgrade.go
+++ b/controlplane/kubeadm/controllers/upgrade.go
@@ -57,12 +57,6 @@ func (r *KubeadmControlPlaneReconciler) upgradeControlPlane(
 		return ctrl.Result{}, errors.Wrap(err, "failed to reconcile the remote kubelet RBAC binding")
 	}
 
-	// Ensure kubeadm cluster role  & bindings for v1.18+
-	// as per https://github.com/kubernetes/kubernetes/commit/b117a928a6c3f650931bdac02a41fca6680548c4
-	if err := workloadCluster.AllowBootstrapTokensToGetNodes(ctx); err != nil {
-		return ctrl.Result{}, errors.Wrap(err, "failed to set role and role binding for kubeadm")
-	}
-
 	if err := workloadCluster.UpdateKubernetesVersionInKubeadmConfigMap(ctx, parsedVersion); err != nil {
 		return ctrl.Result{}, errors.Wrap(err, "failed to update the kubernetes version in the kubeadm config map")
 	}
diff --git a/controlplane/kubeadm/internal/cluster.go b/controlplane/kubeadm/internal/cluster.go
index 24cf31000..40a6be3d3 100644
--- a/controlplane/kubeadm/internal/cluster.go
+++ b/controlplane/kubeadm/internal/cluster.go
@@ -46,7 +46,7 @@ type ManagementCluster interface {
 
 // Management holds operations on the management cluster.
 type Management struct {
-	Client ctrlclient.Reader
+	Client ctrlclient.Client
 }
 
 // GetMachinesForCluster returns a list of machines that can be filtered or not.
diff --git a/controlplane/kubeadm/internal/workload_cluster.go b/controlplane/kubeadm/internal/workload_cluster.go
index 3337972f1..e96967a69 100644
--- a/controlplane/kubeadm/internal/workload_cluster.go
+++ b/controlplane/kubeadm/internal/workload_cluster.go
@@ -31,6 +31,7 @@ import (
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
@@ -72,7 +73,6 @@ type WorkloadCluster interface {
 	RemoveMachineFromKubeadmConfigMap(ctx context.Context, machine *clusterv1.Machine) error
 	RemoveNodeFromKubeadmConfigMap(ctx context.Context, nodeName string) error
 	ForwardEtcdLeadership(ctx context.Context, machine *clusterv1.Machine, leaderCandidate *clusterv1.Machine) error
-	AllowBootstrapTokensToGetNodes(ctx context.Context) error
 
 	// State recovery tasks.
 	ReconcileEtcdMembers(ctx context.Context) error
@@ -251,6 +251,83 @@ func (w *Workload) RemoveNodeFromKubeadmConfigMap(ctx context.Context, name stri
 	return nil
 }
 
+// ReconcileKubeletRBACBinding will create a RoleBinding for the new kubelet version during upgrades.
+// If the role binding already exists this function is a no-op.
+func (w *Workload) ReconcileKubeletRBACBinding(ctx context.Context, version semver.Version) error {
+	roleName := fmt.Sprintf("kubeadm:kubelet-config-%d.%d", version.Major, version.Minor)
+	roleBinding := &rbacv1.RoleBinding{}
+	err := w.Client.Get(ctx, ctrlclient.ObjectKey{Name: roleName, Namespace: metav1.NamespaceSystem}, roleBinding)
+	if err != nil && !apierrors.IsNotFound(err) {
+		return errors.Wrapf(err, "failed to determine if kubelet config rbac role binding %q already exists", roleName)
+	} else if err == nil {
+		// The required role binding already exists, nothing left to do
+		return nil
+	}
+
+	newRoleBinding := &rbacv1.RoleBinding{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      roleName,
+			Namespace: metav1.NamespaceSystem,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "Group",
+				Name:     "system:nodes",
+			},
+			{
+				APIGroup: "rbac.authorization.k8s.io",
+				Kind:     "Group",
+				Name:     "system:bootstrappers:kubeadm:default-node-token",
+			},
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: "rbac.authorization.k8s.io",
+			Kind:     "Role",
+			Name:     roleName,
+		},
+	}
+	if err := w.Client.Create(ctx, newRoleBinding); err != nil && !apierrors.IsAlreadyExists(err) {
+		return errors.Wrapf(err, "failed to create kubelet rbac role binding %q", roleName)
+	}
+
+	return nil
+}
+
+// ReconcileKubeletRBACRole will create a Role for the new kubelet version during upgrades.
+// If the role already exists this function is a no-op.
+func (w *Workload) ReconcileKubeletRBACRole(ctx context.Context, version semver.Version) error {
+	majorMinor := fmt.Sprintf("%d.%d", version.Major, version.Minor)
+	roleName := fmt.Sprintf("kubeadm:kubelet-config-%s", majorMinor)
+	role := &rbacv1.Role{}
+	if err := w.Client.Get(ctx, ctrlclient.ObjectKey{Name: roleName, Namespace: metav1.NamespaceSystem}, role); err != nil && !apierrors.IsNotFound(err) {
+		return errors.Wrapf(err, "failed to determine if kubelet config rbac role %q already exists", roleName)
+	} else if err == nil {
+		// The required role already exists, nothing left to do
+		return nil
+	}
+
+	newRole := &rbacv1.Role{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      roleName,
+			Namespace: metav1.NamespaceSystem,
+		},
+		Rules: []rbacv1.PolicyRule{
+			{
+				Verbs:         []string{"get"},
+				APIGroups:     []string{""},
+				Resources:     []string{"configmaps"},
+				ResourceNames: []string{fmt.Sprintf("kubelet-config-%s", majorMinor)},
+			},
+		},
+	}
+	if err := w.Client.Create(ctx, newRole); err != nil && !apierrors.IsAlreadyExists(err) {
+		return errors.Wrapf(err, "failed to create kubelet rbac role %q", roleName)
+	}
+
+	return nil
+}
+
 // ClusterStatus holds stats information about the cluster.
 type ClusterStatus struct {
 	// Nodes are a total count of nodes
diff --git a/controlplane/kubeadm/internal/workload_cluster_etcd.go b/controlplane/kubeadm/internal/workload_cluster_etcd.go
index d3eca2d8e..ec2029519 100644
--- a/controlplane/kubeadm/internal/workload_cluster_etcd.go
+++ b/controlplane/kubeadm/internal/workload_cluster_etcd.go
@@ -249,9 +249,6 @@ func (w *Workload) ForwardEtcdLeadership(ctx context.Context, machine *clusterv1
 	if leaderCandidate == nil {
 		return errors.New("leader candidate cannot be nil")
 	}
-	if leaderCandidate.Status.NodeRef == nil {
-		return errors.New("leader has no node reference")
-	}
 
 	nodes, err := w.getControlPlaneNodes(ctx)
 	if err != nil {
diff --git a/controlplane/kubeadm/internal/workload_cluster_rbac.go b/controlplane/kubeadm/internal/workload_cluster_rbac.go
deleted file mode 100644
index 0c3567506..000000000
--- a/controlplane/kubeadm/internal/workload_cluster_rbac.go
+++ /dev/null
@@ -1,163 +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 internal
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/blang/semver"
-	"github.com/pkg/errors"
-	rbac "k8s.io/api/rbac/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-const (
-	// NodeBootstrapTokenAuthGroup specifies which group a Node Bootstrap Token should be authenticated in
-	NodeBootstrapTokenAuthGroup = "system:bootstrappers:kubeadm:default-node-token"
-
-	// GetNodesClusterRoleName defines the name of the ClusterRole and ClusterRoleBinding to get nodes
-	GetNodesClusterRoleName = "kubeadm:get-nodes"
-
-	// NodesGroup defines the well-known group for all nodes.
-	NodesGroup = "system:nodes"
-
-	// KubeletConfigMapRolePrefix defines base kubelet configuration ConfigMap role prefix.
-	KubeletConfigMapRolePrefix = "kubeadm:"
-
-	// KubeletConfigMapName defines base kubelet configuration ConfigMap name.
-	KubeletConfigMapName = "kubelet-config-%d.%d"
-)
-
-// EnsureResource creates a resoutce if the target resource doesn't exist. If the resource exists already, this function will ignore the resource instead.
-func (w *Workload) EnsureResource(ctx context.Context, obj runtime.Object) error {
-	testObj := obj.DeepCopyObject()
-	key, err := ctrlclient.ObjectKeyFromObject(obj)
-	if err != nil {
-		return errors.Wrap(err, "unable to derive key for resource")
-	}
-	if err := w.Client.Get(ctx, key, testObj); err != nil && !apierrors.IsNotFound(err) {
-		return errors.Wrapf(err, "failed to determine if resource %s/%s already exists", key.Namespace, key.Name)
-	} else if err == nil {
-		// If object already exists, nothing left to do
-		return nil
-	}
-	if err := w.Client.Create(ctx, obj); err != nil {
-		if !apierrors.IsAlreadyExists(err) {
-			return errors.Wrapf(err, "unable to create resource %s/%s on workload cluster", key.Namespace, key.Name)
-		}
-	}
-	return nil
-}
-
-// AllowBootstrapTokensToGetNodes creates RBAC rules to allow Node Bootstrap Tokens to list nodes
-func (w *Workload) AllowBootstrapTokensToGetNodes(ctx context.Context) error {
-	if err := w.EnsureResource(ctx, &rbac.ClusterRole{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      GetNodesClusterRoleName,
-			Namespace: metav1.NamespaceSystem,
-		},
-		Rules: []rbac.PolicyRule{
-			{
-				Verbs:     []string{"get"},
-				APIGroups: []string{""},
-				Resources: []string{"nodes"},
-			},
-		},
-	}); err != nil {
-		return err
-	}
-
-	return w.EnsureResource(ctx, &rbac.ClusterRoleBinding{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      GetNodesClusterRoleName,
-			Namespace: metav1.NamespaceSystem,
-		},
-		RoleRef: rbac.RoleRef{
-			APIGroup: rbac.GroupName,
-			Kind:     "ClusterRole",
-			Name:     GetNodesClusterRoleName,
-		},
-		Subjects: []rbac.Subject{
-			{
-				Kind: rbac.GroupKind,
-				Name: NodeBootstrapTokenAuthGroup,
-			},
-		},
-	})
-}
-
-func generateKubeletConfigName(version semver.Version) string {
-	return fmt.Sprintf(KubeletConfigMapName, version.Major, version.Minor)
-}
-
-func generateKubeletConfigRoleName(version semver.Version) string {
-	return KubeletConfigMapRolePrefix + generateKubeletConfigName(version)
-}
-
-// ReconcileKubeletRBACBinding will create a RoleBinding for the new kubelet version during upgrades.
-// If the role binding already exists this function is a no-op.
-func (w *Workload) ReconcileKubeletRBACBinding(ctx context.Context, version semver.Version) error {
-	roleName := generateKubeletConfigRoleName(version)
-	return w.EnsureResource(ctx, &rbac.RoleBinding{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: metav1.NamespaceSystem,
-			Name:      roleName,
-		},
-		Subjects: []rbacv1.Subject{
-			{
-				APIGroup: rbac.GroupName,
-				Kind:     rbac.GroupKind,
-				Name:     NodesGroup,
-			},
-			{
-				APIGroup: rbac.GroupName,
-				Kind:     rbac.GroupKind,
-				Name:     NodeBootstrapTokenAuthGroup,
-			},
-		},
-		RoleRef: rbacv1.RoleRef{
-			APIGroup: rbac.GroupName,
-			Kind:     "Role",
-			Name:     roleName,
-		},
-	})
-
-}
-
-// ReconcileKubeletRBACRole will create a Role for the new kubelet version during upgrades.
-// If the role already exists this function is a no-op.
-func (w *Workload) ReconcileKubeletRBACRole(ctx context.Context, version semver.Version) error {
-	return w.EnsureResource(ctx, &rbacv1.Role{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      generateKubeletConfigRoleName(version),
-			Namespace: metav1.NamespaceSystem,
-		},
-		Rules: []rbacv1.PolicyRule{
-			{
-				Verbs:         []string{"get"},
-				APIGroups:     []string{""},
-				Resources:     []string{"configmaps"},
-				ResourceNames: []string{generateKubeletConfigName(version)},
-			},
-		},
-	})
-}
diff --git a/docs/book/src/developer/guide.md b/docs/book/src/developer/guide.md
index a42e43358..3bfbfe890 100644
--- a/docs/book/src/developer/guide.md
+++ b/docs/book/src/developer/guide.md
@@ -62,24 +62,6 @@ You'll need to [install `kubebuilder`][kubebuilder].
 
 [kubebuilder]: https://book.kubebuilder.io/quick-start.html#installation
 
-### Cert-Manager
-
-You'll need to deploy [cert-manager] components on your [management cluster][mcluster], using `kubectl`
-
-```bash
-kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v0.11.0/cert-manager.yaml
-```
-
-Ensure the cert-manager webhook service is ready before creating the Cluster API components. 
-
-This can be done by running: 
-
-```bash
-kubectl wait --for=condition=Available --timeout=300s apiservice v1beta1.webhook.cert-manager.io
-```
-
-[cert-manager]: https://github.com/jetstack/cert-manager
-
 ## Development
 
 ## Option 1: Tilt
@@ -133,7 +115,7 @@ and
 
 ```
 $EDITOR config/manager/manager_image_patch.yaml
-$EDITOR test/infrastructure/docker/config/default/manager_image_patch.yaml
+$EDITOR
 ```
 
 In both cases, change the `- image:` url to the digest URL mentioned above:
@@ -167,7 +149,6 @@ clusterrole.rbac.authorization.k8s.io/capi-manager-role configured
 rolebinding.rbac.authorization.k8s.io/capi-leader-election-rolebinding configured
 clusterrolebinding.rbac.authorization.k8s.io/capi-manager-rolebinding configured
 deployment.apps/capi-controller-manager created
-
 $ kustomize build test/infrastructure/docker/config | kubectl apply -f -
 namespace/capd-system configured
 customresourcedefinition.apiextensions.k8s.io/dockerclusters.infrastructure.cluster.x-k8s.io configured
diff --git a/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md b/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md
index 6c3b287a5..d51bbc2ff 100644
--- a/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md
+++ b/docs/book/src/developer/providers/v1alpha2-to-v1alpha3.md
@@ -162,45 +162,18 @@ outside of the existing module.
   }
   ```
 - Unless your controller is already watching Clusters, add a Watch to get notifications when Cluster.Spec.Paused field changes.
-  In most cases, `predicates.ClusterUnpaused` and `util.ClusterToObjectsMapper` can be used like in the example below:
+  In most cases, `util.WatchOnClusterPaused` and `util.ClusterToObjectsMapper` can be used like in the example below:
   ```go
   // Add a watch on clusterv1.Cluster object for paused notifications.
   clusterToObjectFunc, err := util.ClusterToObjectsMapper(mgr.GetClient(), <List object here>, mgr.GetScheme())
   if err != nil {
     return err
   }
-  err = controller.Watch(
-      &source.Kind{Type: &cluserv1.Cluster{}},
-      &handler.EnqueueRequestsFromMapFunc{
-          ToRequests: clusterToObjectFunc,
-      },
-      predicates.ClusterUnpaused(r.Log),
-  )
-  if err != nil {
+  if err := util.WatchOnClusterPaused(controller, clusterToObjectFunc); err != nil {
     return err
   }
   ```
   NB: You need to have `cluster.x-k8s.io/cluster-name` applied to all your objects for the mapper to function.
-- In some cases, you'll want to not just watch on Cluster.Spec.Paused changes, but also on
-  Cluster.Status.InfrastructureReady. For those cases `predicates.ClusterUnpausedAndInfrastructureReady` should be used
-  instead. 
-  ```go
-  // Add a watch on clusterv1.Cluster object for paused and infrastructureReady notifications.
-  clusterToObjectFunc, err := util.ClusterToObjectsMapper(mgr.GetClient(), <List object here>, mgr.GetScheme())
-  if err != nil {
-    return err
-  }
-  err = controller.Watch(
-        &source.Kind{Type: &cluserv1.Cluster{}},
-        &handler.EnqueueRequestsFromMapFunc{
-            ToRequests: clusterToObjectFunc,
-        },
-        predicates.ClusterUnpausedAndInfrastructureReady(r.Log),
-    )
-    if err != nil {
-      return err
-    }
-    ```
 
 ## [OPTIONAL] Support failure domains.
 
diff --git a/docs/book/src/developer/tilt.md b/docs/book/src/developer/tilt.md
index fd37cecc7..37af5162e 100644
--- a/docs/book/src/developer/tilt.md
+++ b/docs/book/src/developer/tilt.md
@@ -131,27 +131,6 @@ base64 -i ~/path/to/gcp/credentials.json
 **trigger_mode** (String, default=`auto`): Optional setting to configure if tilt should automatically rebuild on changes.
 Set to `manual` to disable auto-rebuilding and require users to trigger rebuilds of individual changed components through the UI.
 
-**extra_args** (Object, default={}): A mapping of provider to additional arguments to pass to the main binary configured
-for this provider. Each item in the array will be passed in to the manager for the given provider. 
-
-Example:
-
-```json
-{
-    "extra_args": {
-        "core": ["--feature-gates=MachinePool=true"],
-        "kubeadm-bootstrap": ["--feature-gates=MachinePool=true"],
-        "azure": ["--feature-gates=MachinePool=true"]
-    }
-}
-```
-
-With this config, the respective managers will be invoked with:
-
-```bash
-manager --feature-gates=MachinePool=true
-```
-
 ### Run Tilt!
 
 To launch your development environment, run
@@ -213,6 +192,28 @@ COPY --from=tilt-helper /usr/bin/docker /usr/bin/docker
 COPY --from=tilt-helper /go/kubernetes/client/bin/kubectl /usr/bin/kubectl
 ```
 
+
+**extra_args** (Object, default={}): A mapping of provider to additional arguments to pass to the main binary configured
+for this provider. Each item in the array will be passed in to the manager. 
+
+Example:
+
+```json
+{
+    "extra_args": {
+        "core": ["--feature-gates=MachinePool=true"],
+        "kubeadm-bootstrap": ["--feature-gates=MachinePool=true"],
+        "azure": ["--feature-gates=MachinePool=true"]
+    }
+}
+```
+
+With this config, the manager will be invoked with:
+
+```bash
+manager --feature-gates=MachinePool=true
+```
+
 ## Customizing Tilt
 
 If you need to customize Tilt's behavior, you can create files in cluster-api's `tilt.d` directory. This file is ignored
diff --git a/exp/controllers/machinepool_controller.go b/exp/controllers/machinepool_controller.go
index 9f8030317..1d1d64ffc 100644
--- a/exp/controllers/machinepool_controller.go
+++ b/exp/controllers/machinepool_controller.go
@@ -36,15 +36,11 @@ import (
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-	"sigs.k8s.io/controller-runtime/pkg/handler"
-	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
 // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;patch
@@ -66,30 +62,13 @@ type MachinePoolReconciler struct {
 }
 
 func (r *MachinePoolReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-	clusterToMachinePools, err := util.ClusterToObjectsMapper(mgr.GetClient(), &expv1.MachinePoolList{}, mgr.GetScheme())
-	if err != nil {
-		return err
-	}
-
 	c, err := ctrl.NewControllerManagedBy(mgr).
 		For(&expv1.MachinePool{}).
 		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Build(r)
 	if err != nil {
 		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
-	err = c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: clusterToMachinePools,
-		},
-		// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
-		predicates.ClusterUnpaused(r.Log),
-	)
-	if err != nil {
-		return errors.Wrap(err, "failed adding Watch for Cluster to controller manager")
-	}
 
 	r.controller = c
 	r.recorder = mgr.GetEventRecorderFor("machinepool-controller")
@@ -121,7 +100,7 @@ func (r *MachinePoolReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, rete
 	}
 
 	// Return early if the object or Cluster is paused.
-	if annotations.IsPaused(cluster, mp) {
+	if util.IsPaused(cluster, mp) {
 		logger.Info("Reconciliation is paused for this object")
 		return ctrl.Result{}, nil
 	}
diff --git a/exp/controllers/machinepool_controller_phases.go b/exp/controllers/machinepool_controller_phases.go
index ec0af7ddf..7ed61f1f0 100644
--- a/exp/controllers/machinepool_controller_phases.go
+++ b/exp/controllers/machinepool_controller_phases.go
@@ -33,7 +33,6 @@ import (
 	capierrors "sigs.k8s.io/cluster-api/errors"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -91,7 +90,7 @@ func (r *MachinePoolReconciler) reconcileExternal(ctx context.Context, cluster *
 	}
 
 	// if external ref is paused, return error.
-	if annotations.IsPaused(cluster, obj) {
+	if util.IsPaused(cluster, obj) {
 		logger.V(3).Info("External object referenced is paused")
 		return external.ReconcileOutput{Paused: true}, nil
 	}
diff --git a/go.mod b/go.mod
index 48337f501..e58637016 100644
--- a/go.mod
+++ b/go.mod
@@ -11,9 +11,12 @@ require (
 	github.com/evanphx/json-patch v4.5.0+incompatible
 	github.com/go-logr/logr v0.1.0
 	github.com/gogo/protobuf v1.3.1
+	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/google/gofuzz v1.1.0
+	github.com/hashicorp/golang-lru v0.5.4 // indirect
+	github.com/imdario/mergo v0.3.8 // indirect
 	github.com/onsi/ginkgo v1.12.0
 	github.com/onsi/gomega v1.9.0
 	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
@@ -24,8 +27,10 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.6.2
 	go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738
+	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect
 	golang.org/x/net v0.0.0-20200301022130-244492dfa37a
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
+	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	google.golang.org/appengine v1.6.1 // indirect
 	google.golang.org/grpc v1.26.0
 	k8s.io/api v0.17.2
diff --git a/go.sum b/go.sum
index c468b50c2..e129e3537 100644
--- a/go.sum
+++ b/go.sum
@@ -185,8 +185,9 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -239,15 +240,17 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
@@ -462,8 +465,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -538,8 +542,9 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/util/annotations/paused.go b/util/annotations/paused.go
deleted file mode 100644
index acb817a05..000000000
--- a/util/annotations/paused.go
+++ /dev/null
@@ -1,40 +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 annotations
-
-import (
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-)
-
-// IsPaused returns true if the Cluster is paused or the object has the `paused` annotation.
-func IsPaused(cluster *clusterv1.Cluster, o metav1.Object) bool {
-	if cluster.Spec.Paused {
-		return true
-	}
-	return HasPausedAnnotation(o)
-}
-
-// HasPausedAnnotation returns true if the object has the `paused` annotation.
-func HasPausedAnnotation(o metav1.Object) bool {
-	annotations := o.GetAnnotations()
-	if annotations == nil {
-		return false
-	}
-	_, ok := annotations[clusterv1.PausedAnnotation]
-	return ok
-}
diff --git a/util/kubeconfig/kubeconfig.go b/util/kubeconfig/kubeconfig.go
index a5ffc11f2..32a51d7a2 100644
--- a/util/kubeconfig/kubeconfig.go
+++ b/util/kubeconfig/kubeconfig.go
@@ -40,7 +40,7 @@ var (
 )
 
 // FromSecret fetches the Kubeconfig for a Cluster.
-func FromSecret(ctx context.Context, c client.Reader, cluster client.ObjectKey) ([]byte, error) {
+func FromSecret(ctx context.Context, c client.Client, cluster client.ObjectKey) ([]byte, error) {
 	out, err := secret.Get(ctx, c, cluster, secret.Kubeconfig)
 	if err != nil {
 		return nil, err
diff --git a/util/predicates/cluster_predicates.go b/util/predicates/cluster_predicates.go
deleted file mode 100644
index 06b76ee96..000000000
--- a/util/predicates/cluster_predicates.go
+++ /dev/null
@@ -1,195 +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 predicates
-
-import (
-	"github.com/go-logr/logr"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/controller-runtime/pkg/event"
-	"sigs.k8s.io/controller-runtime/pkg/predicate"
-)
-
-// ClusterCreateInfraReady returns a predicate that returns true for a create event when a cluster has Status.InfrastructureReady set as true
-// it also returns true if the resource provided is not a Cluster to allow for use with controller-runtime NewControllerManagedBy
-func ClusterCreateInfraReady(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterCreateInfraReady")
-	return predicate.Funcs{
-		CreateFunc: func(e event.CreateEvent) bool {
-			log = log.WithValues("eventType", "create")
-
-			c, ok := e.Object.(*clusterv1.Cluster)
-			if !ok {
-
-				log.V(4).Info("Expected Cluster", "type", e.Object.GetObjectKind().GroupVersionKind().String())
-				return false
-			}
-			log = log.WithValues("namespace", c.Namespace, "cluster", c.Name)
-
-			// Only need to trigger a reconcile if the Cluster.Status.InfrastructureReady is true
-			if c.Status.InfrastructureReady {
-				log.V(4).Info("Cluster infrastructure is ready, allowing further processing")
-				return true
-			}
-
-			log.V(4).Info("Cluster infrastructure is not ready, blocking further processing")
-			return false
-		},
-		UpdateFunc:  func(e event.UpdateEvent) bool { return false },
-		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
-		GenericFunc: func(e event.GenericEvent) bool { return false },
-	}
-}
-
-// ClusterCreateNotPaused returns a predicate that returns true for a create event when a cluster has Spec.Paused set as false
-// it also returns true if the resource provided is not a Cluster to allow for use with controller-runtime NewControllerManagedBy
-func ClusterCreateNotPaused(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterCreateNotPaused")
-	return predicate.Funcs{
-		CreateFunc: func(e event.CreateEvent) bool {
-			log = log.WithValues("eventType", "create")
-
-			c, ok := e.Object.(*clusterv1.Cluster)
-			if !ok {
-
-				log.V(4).Info("Expected Cluster", "type", e.Object.GetObjectKind().GroupVersionKind().String())
-				return false
-			}
-			log = log.WithValues("namespace", c.Namespace, "cluster", c.Name)
-
-			// Only need to trigger a reconcile if the Cluster.Spec.Paused is false
-			if !c.Spec.Paused {
-				log.V(4).Info("Cluster is not paused, allowing further processing")
-				return true
-			}
-
-			log.V(4).Info("Cluster is not paused, blocking further processing")
-			return false
-		},
-		UpdateFunc:  func(e event.UpdateEvent) bool { return false },
-		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
-		GenericFunc: func(e event.GenericEvent) bool { return false },
-	}
-}
-
-// ClusterUpdateInfraReady returns a predicate that returns true for an update event when a cluster has Status.InfrastructureReady changed from false to true
-// it also returns true if the resource provided is not a Cluster to allow for use with controller-runtime NewControllerManagedBy
-func ClusterUpdateInfraReady(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterUpdateInfraReady")
-	return predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			log = log.WithValues("eventType", "update")
-
-			oldCluster, ok := e.ObjectOld.(*clusterv1.Cluster)
-			if !ok {
-
-				log.V(4).Info("Expected Cluster", "type", e.ObjectOld.GetObjectKind().GroupVersionKind().String())
-				return false
-			}
-			log = log.WithValues("namespace", oldCluster.Namespace, "cluster", oldCluster.Name)
-
-			newCluster := e.ObjectNew.(*clusterv1.Cluster)
-
-			if !oldCluster.Status.InfrastructureReady && newCluster.Status.InfrastructureReady {
-				log.V(4).Info("Cluster infrastructure became ready, allowing further processing")
-				return true
-			}
-
-			log.V(4).Info("Cluster infrastructure did not become ready, blocking further processing")
-			return false
-		},
-		CreateFunc:  func(e event.CreateEvent) bool { return false },
-		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
-		GenericFunc: func(e event.GenericEvent) bool { return false },
-	}
-}
-
-// ClusterUpdateUnpaused returns a predicate that returns true for an update event when a cluster has Spec.Paused changed from true to false
-// it also returns true if the resource provided is not a Cluster to allow for use with controller-runtime NewControllerManagedBy
-func ClusterUpdateUnpaused(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterUpdateUnpaused")
-	return predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			log = log.WithValues("eventType", "update")
-
-			oldCluster, ok := e.ObjectOld.(*clusterv1.Cluster)
-			if !ok {
-
-				log.V(4).Info("Expected Cluster", "type", e.ObjectOld.GetObjectKind().GroupVersionKind().String())
-				return false
-			}
-			log = log.WithValues("namespace", oldCluster.Namespace, "cluster", oldCluster.Name)
-
-			newCluster := e.ObjectNew.(*clusterv1.Cluster)
-
-			if oldCluster.Spec.Paused && !newCluster.Spec.Paused {
-				log.V(4).Info("Cluster was unpaused, allowing further processing")
-				return true
-			}
-
-			log.V(4).Info("Cluster was not unpaused, blocking further processing")
-			return false
-		},
-		CreateFunc:  func(e event.CreateEvent) bool { return false },
-		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
-		GenericFunc: func(e event.GenericEvent) bool { return false },
-	}
-}
-
-// ClusterUnpaused returns a Predicate that returns true on Cluster creation events where Cluster.Spec.Paused is false
-// and Update events when Cluster.Spec.Paused transitions to false.
-// This implements a common requirement for many cluster-api and provider controllers (such as Cluster Infrastructure
-// controllers) to resume reconciliation when the Cluster is unpaused.
-// Example use:
-//  err := controller.Watch(
-//      &source.Kind{Type: &clusterv1.Cluster{}},
-//      &handler.EnqueueRequestsFromMapFunc{
-//          ToRequests: clusterToMachines,
-//      },
-//      predicates.ClusterUnpaused(r.Log),
-//  )
-func ClusterUnpaused(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterUnpaused")
-
-	// Use any to ensure we process either create or update events we care about
-	return Any(log, ClusterCreateNotPaused(log), ClusterUpdateUnpaused(log))
-}
-
-// ClusterUnpausedAndInfrastructureReady returns a Predicate that returns true on Cluster creation events where
-// both Cluster.Spec.Paused is false and Cluster.Status.InfrastructureReady is true and Update events when
-// either Cluster.Spec.Paused transitions to false or Cluster.Status.InfrastructureReady transitions to true.
-// This implements a common requirement for some cluster-api and provider controllers (such as Machine Infrastructure
-// controllers) to resume reconciliation when the Cluster is unpaused and when the infrastructure becomes ready.
-// Example use:
-//  err := controller.Watch(
-//      &source.Kind{Type: &clusterv1.Cluster{}},
-//      &handler.EnqueueRequestsFromMapFunc{
-//          ToRequests: clusterToMachines,
-//      },
-//      predicates.ClusterUnpausedAndInfrastructureReady(r.Log),
-//  )
-func ClusterUnpausedAndInfrastructureReady(logger logr.Logger) predicate.Funcs {
-	log := logger.WithValues("predicate", "ClusterUnpausedAndInfrastructureReady")
-
-	// Only continue processing create events if both not paused and infrastructure is ready
-	createPredicates := All(log, ClusterCreateNotPaused(log), ClusterCreateInfraReady(log))
-
-	// Process update events if either Cluster is unpaused or infrastructure becomes ready
-	updatePredicates := Any(log, ClusterUpdateUnpaused(log), ClusterUpdateInfraReady(log))
-
-	// Use any to ensure we process either create or update events we care about
-	return Any(log, createPredicates, updatePredicates)
-}
diff --git a/util/predicates/generic_predicates.go b/util/predicates/generic_predicates.go
deleted file mode 100644
index 6fac48261..000000000
--- a/util/predicates/generic_predicates.go
+++ /dev/null
@@ -1,164 +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 predicates
-
-import (
-	"strings"
-
-	"github.com/go-logr/logr"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/cluster-api/util/annotations"
-
-	"sigs.k8s.io/controller-runtime/pkg/event"
-	"sigs.k8s.io/controller-runtime/pkg/predicate"
-)
-
-// All returns a predicate that returns true only if all given predicates return true
-func All(logger logr.Logger, predicates ...predicate.Funcs) predicate.Funcs {
-	log := logger.WithValues("predicateAggregation", "All")
-	return predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			for _, p := range predicates {
-				if !p.UpdateFunc(e) {
-					log.V(4).Info("One of the provided predicates returned false, blocking further processing")
-					return false
-				}
-			}
-			log.V(4).Info("All provided predicates returned true, allowing further processing")
-			return true
-		},
-		CreateFunc: func(e event.CreateEvent) bool {
-			for _, p := range predicates {
-				if !p.CreateFunc(e) {
-					log.V(4).Info("One of the provided predicates returned false, blocking further processing")
-					return false
-				}
-			}
-			log.V(4).Info("All provided predicates returned true, allowing further processing")
-			return true
-		},
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			for _, p := range predicates {
-				if !p.DeleteFunc(e) {
-					log.V(4).Info("One of the provided predicates returned false, blocking further processing")
-					return false
-				}
-			}
-			log.V(4).Info("All provided predicates returned true, allowing further processing")
-			return true
-		},
-		GenericFunc: func(e event.GenericEvent) bool {
-			for _, p := range predicates {
-				if !p.GenericFunc(e) {
-					log.V(4).Info("One of the provided predicates returned false, blocking further processing")
-					return false
-				}
-			}
-			log.V(4).Info("All provided predicates returned true, allowing further processing")
-			return true
-		},
-	}
-}
-
-// Any returns a predicate that returns true only if any given predicate returns true
-func Any(logger logr.Logger, predicates ...predicate.Funcs) predicate.Funcs {
-	log := logger.WithValues("predicateAggregation", "Any")
-	return predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			for _, p := range predicates {
-				if p.UpdateFunc(e) {
-					log.V(4).Info("One of the provided predicates returned true, allowing further processing")
-					return true
-				}
-			}
-			log.V(4).Info("All of the provided predicates returned false, blocking further processing")
-			return false
-		},
-		CreateFunc: func(e event.CreateEvent) bool {
-			for _, p := range predicates {
-				if p.CreateFunc(e) {
-					log.V(4).Info("One of the provided predicates returned true, allowing further processing")
-					return true
-				}
-			}
-			log.V(4).Info("All of the provided predicates returned false, blocking further processing")
-			return false
-		},
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			for _, p := range predicates {
-				if p.DeleteFunc(e) {
-					log.V(4).Info("One of the provided predicates returned true, allowing further processing")
-					return true
-				}
-			}
-			log.V(4).Info("All of the provided predicates returned false, blocking further processing")
-			return false
-		},
-		GenericFunc: func(e event.GenericEvent) bool {
-			for _, p := range predicates {
-				if p.GenericFunc(e) {
-					log.V(4).Info("One of the provided predicates returned true, allowing further processing")
-					return true
-				}
-			}
-			log.V(4).Info("All of the provided predicates returned false, blocking further processing")
-			return false
-		},
-	}
-}
-
-// ResourceNotPaused returns a Predicate that returns true only if the provided resource does not contain the
-// paused annotation.
-// This implements a common requirement for all cluster-api and provider controllers skip reconciliation when the paused
-// annotation is present for a resource.
-// Example use:
-//	func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-//		controller, err := ctrl.NewControllerManagedBy(mgr).
-//			For(&v1.MyType{}).
-//			WithOptions(options).
-//			WithEventFilter(util.ResourceNotPaused(r.Log)).
-//			Build(r)
-//		return err
-//	}
-func ResourceNotPaused(logger logr.Logger) predicate.Funcs {
-	return predicate.Funcs{
-		UpdateFunc: func(e event.UpdateEvent) bool {
-			return processIfNotPaused(logger.WithValues("predicate", "updateEvent"), e.ObjectNew, e.MetaNew)
-		},
-		CreateFunc: func(e event.CreateEvent) bool {
-			return processIfNotPaused(logger.WithValues("predicate", "createEvent"), e.Object, e.Meta)
-		},
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			return processIfNotPaused(logger.WithValues("predicate", "deleteEvent"), e.Object, e.Meta)
-		},
-		GenericFunc: func(e event.GenericEvent) bool {
-			return processIfNotPaused(logger.WithValues("predicate", "genericEvent"), e.Object, e.Meta)
-		},
-	}
-}
-
-func processIfNotPaused(logger logr.Logger, obj runtime.Object, meta v1.Object) bool {
-	kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
-	log := logger.WithValues("namespace", meta.GetNamespace(), kind, meta.GetName())
-	if annotations.HasPausedAnnotation(meta) {
-		log.V(4).Info("Resource is paused, will not attempt to map resource")
-		return false
-	}
-	log.V(4).Info("Resource is not paused, will attempt to map resource")
-	return true
-}
diff --git a/util/secret/consts.go b/util/secret/consts.go
index 98cbf6aac..cf3463083 100644
--- a/util/secret/consts.go
+++ b/util/secret/consts.go
@@ -47,8 +47,3 @@ const (
 	// APIServerEtcdClient is the secret name of user-supplied secret containing the apiserver-etcd-client key/cert
 	APIServerEtcdClient Purpose = "apiserver-etcd-client"
 )
-
-var (
-	// allSecretPurposes defines a lists with all the secret suffix used by Cluster API
-	allSecretPurposes = []Purpose{Kubeconfig, ClusterCA, EtcdCA, ServiceAccount, FrontProxyCA, APIServerEtcdClient}
-)
diff --git a/util/secret/secret.go b/util/secret/secret.go
index 5af9fd9f5..e0d775c5f 100644
--- a/util/secret/secret.go
+++ b/util/secret/secret.go
@@ -19,22 +19,20 @@ package secret
 import (
 	"context"
 	"fmt"
-	"strings"
 
-	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 // Get retrieves the specified Secret (if any) from the given
 // cluster name and namespace.
-func Get(ctx context.Context, c client.Reader, cluster client.ObjectKey, purpose Purpose) (*corev1.Secret, error) {
+func Get(ctx context.Context, c client.Client, cluster client.ObjectKey, purpose Purpose) (*corev1.Secret, error) {
 	return GetFromNamespacedName(ctx, c, cluster, purpose)
 }
 
 // GetFromNamespacedName retrieves the specified Secret (if any) from the given
 // cluster name and namespace.
-func GetFromNamespacedName(ctx context.Context, c client.Reader, clusterName client.ObjectKey, purpose Purpose) (*corev1.Secret, error) {
+func GetFromNamespacedName(ctx context.Context, c client.Client, clusterName client.ObjectKey, purpose Purpose) (*corev1.Secret, error) {
 	secret := &corev1.Secret{}
 	secretKey := client.ObjectKey{
 		Namespace: clusterName.Namespace,
@@ -52,20 +50,3 @@ func GetFromNamespacedName(ctx context.Context, c client.Reader, clusterName cli
 func Name(cluster string, suffix Purpose) string {
 	return fmt.Sprintf("%s-%s", cluster, suffix)
 }
-
-// ParseSecretName return the cluster name and the suffix Purpose in name is a valid cluster secrets,
-// otherwise it return error.
-func ParseSecretName(name string) (string, Purpose, error) {
-	separatorPos := strings.LastIndex(name, "-")
-	if separatorPos == -1 {
-		return "", "", errors.Errorf("%q is not a valid cluster secret name. The purpose suffix is missing", name)
-	}
-	clusterName := name[:separatorPos]
-	purposeSuffix := Purpose(name[separatorPos+1:])
-	for _, purpose := range allSecretPurposes {
-		if purpose == purposeSuffix {
-			return clusterName, purposeSuffix, nil
-		}
-	}
-	return "", "", errors.Errorf("%q is not a valid cluster secret name. Invalid purpose suffix", name)
-}
diff --git a/util/util.go b/util/util.go
index e455b8af3..efce5c790 100644
--- a/util/util.go
+++ b/util/util.go
@@ -37,16 +37,15 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/version"
-	"k8s.io/klog/klogr"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/container"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 )
@@ -434,15 +433,23 @@ func HasOwner(refList []metav1.OwnerReference, apiVersion string, kinds []string
 	return false
 }
 
-var (
-	// IsPaused returns true if the Cluster is paused or the object has the `paused` annotation.
-	// Deprecated: use util/annotations/IsPaused instead
-	IsPaused = annotations.IsPaused
+// IsPaused returns true if the Cluster is paused or the object has the `paused` annotation.
+func IsPaused(cluster *clusterv1.Cluster, o metav1.Object) bool {
+	if cluster.Spec.Paused {
+		return true
+	}
+	return HasPausedAnnotation(o)
+}
 
-	// HasPausedAnnotation returns true if the object has the `paused` annotation.
-	// Deprecated: use util/annotations/HasPausedAnnotation instead
-	HasPausedAnnotation = annotations.HasPausedAnnotation
-)
+// HasPausedAnnotation returns true if the object has the `paused` annotation.
+func HasPausedAnnotation(o metav1.Object) bool {
+	annotations := o.GetAnnotations()
+	if annotations == nil {
+		return false
+	}
+	_, ok := annotations[clusterv1.PausedAnnotation]
+	return ok
+}
 
 // GetCRDWithContract retrieves a list of CustomResourceDefinitions from using controller-runtime Client,
 // filtering with the `contract` label passed in.
@@ -498,16 +505,19 @@ func (o MachinesByCreationTimestamp) Less(i, j int) bool {
 // WatchOnClusterPaused adds a conditional watch to the controlled given as input
 // that sends watch notifications on any create or delete, and only updates
 // that toggle Cluster.Spec.Cluster.
-// Deprecated: Instead add the Watch directly and use predicates.ClusterUnpaused or
-// predicates.ClusterUnpausedAndInfrastructureReady depending on your use case.
 func WatchOnClusterPaused(c controller.Controller, mapFunc handler.Mapper) error {
-	log := klogr.New().WithName("WatchOnClusterPaused")
 	return c.Watch(
 		&source.Kind{Type: &clusterv1.Cluster{}},
 		&handler.EnqueueRequestsFromMapFunc{
 			ToRequests: mapFunc,
 		},
-		predicates.ClusterUnpaused(log),
+		predicate.Funcs{
+			UpdateFunc: func(e event.UpdateEvent) bool {
+				oldCluster := e.ObjectOld.(*clusterv1.Cluster)
+				newCluster := e.ObjectNew.(*clusterv1.Cluster)
+				return oldCluster.Spec.Paused && !newCluster.Spec.Paused
+			},
+		},
 	)
 }
 


Test patch:
diff --git a/cmd/clusterctl/client/cluster/objectgraph_test.go b/cmd/clusterctl/client/cluster/objectgraph_test.go
index 1fbfb30c1..cd1e6b13e 100644
--- a/cmd/clusterctl/client/cluster/objectgraph_test.go
+++ b/cmd/clusterctl/client/cluster/objectgraph_test.go
@@ -1078,18 +1078,6 @@ func Test_objectGraph_setSoftOwnership(t *testing.T) {
 				"/v1, Kind=Secret, ns1/foo-kubeconfig": {}, // the kubeconfig secret has explicit OwnerRef to the cluster, so it should NOT be identified as a soft ownership
 			},
 		},
-		{
-			name: "A cluster with a soft owned secret (cluster name with - in the middle)",
-			fields: fields{
-				objs: test.NewFakeCluster("ns1", "foo-bar").Objs(),
-			},
-			wantSecrets: map[string][]string{ // wantSecrets is a map[node UID] --> list of soft owner UIDs
-				"/v1, Kind=Secret, ns1/foo-bar-ca": { // the ca secret has no explicit OwnerRef to the cluster, so it should be identified as a soft ownership
-					"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/foo-bar",
-				},
-				"/v1, Kind=Secret, ns1/foo-bar-kubeconfig": {}, // the kubeconfig secret has explicit OwnerRef to the cluster, so it should NOT be identified as a soft ownership
-			},
-		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/cmd/clusterctl/client/config/reader_viper_test.go b/cmd/clusterctl/client/config/reader_viper_test.go
index 5f9f037aa..52db5d116 100644
--- a/cmd/clusterctl/client/config/reader_viper_test.go
+++ b/cmd/clusterctl/client/config/reader_viper_test.go
@@ -17,11 +17,9 @@ limitations under the License.
 package config
 
 import (
-	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"strings"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -40,10 +38,10 @@ func Test_viperReader_Init(t *testing.T) {
 	g.Expect(err).NotTo(HaveOccurred())
 	defer os.RemoveAll(dir)
 
-	configFile := filepath.Join(dir, "clusterctl.yaml")
+	configFile := filepath.Join(dir, ".clusterctl.yaml")
 	g.Expect(ioutil.WriteFile(configFile, []byte("bar: bar"), 0640)).To(Succeed())
 
-	configFileBadContents := filepath.Join(dir, "clusterctl-bad.yaml")
+	configFileBadContents := filepath.Join(dir, ".clusterctl-bad.yaml")
 	g.Expect(ioutil.WriteFile(configFileBadContents, []byte("bad-contents"), 0640)).To(Succeed())
 
 	tests := []struct {
@@ -101,7 +99,7 @@ func Test_viperReader_Get(t *testing.T) {
 
 	os.Setenv("FOO", "foo")
 
-	configFile := filepath.Join(dir, "clusterctl.yaml")
+	configFile := filepath.Join(dir, ".clusterctl.yaml")
 	g.Expect(ioutil.WriteFile(configFile, []byte("bar: bar"), 0640)).To(Succeed())
 
 	type args struct {
@@ -142,7 +140,7 @@ func Test_viperReader_Get(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			gs := NewWithT(t)
 
-			v := newViperReader(InjectConfigPaths([]string{dir}))
+			v := &viperReader{}
 
 			gs.Expect(v.Init(configFile)).To(Succeed())
 
@@ -158,22 +156,6 @@ func Test_viperReader_Get(t *testing.T) {
 	}
 }
 
-func Test_viperReader_GetWithoutDefaultConfig(t *testing.T) {
-	g := NewWithT(t)
-	dir, err := ioutil.TempDir("", "clusterctl")
-	g.Expect(err).NotTo(HaveOccurred())
-	defer os.RemoveAll(dir)
-
-	os.Setenv("FOO_FOO", "bar")
-
-	v := newViperReader(InjectConfigPaths([]string{dir}))
-	g.Expect(v.Init("")).To(Succeed())
-
-	got, err := v.Get("FOO_FOO")
-	g.Expect(err).NotTo(HaveOccurred())
-	g.Expect(got).To(Equal("bar"))
-}
-
 func Test_viperReader_Set(t *testing.T) {
 	g := NewWithT(t)
 
@@ -183,7 +165,7 @@ func Test_viperReader_Set(t *testing.T) {
 
 	os.Setenv("FOO", "foo")
 
-	configFile := filepath.Join(dir, "clusterctl.yaml")
+	configFile := filepath.Join(dir, ".clusterctl.yaml")
 
 	g.Expect(ioutil.WriteFile(configFile, []byte("bar: bar"), 0640)).To(Succeed())
 
@@ -221,48 +203,3 @@ func Test_viperReader_Set(t *testing.T) {
 		})
 	}
 }
-
-func Test_viperReader_checkDefaultConfig(t *testing.T) {
-	g := NewWithT(t)
-	dir, err := ioutil.TempDir("", "clusterctl")
-	g.Expect(err).NotTo(HaveOccurred())
-	defer os.RemoveAll(dir)
-	dir = strings.TrimSuffix(dir, "/")
-
-	configFile := filepath.Join(dir, "clusterctl.yaml")
-	g.Expect(ioutil.WriteFile(configFile, []byte("bar: bar"), 0640)).To(Succeed())
-
-	type fields struct {
-		configPaths []string
-	}
-	tests := []struct {
-		name   string
-		fields fields
-		want   bool
-	}{
-		{
-			name: "tmp path without final /",
-			fields: fields{
-				configPaths: []string{dir},
-			},
-			want: true,
-		},
-		{
-			name: "tmp path with final /",
-			fields: fields{
-				configPaths: []string{fmt.Sprintf("%s/", dir)},
-			},
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gs := NewWithT(t)
-
-			v := &viperReader{
-				configPaths: tt.fields.configPaths,
-			}
-			gs.Expect(v.checkDefaultConfig()).To(Equal(tt.want))
-		})
-	}
-}
diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go
index ca25d3302..ab1b27706 100644
--- a/controllers/cluster_controller_test.go
+++ b/controllers/cluster_controller_test.go
@@ -40,7 +40,6 @@ import (
 
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	capierrors "sigs.k8s.io/cluster-api/errors"
-	"sigs.k8s.io/cluster-api/util/kubeconfig"
 	"sigs.k8s.io/cluster-api/util/patch"
 )
 
@@ -56,16 +55,16 @@ var _ = Describe("Cluster Reconciler", func() {
 		}
 
 		// Create the Cluster object and expect the Reconcile and Deployment to be created
-		Expect(k8sClient.Create(ctx, instance)).ToNot(HaveOccurred())
+		Expect(testEnv.Create(ctx, instance)).ToNot(HaveOccurred())
 		key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
 		defer func() {
-			err := k8sClient.Delete(ctx, instance)
+			err := testEnv.Delete(ctx, instance)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
 		// Make sure the Cluster exists.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return false
 			}
 			return len(instance.Finalizers) > 0
@@ -80,16 +79,16 @@ var _ = Describe("Cluster Reconciler", func() {
 				Namespace:    "default",
 			},
 		}
-		Expect(k8sClient.Create(ctx, cluster)).To(BeNil())
+		Expect(testEnv.Create(ctx, cluster)).To(BeNil())
 		key := client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, cluster)
+			err := testEnv.Delete(ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
 		// Wait for reconciliation to happen.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return len(cluster.Finalizers) > 0
@@ -97,7 +96,7 @@ var _ = Describe("Cluster Reconciler", func() {
 
 		// Patch
 		Eventually(func() bool {
-			ph, err := patch.NewHelper(cluster, k8sClient)
+			ph, err := patch.NewHelper(cluster, testEnv)
 			Expect(err).ShouldNot(HaveOccurred())
 			cluster.Spec.InfrastructureRef = &v1.ObjectReference{Name: "test"}
 			cluster.Spec.ControlPlaneRef = &v1.ObjectReference{Name: "test-too"}
@@ -108,7 +107,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// Assertions
 		Eventually(func() bool {
 			instance := &clusterv1.Cluster{}
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return false
 			}
 			return instance.Spec.InfrastructureRef != nil &&
@@ -124,16 +123,16 @@ var _ = Describe("Cluster Reconciler", func() {
 				Namespace:    "default",
 			},
 		}
-		Expect(k8sClient.Create(ctx, cluster)).To(BeNil())
+		Expect(testEnv.Create(ctx, cluster)).To(BeNil())
 		key := client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, cluster)
+			err := testEnv.Delete(ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
 		// Wait for reconciliation to happen.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return len(cluster.Finalizers) > 0
@@ -141,7 +140,7 @@ var _ = Describe("Cluster Reconciler", func() {
 
 		// Patch
 		Eventually(func() bool {
-			ph, err := patch.NewHelper(cluster, k8sClient)
+			ph, err := patch.NewHelper(cluster, testEnv)
 			Expect(err).ShouldNot(HaveOccurred())
 			cluster.Status.InfrastructureReady = true
 			Expect(ph.Patch(ctx, cluster)).ShouldNot(HaveOccurred())
@@ -151,7 +150,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// Assertions
 		Eventually(func() bool {
 			instance := &clusterv1.Cluster{}
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return false
 			}
 			return instance.Status.InfrastructureReady
@@ -166,16 +165,16 @@ var _ = Describe("Cluster Reconciler", func() {
 				Namespace:    "default",
 			},
 		}
-		Expect(k8sClient.Create(ctx, cluster)).To(BeNil())
+		Expect(testEnv.Create(ctx, cluster)).To(BeNil())
 		key := client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, cluster)
+			err := testEnv.Delete(ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
 		// Wait for reconciliation to happen.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return len(cluster.Finalizers) > 0
@@ -183,7 +182,7 @@ var _ = Describe("Cluster Reconciler", func() {
 
 		// Patch
 		Eventually(func() bool {
-			ph, err := patch.NewHelper(cluster, k8sClient)
+			ph, err := patch.NewHelper(cluster, testEnv)
 			Expect(err).ShouldNot(HaveOccurred())
 			cluster.Status.InfrastructureReady = true
 			cluster.Spec.InfrastructureRef = &v1.ObjectReference{Name: "test"}
@@ -194,7 +193,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// Assertions
 		Eventually(func() bool {
 			instance := &clusterv1.Cluster{}
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return false
 			}
 			return instance.Status.InfrastructureReady &&
@@ -211,16 +210,16 @@ var _ = Describe("Cluster Reconciler", func() {
 				Namespace:    "default",
 			},
 		}
-		Expect(k8sClient.Create(ctx, cluster)).To(BeNil())
+		Expect(testEnv.Create(ctx, cluster)).To(BeNil())
 		key := client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, cluster)
+			err := testEnv.Delete(ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
 		// Wait for reconciliation to happen.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return len(cluster.Finalizers) > 0
@@ -228,7 +227,7 @@ var _ = Describe("Cluster Reconciler", func() {
 
 		// Patch
 		Eventually(func() bool {
-			ph, err := patch.NewHelper(cluster, k8sClient)
+			ph, err := patch.NewHelper(cluster, testEnv)
 			Expect(err).ShouldNot(HaveOccurred())
 			cluster.SetFinalizers([]string{})
 			Expect(ph.Patch(ctx, cluster)).ShouldNot(HaveOccurred())
@@ -240,7 +239,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// Assertions
 		Eventually(func() []string {
 			instance := &clusterv1.Cluster{}
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return []string{"not-empty"}
 			}
 			return instance.Finalizers
@@ -255,17 +254,17 @@ var _ = Describe("Cluster Reconciler", func() {
 			},
 		}
 
-		Expect(k8sClient.Create(ctx, cluster)).To(BeNil())
+		Expect(testEnv.Create(ctx, cluster)).To(BeNil())
 		key := client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, cluster)
+			err := testEnv.Delete(ctx, cluster)
 			Expect(err).NotTo(HaveOccurred())
 		}()
-		Expect(kubeconfig.CreateEnvTestSecret(k8sClient, cfg, cluster)).To(Succeed())
+		Expect(testEnv.CreateKubeconfigSecret(cluster)).To(Succeed())
 
 		// Wait for reconciliation to happen.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return len(cluster.Finalizers) > 0
@@ -282,7 +281,7 @@ var _ = Describe("Cluster Reconciler", func() {
 			},
 		}
 
-		Expect(k8sClient.Create(ctx, node)).To(Succeed())
+		Expect(testEnv.Create(ctx, node)).To(Succeed())
 
 		machine := &clusterv1.Machine{
 			ObjectMeta: metav1.ObjectMeta{
@@ -301,10 +300,10 @@ var _ = Describe("Cluster Reconciler", func() {
 			},
 		}
 
-		Expect(k8sClient.Create(ctx, machine)).To(BeNil())
+		Expect(testEnv.Create(ctx, machine)).To(BeNil())
 		key = client.ObjectKey{Name: machine.Name, Namespace: machine.Namespace}
 		defer func() {
-			err := k8sClient.Delete(ctx, machine)
+			err := testEnv.Delete(ctx, machine)
 			Expect(err).NotTo(HaveOccurred())
 		}()
 
@@ -316,7 +315,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// we continue to see test timeouts here, that will likely point to something else being the problem, but
 		// I've yet to determine any other possibility for the test flakes.
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, machine); err != nil {
+			if err := testEnv.Get(ctx, key, machine); err != nil {
 				return false
 			}
 			return len(machine.Finalizers) > 0
@@ -325,7 +324,7 @@ var _ = Describe("Cluster Reconciler", func() {
 		// Assertion
 		key = client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}
 		Eventually(func() bool {
-			if err := k8sClient.Get(ctx, key, cluster); err != nil {
+			if err := testEnv.Get(ctx, key, cluster); err != nil {
 				return false
 			}
 			return cluster.Status.ControlPlaneInitialized
diff --git a/controllers/machine_controller_phases_test.go b/controllers/machine_controller_phases_test.go
index 42c7c8554..5ad703118 100644
--- a/controllers/machine_controller_phases_test.go
+++ b/controllers/machine_controller_phases_test.go
@@ -105,7 +105,7 @@ var _ = Describe("Reconcile Machine Phases", func() {
 	}
 
 	BeforeEach(func() {
-		defaultKubeconfigSecret = kubeconfig.GenerateSecret(defaultCluster, kubeconfig.FromEnvTestConfig(cfg, defaultCluster))
+		defaultKubeconfigSecret = kubeconfig.GenerateSecret(defaultCluster, kubeconfig.FromEnvTestConfig(testEnv.Config, defaultCluster))
 	})
 
 	It("Should set OwnerReference and cluster name label on external objects", func() {
diff --git a/controllers/machine_controller_test.go b/controllers/machine_controller_test.go
index ee4d1574b..04eaa017c 100644
--- a/controllers/machine_controller_test.go
+++ b/controllers/machine_controller_test.go
@@ -19,7 +19,6 @@ package controllers
 import (
 	"context"
 	"testing"
-	"time"
 
 	. "github.com/onsi/gomega"
 
@@ -338,7 +337,7 @@ func TestReconcileRequest(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "created",
 					Namespace:  "default",
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName: "test-cluster",
@@ -365,7 +364,7 @@ func TestReconcileRequest(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "updated",
 					Namespace:  "default",
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName: "test-cluster",
@@ -395,7 +394,7 @@ func TestReconcileRequest(t *testing.T) {
 					Labels: map[string]string{
 						clusterv1.MachineControlPlaneLabelName: "",
 					},
-					Finalizers:        []string{clusterv1.MachineFinalizer},
+					Finalizers:        []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 					DeletionTimestamp: &time,
 				},
 				Spec: clusterv1.MachineSpec{
@@ -575,7 +574,7 @@ func TestRemoveMachineFinalizerAfterDeleteReconcile(t *testing.T) {
 		ObjectMeta: metav1.ObjectMeta{
 			Name:              "delete123",
 			Namespace:         "default",
-			Finalizers:        []string{clusterv1.MachineFinalizer},
+			Finalizers:        []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 			DeletionTimestamp: &dt,
 		},
 		Spec: clusterv1.MachineSpec{
@@ -597,9 +596,8 @@ func TestRemoveMachineFinalizerAfterDeleteReconcile(t *testing.T) {
 	_, err := mr.Reconcile(reconcile.Request{NamespacedName: key})
 	g.Expect(err).ToNot(HaveOccurred())
 
-	var actual clusterv1.Machine
-	g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
-	g.Expect(actual.ObjectMeta.Finalizers).To(BeEmpty())
+	g.Expect(mr.Client.Get(ctx, key, m)).To(Succeed())
+	g.Expect(m.ObjectMeta.Finalizers).To(Equal([]string{metav1.FinalizerDeleteDependents}))
 }
 
 func TestReconcileMetrics(t *testing.T) {
@@ -804,7 +802,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "created",
 					Namespace:  "default",
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
@@ -822,7 +820,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "created",
 					Namespace:  "default",
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
@@ -838,7 +836,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 			expectedError: errNoControlPlaneNodes,
 		},
 		{
-			name:    "is last control plane member",
+			name:    "is last control plane members",
 			cluster: &clusterv1.Cluster{},
 			machine: &clusterv1.Machine{
 				ObjectMeta: metav1.ObjectMeta{
@@ -848,8 +846,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 						clusterv1.ClusterLabelName:             "test",
 						clusterv1.MachineControlPlaneLabelName: "",
 					},
-					Finalizers:        []string{clusterv1.MachineFinalizer},
-					DeletionTimestamp: &metav1.Time{Time: time.Now().UTC()},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
@@ -862,7 +859,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 					},
 				},
 			},
-			expectedError: errNoControlPlaneNodes,
+			expectedError: errLastControlPlaneNode,
 		},
 		{
 			name:    "has nodeRef and control plane is healthy",
@@ -874,7 +871,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 					Labels: map[string]string{
 						clusterv1.ClusterLabelName: "test",
 					},
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
@@ -912,7 +909,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 					Labels: map[string]string{
 						clusterv1.ClusterLabelName: "test",
 					},
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
@@ -932,7 +929,7 @@ func TestIsDeleteNodeAllowed(t *testing.T) {
 					Labels: map[string]string{
 						clusterv1.ClusterLabelName: "test",
 					},
-					Finalizers: []string{clusterv1.MachineFinalizer},
+					Finalizers: []string{clusterv1.MachineFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: clusterv1.MachineSpec{
 					ClusterName:       "test-cluster",
diff --git a/controllers/machinedeployment_controller_test.go b/controllers/machinedeployment_controller_test.go
index b73458c07..4df168676 100644
--- a/controllers/machinedeployment_controller_test.go
+++ b/controllers/machinedeployment_controller_test.go
@@ -37,7 +37,6 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/controllers/external"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/kubeconfig"
 )
 
 var _ reconcile.Reconciler = &MachineDeploymentReconciler{}
@@ -48,18 +47,18 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 
 	BeforeEach(func() {
 		By("Creating the namespace")
-		Expect(k8sClient.Create(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Create(ctx, namespace)).To(Succeed())
 		By("Creating the Cluster")
-		Expect(k8sClient.Create(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Create(ctx, testCluster)).To(Succeed())
 		By("Creating the Cluster Kubeconfig Secret")
-		Expect(kubeconfig.CreateEnvTestSecret(k8sClient, cfg, testCluster)).To(Succeed())
+		Expect(testEnv.CreateKubeconfigSecret(testCluster)).To(Succeed())
 	})
 
 	AfterEach(func() {
 		By("Deleting the Cluster")
-		Expect(k8sClient.Delete(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Delete(ctx, testCluster)).To(Succeed())
 		By("Deleting the namespace")
-		Expect(k8sClient.Delete(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Delete(ctx, namespace)).To(Succeed())
 	})
 
 	It("Should reconcile a MachineDeployment", func() {
@@ -139,20 +138,20 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		infraTmpl.SetName("md-template")
 		infraTmpl.SetNamespace(namespace.Name)
 		By("Creating the infrastructure template")
-		Expect(k8sClient.Create(ctx, infraTmpl)).To(Succeed())
+		Expect(testEnv.Create(ctx, infraTmpl)).To(Succeed())
 
 		// Create the MachineDeployment object and expect Reconcile to be called.
 		By("Creating the MachineDeployment")
-		Expect(k8sClient.Create(ctx, deployment)).To(Succeed())
+		Expect(testEnv.Create(ctx, deployment)).To(Succeed())
 		defer func() {
 			By("Deleting the MachineDeployment")
-			Expect(k8sClient.Delete(ctx, deployment)).To(Succeed())
+			Expect(testEnv.Delete(ctx, deployment)).To(Succeed())
 		}()
 
 		By("Verifying the MachineDeployment has a cluster label and ownerRef")
 		Eventually(func() bool {
 			key := client.ObjectKey{Name: deployment.Name, Namespace: deployment.Namespace}
-			if err := k8sClient.Get(ctx, key, deployment); err != nil {
+			if err := testEnv.Get(ctx, key, deployment); err != nil {
 				return false
 			}
 			if len(deployment.Labels) == 0 || deployment.Labels[clusterv1.ClusterLabelName] != testCluster.Name {
@@ -168,7 +167,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		By("Verifying the MachineSet was created")
 		machineSets := &clusterv1.MachineSetList{}
 		Eventually(func() int {
-			if err := k8sClient.List(ctx, machineSets, msListOpts...); err != nil {
+			if err := testEnv.List(ctx, machineSets, msListOpts...); err != nil {
 				return -1
 			}
 			return len(machineSets.Items)
@@ -176,7 +175,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 
 		By("Verifying the linked infrastructure template has a cluster owner reference")
 		Eventually(func() bool {
-			obj, err := external.Get(ctx, k8sClient, &deployment.Spec.Template.Spec.InfrastructureRef, deployment.Namespace)
+			obj, err := external.Get(ctx, testEnv, &deployment.Spec.Template.Spec.InfrastructureRef, deployment.Namespace)
 			if err != nil {
 				return false
 			}
@@ -194,7 +193,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		By("Verify expected number of machines are created")
 		machines := &clusterv1.MachineList{}
 		Eventually(func() int {
-			if err := k8sClient.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
+			if err := testEnv.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
 				return -1
 			}
 			return len(machines.Items)
@@ -214,9 +213,9 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		// Delete firstMachineSet and expect Reconcile to be called to replace it.
 		//
 		By("Deleting the initial MachineSet")
-		Expect(k8sClient.Delete(ctx, &firstMachineSet)).To(Succeed())
+		Expect(testEnv.Delete(ctx, &firstMachineSet)).To(Succeed())
 		Eventually(func() bool {
-			if err := k8sClient.List(ctx, machineSets, msListOpts...); err != nil {
+			if err := testEnv.List(ctx, machineSets, msListOpts...); err != nil {
 				return false
 			}
 			for _, ms := range machineSets.Items {
@@ -233,10 +232,10 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		secondMachineSet := machineSets.Items[0]
 		By("Scaling the MachineDeployment to 3 replicas")
 		modifyFunc := func(d *clusterv1.MachineDeployment) { d.Spec.Replicas = pointer.Int32Ptr(3) }
-		Expect(updateMachineDeployment(k8sClient, deployment, modifyFunc)).To(Succeed())
+		Expect(updateMachineDeployment(testEnv, deployment, modifyFunc)).To(Succeed())
 		Eventually(func() int {
 			key := client.ObjectKey{Name: secondMachineSet.Name, Namespace: secondMachineSet.Namespace}
-			if err := k8sClient.Get(ctx, key, &secondMachineSet); err != nil {
+			if err := testEnv.Get(ctx, key, &secondMachineSet); err != nil {
 				return -1
 			}
 			return int(*secondMachineSet.Spec.Replicas)
@@ -247,9 +246,9 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		//
 		By("Setting a label on the MachineDeployment")
 		modifyFunc = func(d *clusterv1.MachineDeployment) { d.Spec.Template.Labels["updated"] = "true" }
-		Expect(updateMachineDeployment(k8sClient, deployment, modifyFunc)).To(Succeed())
+		Expect(updateMachineDeployment(testEnv, deployment, modifyFunc)).To(Succeed())
 		Eventually(func() int {
-			if err := k8sClient.List(ctx, machineSets, msListOpts...); err != nil {
+			if err := testEnv.List(ctx, machineSets, msListOpts...); err != nil {
 				return -1
 			}
 			return len(machineSets.Items)
@@ -258,7 +257,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		// Verify that all the MachineSets have the expected OwnerRef.
 		By("Verifying MachineSet owner references")
 		Eventually(func() bool {
-			if err := k8sClient.List(ctx, machineSets, msListOpts...); err != nil {
+			if err := testEnv.List(ctx, machineSets, msListOpts...); err != nil {
 				return false
 			}
 			for i := 0; i < len(machineSets.Items); i++ {
@@ -286,7 +285,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 			// Set the all non-deleted machines as ready with a NodeRef, so the MachineSet controller can proceed
 			// to properly set AvailableReplicas.
 			foundMachines := &clusterv1.MachineList{}
-			Expect(k8sClient.List(ctx, foundMachines, client.InNamespace(namespace.Name))).To(Succeed())
+			Expect(testEnv.List(ctx, foundMachines, client.InNamespace(namespace.Name))).To(Succeed())
 			for i := 0; i < len(foundMachines.Items); i++ {
 				m := foundMachines.Items[i]
 				// Skip over deleted Machines
@@ -301,7 +300,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 				fakeMachineNodeRef(&m)
 			}
 
-			if err := k8sClient.List(ctx, machineSets, msListOpts...); err != nil {
+			if err := testEnv.List(ctx, machineSets, msListOpts...); err != nil {
 				return -1
 			}
 			return len(machineSets.Items)
@@ -325,12 +324,12 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 			d.Spec.Selector.MatchLabels = newLabels
 			d.Spec.Template.Labels = newLabels
 		}
-		Expect(updateMachineDeployment(k8sClient, deployment, modifyFunc)).To(Succeed())
+		Expect(updateMachineDeployment(testEnv, deployment, modifyFunc)).To(Succeed())
 
 		By("Verifying if a new MachineSet with updated labels are created")
 		Eventually(func() int {
 			listOpts := client.MatchingLabels(newLabels)
-			if err := k8sClient.List(ctx, machineSets, listOpts); err != nil {
+			if err := testEnv.List(ctx, machineSets, listOpts); err != nil {
 				return -1
 			}
 			return len(machineSets.Items)
@@ -342,7 +341,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 			// Set the all non-deleted machines as ready with a NodeRef, so the MachineSet controller can proceed
 			// to properly set AvailableReplicas.
 			foundMachines := &clusterv1.MachineList{}
-			Expect(k8sClient.List(ctx, foundMachines, client.InNamespace(namespace.Name))).To(Succeed())
+			Expect(testEnv.List(ctx, foundMachines, client.InNamespace(namespace.Name))).To(Succeed())
 			for i := 0; i < len(foundMachines.Items); i++ {
 				m := foundMachines.Items[i]
 				if !m.DeletionTimestamp.IsZero() {
@@ -357,7 +356,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 			}
 
 			listOpts := client.MatchingLabels(newLabels)
-			if err := k8sClient.List(ctx, machineSets, listOpts); err != nil {
+			if err := testEnv.List(ctx, machineSets, listOpts); err != nil {
 				return false
 			}
 			return machineSets.Items[0].Status.Replicas == *deployment.Spec.Replicas
@@ -366,7 +365,7 @@ var _ = Describe("MachineDeployment Reconciler", func() {
 		By("Verifying MachineSets with old labels are deleted")
 		Eventually(func() int {
 			listOpts := client.MatchingLabels(oldLabels)
-			if err := k8sClient.List(ctx, machineSets, listOpts); err != nil {
+			if err := testEnv.List(ctx, machineSets, listOpts); err != nil {
 				return -1
 			}
 
diff --git a/controllers/machinehealthcheck_controller_test.go b/controllers/machinehealthcheck_controller_test.go
index e958bd35d..dcda73fc8 100644
--- a/controllers/machinehealthcheck_controller_test.go
+++ b/controllers/machinehealthcheck_controller_test.go
@@ -17,7 +17,6 @@ package controllers
 
 import (
 	"context"
-	"path/filepath"
 	"testing"
 	"time"
 
@@ -32,18 +31,14 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
-	"k8s.io/client-go/kubernetes/scheme"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/controllers/external"
 	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha3"
+	"sigs.k8s.io/cluster-api/test/helpers"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/kubeconfig"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
-	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
-	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
@@ -62,36 +57,36 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 		testCluster = &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: clusterName}}
 
 		By("Ensuring the namespace exists")
-		Expect(k8sClient.Create(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Create(ctx, namespace)).To(Succeed())
 		namespaceName = namespace.Name
 
 		By("Creating the Cluster")
 		testCluster.Namespace = namespaceName
-		Expect(k8sClient.Create(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Create(ctx, testCluster)).To(Succeed())
 
 		By("Creating the remote Cluster kubeconfig")
-		Expect(kubeconfig.CreateEnvTestSecret(k8sClient, cfg, testCluster)).To(Succeed())
+		Expect(testEnv.CreateKubeconfigSecret(testCluster)).To(Succeed())
 	})
 
 	AfterEach(func() {
 		By("Deleting any Nodes")
-		Expect(cleanupTestNodes(ctx, k8sClient)).To(Succeed())
+		Expect(cleanupTestNodes(ctx, testEnv)).To(Succeed())
 		By("Deleting any Machines")
-		Expect(cleanupTestMachines(ctx, k8sClient)).To(Succeed())
+		Expect(cleanupTestMachines(ctx, testEnv)).To(Succeed())
 		By("Deleting any MachineHealthChecks")
-		Expect(cleanupTestMachineHealthChecks(ctx, k8sClient)).To(Succeed())
+		Expect(cleanupTestMachineHealthChecks(ctx, testEnv)).To(Succeed())
 		By("Deleting the Cluster")
-		Expect(k8sClient.Delete(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Delete(ctx, testCluster)).To(Succeed())
 		By("Deleting the remote Cluster kubeconfig")
 		remoteClusterKubeconfig := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: namespaceName, Name: clusterKubeconfigName}}
-		Expect(k8sClient.Delete(ctx, remoteClusterKubeconfig)).To(Succeed())
+		Expect(testEnv.Delete(ctx, remoteClusterKubeconfig)).To(Succeed())
 		By("Deleting the Namespace")
-		Expect(k8sClient.Delete(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Delete(ctx, namespace)).To(Succeed())
 
 		// Ensure the cluster is actually gone before moving on
 		Eventually(func() error {
 			c := &clusterv1.Cluster{}
-			err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: clusterName}, c)
+			err := testEnv.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: clusterName}, c)
 			if err != nil && apierrors.IsNotFound(err) {
 				return nil
 			} else if err != nil {
@@ -127,11 +122,11 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				},
 			}
 			mhcToCreate.Default()
-			Expect(k8sClient.Create(ctx, mhcToCreate)).To(Succeed())
+			Expect(testEnv.Create(ctx, mhcToCreate)).To(Succeed())
 
 			Eventually(func() map[string]string {
 				mhc := &clusterv1.MachineHealthCheck{}
-				err := k8sClient.Get(ctx, util.ObjectKey(mhcToCreate), mhc)
+				err := testEnv.Get(ctx, util.ObjectKey(mhcToCreate), mhc)
 				if err != nil {
 					return nil
 				}
@@ -179,11 +174,11 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				},
 			}
 			mhcToCreate.Default()
-			Expect(k8sClient.Create(ctx, mhcToCreate)).To(Succeed())
+			Expect(testEnv.Create(ctx, mhcToCreate)).To(Succeed())
 
 			Eventually(func() []metav1.OwnerReference {
 				mhc := &clusterv1.MachineHealthCheck{}
-				err := k8sClient.Get(ctx, util.ObjectKey(mhcToCreate), mhc)
+				err := testEnv.Get(ctx, util.ObjectKey(mhcToCreate), mhc)
 				if err != nil {
 					return []metav1.OwnerReference{}
 				}
@@ -210,14 +205,14 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 		// has been set on it
 		createMachine := func(m *clusterv1.Machine) {
 			status := m.Status
-			Expect(k8sClient.Create(ctx, m)).To(Succeed())
+			Expect(testEnv.Create(ctx, m)).To(Succeed())
 			key := util.ObjectKey(m)
 			Eventually(func() error {
-				if err := k8sClient.Get(ctx, key, m); err != nil {
+				if err := testEnv.Get(ctx, key, m); err != nil {
 					return err
 				}
 				m.Status = status
-				return k8sClient.Status().Update(ctx, m)
+				return testEnv.Status().Update(ctx, m)
 			}, timeout).Should(Succeed())
 		}
 
@@ -306,7 +301,7 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				By("Creating Nodes")
 				for _, n := range rtc.nodes() {
 					node := *n
-					Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+					Expect(testEnv.Create(ctx, &node)).To(Succeed())
 				}
 
 				By("Creating Machines")
@@ -318,12 +313,12 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				By("Creating a MachineHealthCheck")
 				mhc := rtc.mhc()
 				mhc.Default()
-				Expect(k8sClient.Create(ctx, mhc)).To(Succeed())
+				Expect(testEnv.Create(ctx, mhc)).To(Succeed())
 
 				By("Verifying the status has been updated")
 				Eventually(func() clusterv1.MachineHealthCheckStatus {
 					mhc := &clusterv1.MachineHealthCheck{}
-					if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: namespaceName, Name: rtc.mhc().Name}, mhc); err != nil {
+					if err := testEnv.Get(ctx, types.NamespacedName{Namespace: namespaceName, Name: rtc.mhc().Name}, mhc); err != nil {
 						return clusterv1.MachineHealthCheckStatus{}
 					}
 					return mhc.Status
@@ -335,14 +330,15 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				for _, m := range rtc.expectNotRemediated() {
 					machine := &clusterv1.Machine{}
 					key := types.NamespacedName{Namespace: m.Namespace, Name: m.Name}
-					Expect(k8sClient.Get(ctx, key, machine)).To(Succeed())
+					Expect(testEnv.Get(ctx, key, machine)).To(Succeed())
 					Expect(machine.GetDeletionTimestamp().IsZero()).To(BeTrue())
 				}
 
 				for _, m := range rtc.expectRemediated() {
 					machine := &clusterv1.Machine{}
 					key := types.NamespacedName{Namespace: m.Namespace, Name: m.Name}
-					Expect(k8sClient.Get(ctx, key, machine)).ToNot(Succeed())
+					err := testEnv.Get(ctx, key, machine)
+					Expect(apierrors.IsNotFound(err)).To(BeTrue())
 				}
 			},
 			Entry("with healthy Machines", &reconcileTestCase{
@@ -505,7 +501,7 @@ func cleanupTestNodes(ctx context.Context, c client.Client) error {
 func ownerReferenceForCluster(ctx context.Context, c *clusterv1.Cluster) metav1.OwnerReference {
 	// Fetch the cluster to populate the UID
 	cc := &clusterv1.Cluster{}
-	Expect(k8sClient.Get(ctx, util.ObjectKey(c), cc)).To(Succeed())
+	Expect(testEnv.GetClient().Get(ctx, util.ObjectKey(c), cc)).To(Succeed())
 
 	return metav1.OwnerReference{
 		APIVersion: clusterv1.GroupVersion.String(),
@@ -522,43 +518,22 @@ func TestClusterToMachineHealthCheck(t *testing.T) {
 	// BEGIN: Set up test environment
 	g := NewWithT(t)
 
-	testEnv = &envtest.Environment{
-		CRDs: []runtime.Object{
-			external.TestGenericBootstrapCRD,
-			external.TestGenericBootstrapTemplateCRD,
-			external.TestGenericInfrastructureCRD,
-			external.TestGenericInfrastructureTemplateCRD,
-		},
-		CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
-	}
-
-	var err error
-	cfg, err := testEnv.Start()
+	testEnv, err := helpers.NewTestEnvironment()
 	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(cfg).ToNot(BeNil())
+
 	defer func() {
 		g.Expect(testEnv.Stop()).To(Succeed())
 	}()
 
-	g.Expect(clusterv1.AddToScheme(scheme.Scheme)).To(Succeed())
-
-	mgr, err = manager.New(cfg, manager.Options{
-		Scheme:             scheme.Scheme,
-		MetricsBindAddress: "0",
-	})
-	g.Expect(err).ToNot(HaveOccurred())
-
 	r := &MachineHealthCheckReconciler{
 		Log:    log.Log,
-		Client: mgr.GetClient(),
+		Client: testEnv.GetClient(),
 	}
-	g.Expect(r.SetupWithManager(mgr, controller.Options{})).To(Succeed())
+	g.Expect(r.SetupWithManager(testEnv.Manager, controller.Options{})).To(Succeed())
 
-	doneMgr := make(chan struct{})
 	go func() {
-		g.Expect(mgr.Start(doneMgr)).To(Succeed())
+		g.Expect(testEnv.StartManager()).To(Succeed())
 	}()
-	defer close(doneMgr)
 
 	// END: setup test environment
 
@@ -721,43 +696,22 @@ func TestMachineToMachineHealthCheck(t *testing.T) {
 	// BEGIN: Set up test environment
 	g := NewWithT(t)
 
-	testEnv = &envtest.Environment{
-		CRDs: []runtime.Object{
-			external.TestGenericBootstrapCRD,
-			external.TestGenericBootstrapTemplateCRD,
-			external.TestGenericInfrastructureCRD,
-			external.TestGenericInfrastructureTemplateCRD,
-		},
-		CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
-	}
-
-	var err error
-	cfg, err := testEnv.Start()
+	testEnv, err := helpers.NewTestEnvironment()
 	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(cfg).ToNot(BeNil())
+
 	defer func() {
 		g.Expect(testEnv.Stop()).To(Succeed())
 	}()
 
-	g.Expect(clusterv1.AddToScheme(scheme.Scheme)).To(Succeed())
-
-	mgr, err = manager.New(cfg, manager.Options{
-		Scheme:             scheme.Scheme,
-		MetricsBindAddress: "0",
-	})
-	g.Expect(err).ToNot(HaveOccurred())
-
 	r := &MachineHealthCheckReconciler{
 		Log:    log.Log,
-		Client: mgr.GetClient(),
+		Client: testEnv.GetClient(),
 	}
-	g.Expect(r.SetupWithManager(mgr, controller.Options{})).To(Succeed())
+	g.Expect(r.SetupWithManager(testEnv.Manager, controller.Options{})).To(Succeed())
 
-	doneMgr := make(chan struct{})
 	go func() {
-		g.Expect(mgr.Start(doneMgr)).To(Succeed())
+		g.Expect(testEnv.StartManager()).To(Succeed())
 	}()
-	defer close(doneMgr)
 
 	// END: setup test environment
 
@@ -853,43 +807,22 @@ func TestNodeToMachineHealthCheck(t *testing.T) {
 	// BEGIN: Set up test environment
 	g := NewWithT(t)
 
-	testEnv = &envtest.Environment{
-		CRDs: []runtime.Object{
-			external.TestGenericBootstrapCRD,
-			external.TestGenericBootstrapTemplateCRD,
-			external.TestGenericInfrastructureCRD,
-			external.TestGenericInfrastructureTemplateCRD,
-		},
-		CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
-	}
-
-	var err error
-	cfg, err := testEnv.Start()
+	testEnv, err := helpers.NewTestEnvironment()
 	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(cfg).ToNot(BeNil())
+
 	defer func() {
 		g.Expect(testEnv.Stop()).To(Succeed())
 	}()
 
-	g.Expect(clusterv1.AddToScheme(scheme.Scheme)).To(Succeed())
-
-	mgr, err = manager.New(cfg, manager.Options{
-		Scheme:             scheme.Scheme,
-		MetricsBindAddress: "0",
-	})
-	g.Expect(err).ToNot(HaveOccurred())
-
 	r := &MachineHealthCheckReconciler{
 		Log:    log.Log,
-		Client: mgr.GetClient(),
+		Client: testEnv.GetClient(),
 	}
-	g.Expect(r.SetupWithManager(mgr, controller.Options{})).To(Succeed())
+	g.Expect(r.SetupWithManager(testEnv.Manager, controller.Options{})).To(Succeed())
 
-	doneMgr := make(chan struct{})
 	go func() {
-		g.Expect(mgr.Start(doneMgr)).To(Succeed())
+		g.Expect(testEnv.StartManager()).To(Succeed())
 	}()
-	defer close(doneMgr)
 
 	// END: setup test environment
 
diff --git a/controllers/machineset_controller_test.go b/controllers/machineset_controller_test.go
index a245ee60a..9c94ccb6e 100644
--- a/controllers/machineset_controller_test.go
+++ b/controllers/machineset_controller_test.go
@@ -40,7 +40,6 @@ import (
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/controllers/external"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/kubeconfig"
 )
 
 var _ reconcile.Reconciler = &MachineSetReconciler{}
@@ -51,18 +50,18 @@ var _ = Describe("MachineSet Reconciler", func() {
 
 	BeforeEach(func() {
 		By("Creating the namespace")
-		Expect(k8sClient.Create(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Create(ctx, namespace)).To(Succeed())
 		By("Creating the Cluster")
-		Expect(k8sClient.Create(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Create(ctx, testCluster)).To(Succeed())
 		By("Creating the Cluster Kubeconfig Secret")
-		Expect(kubeconfig.CreateEnvTestSecret(k8sClient, cfg, testCluster)).To(Succeed())
+		Expect(testEnv.CreateKubeconfigSecret(testCluster)).To(Succeed())
 	})
 
 	AfterEach(func() {
 		By("Deleting the Cluster")
-		Expect(k8sClient.Delete(ctx, testCluster)).To(Succeed())
+		Expect(testEnv.Delete(ctx, testCluster)).To(Succeed())
 		By("Deleting the namespace")
-		Expect(k8sClient.Delete(ctx, namespace)).To(Succeed())
+		Expect(testEnv.Delete(ctx, namespace)).To(Succeed())
 	})
 
 	It("Should reconcile a MachineSet", func() {
@@ -124,7 +123,7 @@ var _ = Describe("MachineSet Reconciler", func() {
 		bootstrapTmpl.SetAPIVersion("bootstrap.cluster.x-k8s.io/v1alpha3")
 		bootstrapTmpl.SetName("ms-template")
 		bootstrapTmpl.SetNamespace(namespace.Name)
-		Expect(k8sClient.Create(ctx, bootstrapTmpl)).To(Succeed())
+		Expect(testEnv.Create(ctx, bootstrapTmpl)).To(Succeed())
 
 		// Create infrastructure template resource.
 		infraResource := map[string]interface{}{
@@ -147,17 +146,17 @@ var _ = Describe("MachineSet Reconciler", func() {
 		infraTmpl.SetAPIVersion("infrastructure.cluster.x-k8s.io/v1alpha3")
 		infraTmpl.SetName("ms-template")
 		infraTmpl.SetNamespace(namespace.Name)
-		Expect(k8sClient.Create(ctx, infraTmpl)).To(Succeed())
+		Expect(testEnv.Create(ctx, infraTmpl)).To(Succeed())
 
 		// Create the MachineSet.
-		Expect(k8sClient.Create(ctx, instance)).To(Succeed())
+		Expect(testEnv.Create(ctx, instance)).To(Succeed())
 		defer func() {
-			Expect(k8sClient.Delete(ctx, instance)).To(Succeed())
+			Expect(testEnv.Delete(ctx, instance)).To(Succeed())
 		}()
 
 		By("Verifying the linked bootstrap template has a cluster owner reference")
 		Eventually(func() bool {
-			obj, err := external.Get(ctx, k8sClient, instance.Spec.Template.Spec.Bootstrap.ConfigRef, instance.Namespace)
+			obj, err := external.Get(ctx, testEnv, instance.Spec.Template.Spec.Bootstrap.ConfigRef, instance.Namespace)
 			if err != nil {
 				return false
 			}
@@ -172,7 +171,7 @@ var _ = Describe("MachineSet Reconciler", func() {
 
 		By("Verifying the linked infrastructure template has a cluster owner reference")
 		Eventually(func() bool {
-			obj, err := external.Get(ctx, k8sClient, &instance.Spec.Template.Spec.InfrastructureRef, instance.Namespace)
+			obj, err := external.Get(ctx, testEnv, &instance.Spec.Template.Spec.InfrastructureRef, instance.Namespace)
 			if err != nil {
 				return false
 			}
@@ -189,7 +188,7 @@ var _ = Describe("MachineSet Reconciler", func() {
 
 		// Verify that we have 2 replicas.
 		Eventually(func() int {
-			if err := k8sClient.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
+			if err := testEnv.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
 				return -1
 			}
 			return len(machines.Items)
@@ -202,12 +201,12 @@ var _ = Describe("MachineSet Reconciler", func() {
 
 		// Try to delete 1 machine and check the MachineSet scales back up.
 		machineToBeDeleted := machines.Items[0]
-		Expect(k8sClient.Delete(ctx, &machineToBeDeleted)).To(Succeed())
+		Expect(testEnv.Delete(ctx, &machineToBeDeleted)).To(Succeed())
 
 		// Verify that the Machine has been deleted.
 		Eventually(func() bool {
 			key := client.ObjectKey{Name: machineToBeDeleted.Name, Namespace: machineToBeDeleted.Namespace}
-			if err := k8sClient.Get(ctx, key, &machineToBeDeleted); apierrors.IsNotFound(err) || !machineToBeDeleted.DeletionTimestamp.IsZero() {
+			if err := testEnv.Get(ctx, key, &machineToBeDeleted); apierrors.IsNotFound(err) || !machineToBeDeleted.DeletionTimestamp.IsZero() {
 				return true
 			}
 			return false
@@ -215,7 +214,7 @@ var _ = Describe("MachineSet Reconciler", func() {
 
 		// Verify that we have 2 replicas.
 		Eventually(func() (ready int) {
-			if err := k8sClient.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
+			if err := testEnv.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil {
 				return -1
 			}
 			for _, m := range machines.Items {
@@ -245,7 +244,7 @@ var _ = Describe("MachineSet Reconciler", func() {
 		// Verify that all Machines are Ready.
 		Eventually(func() int32 {
 			key := client.ObjectKey{Name: instance.Name, Namespace: instance.Namespace}
-			if err := k8sClient.Get(ctx, key, instance); err != nil {
+			if err := testEnv.Get(ctx, key, instance); err != nil {
 				return -1
 			}
 			return instance.Status.AvailableReplicas
diff --git a/controllers/suite_test.go b/controllers/suite_test.go
index 195ac1e77..8f7a7c706 100644
--- a/controllers/suite_test.go
+++ b/controllers/suite_test.go
@@ -18,30 +18,19 @@ package controllers
 
 import (
 	"context"
-	"path/filepath"
 	"testing"
 	"time"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/client-go/kubernetes/scheme"
-	"k8s.io/client-go/rest"
 	"k8s.io/klog"
 	"k8s.io/klog/klogr"
-	"sigs.k8s.io/controller-runtime/pkg/cache"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
-	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
-	"sigs.k8s.io/controller-runtime/pkg/log"
-	logf "sigs.k8s.io/controller-runtime/pkg/log"
-	"sigs.k8s.io/controller-runtime/pkg/manager"
 
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/controllers/external"
+	"sigs.k8s.io/cluster-api/cmd/clusterctl/log"
+	"sigs.k8s.io/cluster-api/test/helpers"
 	// +kubebuilder:scaffold:imports
 )
 
@@ -50,11 +39,7 @@ import (
 
 func init() {
 	klog.InitFlags(nil)
-	logf.SetLogger(klogr.New())
-
-	// Register required object kinds with global scheme.
-	_ = apiextensionsv1.AddToScheme(scheme.Scheme)
-	_ = clusterv1.AddToScheme(scheme.Scheme)
+	log.SetLogger(klogr.New())
 }
 
 const (
@@ -62,12 +47,8 @@ const (
 )
 
 var (
-	cfg               *rest.Config
-	k8sClient         client.Client
-	testEnv           *envtest.Environment
-	mgr               manager.Manager
+	testEnv           *helpers.TestEnvironment
 	clusterReconciler *ClusterReconciler
-	doneMgr           = make(chan struct{})
 	ctx               = context.Background()
 )
 
@@ -81,75 +62,48 @@ func TestAPIs(t *testing.T) {
 
 var _ = BeforeSuite(func(done Done) {
 	By("bootstrapping test environment")
-	testEnv = &envtest.Environment{
-		CRDs: []runtime.Object{
-			external.TestGenericBootstrapCRD,
-			external.TestGenericBootstrapTemplateCRD,
-			external.TestGenericInfrastructureCRD,
-			external.TestGenericInfrastructureTemplateCRD,
-		},
-		CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
-	}
-
 	var err error
-	cfg, err = testEnv.Start()
-	Expect(err).ToNot(HaveOccurred())
-	Expect(cfg).ToNot(BeNil())
-
-	// +kubebuilder:scaffold:scheme
-
-	By("setting up a new manager")
-	mgr, err = manager.New(cfg, manager.Options{
-		Scheme:             scheme.Scheme,
-		MetricsBindAddress: "0",
-		NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
-			syncPeriod := 1 * time.Second
-			opts.Resync = &syncPeriod
-			return cache.New(config, opts)
-		},
-	})
+	testEnv, err = helpers.NewTestEnvironment()
 	Expect(err).NotTo(HaveOccurred())
 
-	k8sClient = mgr.GetClient()
-
 	clusterReconciler = &ClusterReconciler{
-		Client:   k8sClient,
+		Client:   testEnv,
 		Log:      log.Log,
-		recorder: mgr.GetEventRecorderFor("cluster-controller"),
+		recorder: testEnv.GetEventRecorderFor("cluster-controller"),
 	}
-	Expect(clusterReconciler.SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+	Expect(clusterReconciler.SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 	Expect((&MachineReconciler{
-		Client:   k8sClient,
+		Client:   testEnv,
 		Log:      log.Log,
-		recorder: mgr.GetEventRecorderFor("machine-controller"),
-	}).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+		recorder: testEnv.GetEventRecorderFor("machine-controller"),
+	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 	Expect((&MachineSetReconciler{
-		Client:   k8sClient,
+		Client:   testEnv,
 		Log:      log.Log,
-		recorder: mgr.GetEventRecorderFor("machineset-controller"),
-	}).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+		recorder: testEnv.GetEventRecorderFor("machineset-controller"),
+	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 	Expect((&MachineDeploymentReconciler{
-		Client:   k8sClient,
+		Client:   testEnv,
 		Log:      log.Log,
-		recorder: mgr.GetEventRecorderFor("machinedeployment-controller"),
-	}).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+		recorder: testEnv.GetEventRecorderFor("machinedeployment-controller"),
+	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 	Expect((&MachineHealthCheckReconciler{
-		Client:   k8sClient,
+		Client:   testEnv,
 		Log:      log.Log,
-		recorder: mgr.GetEventRecorderFor("machinehealthcheck-controller"),
-	}).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+		recorder: testEnv.GetEventRecorderFor("machinehealthcheck-controller"),
+	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 
 	By("starting the manager")
 	go func() {
-		Expect(mgr.Start(doneMgr)).To(Succeed())
+		Expect(testEnv.StartManager()).To(Succeed())
 	}()
 
 	close(done)
 }, 60)
 
 var _ = AfterSuite(func() {
-	By("closing the manager")
-	close(doneMgr)
-	By("tearing down the test environment")
-	Expect(testEnv.Stop()).To(Succeed())
+	if testEnv != nil {
+		By("tearing down the test environment")
+		Expect(testEnv.Stop()).To(Succeed())
+	}
 })
diff --git a/controllers/suite_util_test.go b/controllers/suite_util_test.go
index a02c24abe..a8c6876e8 100644
--- a/controllers/suite_util_test.go
+++ b/controllers/suite_util_test.go
@@ -37,7 +37,7 @@ func intOrStrPtr(i int32) *intstr.IntOrString {
 func fakeInfrastructureRefReady(ref corev1.ObjectReference, base map[string]interface{}) {
 	iref := (&unstructured.Unstructured{Object: base}).DeepCopy()
 	Eventually(func() error {
-		return k8sClient.Get(ctx, client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}, iref)
+		return testEnv.Get(ctx, client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}, iref)
 	}, timeout).Should(Succeed())
 
 	ready, found, err := unstructured.NestedBool(iref.Object, "status", "ready")
@@ -48,13 +48,13 @@ func fakeInfrastructureRefReady(ref corev1.ObjectReference, base map[string]inte
 
 	irefPatch := client.MergeFrom(iref.DeepCopy())
 	Expect(unstructured.SetNestedField(iref.Object, true, "status", "ready")).To(Succeed())
-	Expect(k8sClient.Status().Patch(ctx, iref, irefPatch)).To(Succeed())
+	Expect(testEnv.Status().Patch(ctx, iref, irefPatch)).To(Succeed())
 }
 
 func fakeMachineNodeRef(m *clusterv1.Machine) {
 	Eventually(func() error {
 		key := client.ObjectKey{Name: m.Name, Namespace: m.Namespace}
-		return k8sClient.Get(ctx, key, &clusterv1.Machine{})
+		return testEnv.Get(ctx, key, &clusterv1.Machine{})
 	}, timeout).Should(Succeed())
 
 	if m.Status.NodeRef != nil {
@@ -67,17 +67,17 @@ func fakeMachineNodeRef(m *clusterv1.Machine) {
 			GenerateName: m.Name + "-",
 		},
 	}
-	Expect(k8sClient.Create(ctx, node)).To(Succeed())
+	Expect(testEnv.Create(ctx, node)).To(Succeed())
 
 	Eventually(func() error {
 		key := client.ObjectKey{Name: node.Name, Namespace: node.Namespace}
-		return k8sClient.Get(ctx, key, &corev1.Node{})
+		return testEnv.Get(ctx, key, &corev1.Node{})
 	}, timeout).Should(Succeed())
 
 	// Patch the node and make it look like ready.
 	patchNode := client.MergeFrom(node.DeepCopy())
 	node.Status.Conditions = append(node.Status.Conditions, corev1.NodeCondition{Type: corev1.NodeReady, Status: corev1.ConditionTrue})
-	Expect(k8sClient.Status().Patch(ctx, node, patchNode)).To(Succeed())
+	Expect(testEnv.Status().Patch(ctx, node, patchNode)).To(Succeed())
 
 	// Patch the Machine.
 	patchMachine := client.MergeFrom(m.DeepCopy())
@@ -87,5 +87,5 @@ func fakeMachineNodeRef(m *clusterv1.Machine) {
 		Name:       node.Name,
 		UID:        node.UID,
 	}
-	Expect(k8sClient.Status().Patch(ctx, m, patchMachine)).To(Succeed())
+	Expect(testEnv.Status().Patch(ctx, m, patchMachine)).To(Succeed())
 }
diff --git a/controlplane/kubeadm/controllers/controller_test.go b/controlplane/kubeadm/controllers/controller_test.go
index 18669f08d..f5423e8ff 100644
--- a/controlplane/kubeadm/controllers/controller_test.go
+++ b/controlplane/kubeadm/controllers/controller_test.go
@@ -525,15 +525,6 @@ kubernetesVersion: metav1.16.1`,
 				Status: internal.ClusterStatus{},
 			},
 		},
-		managementClusterUncached: &fakeManagementCluster{
-			Management: &internal.Management{Client: fakeClient},
-			Workload: fakeWorkloadCluster{
-				Workload: &internal.Workload{
-					Client: fakeClient,
-				},
-				Status: internal.ClusterStatus{},
-			},
-		},
 	}
 
 	result, err := r.Reconcile(ctrl.Request{NamespacedName: util.ObjectKey(kcp)})
diff --git a/controlplane/kubeadm/controllers/fakes_test.go b/controlplane/kubeadm/controllers/fakes_test.go
index 4efd2be20..2a713c02a 100644
--- a/controlplane/kubeadm/controllers/fakes_test.go
+++ b/controlplane/kubeadm/controllers/fakes_test.go
@@ -78,10 +78,6 @@ func (f fakeWorkloadCluster) ClusterStatus(_ context.Context) (internal.ClusterS
 	return f.Status, nil
 }
 
-func (f fakeWorkloadCluster) AllowBootstrapTokensToGetNodes(ctx context.Context) error {
-	return nil
-}
-
 func (f fakeWorkloadCluster) ReconcileKubeletRBACRole(ctx context.Context, version semver.Version) error {
 	return nil
 }
diff --git a/controlplane/kubeadm/controllers/scale_test.go b/controlplane/kubeadm/controllers/scale_test.go
index 72fe8688d..088d37167 100644
--- a/controlplane/kubeadm/controllers/scale_test.go
+++ b/controlplane/kubeadm/controllers/scale_test.go
@@ -50,10 +50,6 @@ func TestKubeadmControlPlaneReconciler_initializeControlPlane(t *testing.T) {
 		Client:   fakeClient,
 		Log:      log.Log,
 		recorder: record.NewFakeRecorder(32),
-		managementClusterUncached: &fakeManagementCluster{
-			Management: &internal.Management{Client: fakeClient},
-			Workload:   fakeWorkloadCluster{},
-		},
 	}
 	controlPlane := &internal.ControlPlane{
 		Cluster: cluster,
@@ -105,11 +101,10 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 		fakeClient := newFakeClient(g, initObjs...)
 
 		r := &KubeadmControlPlaneReconciler{
-			Client:                    fakeClient,
-			managementCluster:         fmc,
-			managementClusterUncached: fmc,
-			Log:                       log.Log,
-			recorder:                  record.NewFakeRecorder(32),
+			Client:            fakeClient,
+			managementCluster: fmc,
+			Log:               log.Log,
+			recorder:          record.NewFakeRecorder(32),
 		}
 		controlPlane := &internal.ControlPlane{
 			KCP:      kcp,
@@ -161,11 +156,10 @@ func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) {
 			}
 
 			r := &KubeadmControlPlaneReconciler{
-				Client:                    fakeClient,
-				managementCluster:         fmc,
-				managementClusterUncached: fmc,
-				Log:                       log.Log,
-				recorder:                  record.NewFakeRecorder(32),
+				Client:            fakeClient,
+				managementCluster: fmc,
+				Log:               log.Log,
+				recorder:          record.NewFakeRecorder(32),
 			}
 			controlPlane := &internal.ControlPlane{
 				KCP:      kcp,
diff --git a/controlplane/kubeadm/controllers/upgrade_test.go b/controlplane/kubeadm/controllers/upgrade_test.go
index 7dbd859c0..5136054e5 100644
--- a/controlplane/kubeadm/controllers/upgrade_test.go
+++ b/controlplane/kubeadm/controllers/upgrade_test.go
@@ -53,12 +53,6 @@ func TestKubeadmControlPlaneReconciler_upgradeControlPlane(t *testing.T) {
 			ControlPlaneHealthy: true,
 			EtcdHealthy:         true,
 		},
-		managementClusterUncached: &fakeManagementCluster{
-			Management:          &internal.Management{Client: fakeClient},
-			Workload:            fakeWorkloadCluster{Status: internal.ClusterStatus{Nodes: 1}},
-			ControlPlaneHealthy: true,
-			EtcdHealthy:         true,
-		},
 	}
 	controlPlane := &internal.ControlPlane{
 		KCP:      kcp,
diff --git a/controlplane/kubeadm/internal/workload_cluster_etcd_test.go b/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
index dd5e6dff9..d0cbcac83 100644
--- a/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_etcd_test.go
@@ -339,14 +339,6 @@ func TestForwardEtcdLeadership(t *testing.T) {
 				leaderCandidate: nil,
 				expectErr:       true,
 			},
-			{
-				name:    "returns an error if the leader candidate's noderef is nil",
-				machine: defaultMachine(),
-				leaderCandidate: defaultMachine(func(m *clusterv1.Machine) {
-					m.Status.NodeRef = nil
-				}),
-				expectErr: true,
-			},
 			{
 				name:            "returns an error if it can't retrieve the list of control plane nodes",
 				machine:         defaultMachine(),
diff --git a/controlplane/kubeadm/internal/workload_cluster_rbac_test.go b/controlplane/kubeadm/internal/workload_cluster_rbac_test.go
deleted file mode 100644
index 7c2893e8d..000000000
--- a/controlplane/kubeadm/internal/workload_cluster_rbac_test.go
+++ /dev/null
@@ -1,162 +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 internal
-
-import (
-	"context"
-	"errors"
-	"testing"
-
-	. "github.com/onsi/gomega"
-
-	"github.com/blang/semver"
-	rbacv1 "k8s.io/api/rbac/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-func TestCluster_ReconcileKubeletRBACBinding_NoError(t *testing.T) {
-	tests := []struct {
-		name   string
-		client ctrlclient.Client
-	}{
-		{
-			name: "role binding and role already exist",
-			client: &fakeClient{
-				get: map[string]interface{}{
-					"kube-system/kubeadm:kubelet-config-1.12": &rbacv1.RoleBinding{},
-					"kube-system/kubeadm:kubelet-config-1.13": &rbacv1.Role{},
-				},
-			},
-		},
-		{
-			name:   "role binding and role don't exist",
-			client: &fakeClient{},
-		},
-		{
-			name: "create returns an already exists error",
-			client: &fakeClient{
-				createErr: apierrors.NewAlreadyExists(schema.GroupResource{}, ""),
-			},
-		},
-	}
-	ctx := context.Background()
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			c := &Workload{
-				Client: tt.client,
-			}
-			g.Expect(c.ReconcileKubeletRBACBinding(ctx, semver.MustParse("1.12.3"))).To(Succeed())
-			g.Expect(c.ReconcileKubeletRBACRole(ctx, semver.MustParse("1.13.3"))).To(Succeed())
-		})
-	}
-}
-
-func TestCluster_ReconcileKubeletRBACBinding_Error(t *testing.T) {
-	tests := []struct {
-		name   string
-		client ctrlclient.Client
-	}{
-		{
-			name: "client fails to update an expected error or the role binding/role",
-			client: &fakeClient{
-				createErr: errors.New(""),
-			},
-		},
-	}
-	ctx := context.Background()
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			c := &Workload{
-				Client: tt.client,
-			}
-			g.Expect(c.ReconcileKubeletRBACBinding(ctx, semver.MustParse("1.12.3"))).NotTo(Succeed())
-			g.Expect(c.ReconcileKubeletRBACRole(ctx, semver.MustParse("1.13.3"))).NotTo(Succeed())
-		})
-	}
-}
-
-func TestCluster_AllowBootstrapTokensToGetNodes_NoError(t *testing.T) {
-	tests := []struct {
-		name   string
-		client ctrlclient.Client
-	}{
-		{
-			name: "role binding and role already exist",
-			client: &fakeClient{
-				get: map[string]interface{}{
-					GetNodesClusterRoleName: &rbacv1.ClusterRoleBinding{},
-				},
-			},
-		},
-		{
-			name:   "role binding and role don't exist",
-			client: &fakeClient{},
-		},
-		{
-			name: "create returns an already exists error",
-			client: &fakeClient{
-				createErr: apierrors.NewAlreadyExists(schema.GroupResource{}, ""),
-			},
-		},
-	}
-	ctx := context.Background()
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			c := &Workload{
-				Client: tt.client,
-			}
-			g.Expect(c.AllowBootstrapTokensToGetNodes(ctx)).To(Succeed())
-		})
-	}
-}
-
-func TestCluster_AllowBootstrapTokensToGetNodes_Error(t *testing.T) {
-	tests := []struct {
-		name   string
-		client ctrlclient.Client
-	}{
-		{
-			name: "client fails to retrieve an expected error or the cluster role binding/role",
-			client: &fakeClient{
-				createErr: errors.New(""),
-			},
-		},
-	}
-	ctx := context.Background()
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			c := &Workload{
-				Client: tt.client,
-			}
-			g.Expect(c.AllowBootstrapTokensToGetNodes(ctx)).NotTo(Succeed())
-		})
-	}
-}
diff --git a/controlplane/kubeadm/internal/workload_cluster_test.go b/controlplane/kubeadm/internal/workload_cluster_test.go
index 97ad77de7..ba44efbff 100644
--- a/controlplane/kubeadm/internal/workload_cluster_test.go
+++ b/controlplane/kubeadm/internal/workload_cluster_test.go
@@ -26,9 +26,11 @@ import (
 	"github.com/blang/semver"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	cabpkv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha3"
 	kubeadmv1beta1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1"
@@ -37,6 +39,79 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
+func TestCluster_ReconcileKubeletRBACBinding_NoError(t *testing.T) {
+	tests := []struct {
+		name   string
+		client ctrlclient.Client
+	}{
+		{
+			name: "role binding and role already exist",
+			client: &fakeClient{
+				get: map[string]interface{}{
+					"kube-system/kubeadm:kubelet-config-1.12": &rbacv1.RoleBinding{},
+					"kube-system/kubeadm:kubelet-config-1.13": &rbacv1.Role{},
+				},
+			},
+		},
+		{
+			name:   "role binding and role don't exist",
+			client: &fakeClient{},
+		},
+		{
+			name: "create returns an already exists error",
+			client: &fakeClient{
+				createErr: apierrors.NewAlreadyExists(schema.GroupResource{}, ""),
+			},
+		},
+	}
+	ctx := context.Background()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			c := &Workload{
+				Client: tt.client,
+			}
+			g.Expect(c.ReconcileKubeletRBACBinding(ctx, semver.MustParse("1.12.3"))).To(Succeed())
+			g.Expect(c.ReconcileKubeletRBACRole(ctx, semver.MustParse("1.13.3"))).To(Succeed())
+		})
+	}
+}
+
+func TestCluster_ReconcileKubeletRBACBinding_Error(t *testing.T) {
+	tests := []struct {
+		name   string
+		client ctrlclient.Client
+	}{
+		{
+			name: "client fails to retrieve an expected error or the role binding/role",
+			client: &fakeClient{
+				getErr: errors.New(""),
+			},
+		},
+		{
+			name: "fails to create the role binding/role",
+			client: &fakeClient{
+				createErr: errors.New(""),
+			},
+		},
+	}
+	ctx := context.Background()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			c := &Workload{
+				Client: tt.client,
+			}
+			g.Expect(c.ReconcileKubeletRBACBinding(ctx, semver.MustParse("1.12.3"))).NotTo(Succeed())
+			g.Expect(c.ReconcileKubeletRBACRole(ctx, semver.MustParse("1.13.3"))).NotTo(Succeed())
+		})
+	}
+}
+
 func TestUpdateKubeProxyImageInfo(t *testing.T) {
 	g := NewWithT(t)
 
diff --git a/exp/controllers/machinepool_controller_test.go b/exp/controllers/machinepool_controller_test.go
index 95bfe45a4..26ed9bd16 100644
--- a/exp/controllers/machinepool_controller_test.go
+++ b/exp/controllers/machinepool_controller_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package controllers
 
 import (
+	"sigs.k8s.io/cluster-api/util"
 	"testing"
 
 	. "github.com/onsi/gomega"
@@ -29,7 +30,6 @@ import (
 	"k8s.io/utils/pointer"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/log"
@@ -304,7 +304,7 @@ func TestReconcileMachinePoolRequest(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "created",
 					Namespace:  "default",
-					Finalizers: []string{expv1.MachinePoolFinalizer},
+					Finalizers: []string{expv1.MachinePoolFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: expv1.MachinePoolSpec{
 					ClusterName:    "test-cluster",
@@ -340,7 +340,7 @@ func TestReconcileMachinePoolRequest(t *testing.T) {
 				ObjectMeta: metav1.ObjectMeta{
 					Name:       "updated",
 					Namespace:  "default",
-					Finalizers: []string{expv1.MachinePoolFinalizer},
+					Finalizers: []string{expv1.MachinePoolFinalizer, metav1.FinalizerDeleteDependents},
 				},
 				Spec: expv1.MachinePoolSpec{
 					ClusterName:    "test-cluster",
@@ -378,7 +378,7 @@ func TestReconcileMachinePoolRequest(t *testing.T) {
 					Labels: map[string]string{
 						clusterv1.MachineControlPlaneLabelName: "",
 					},
-					Finalizers:        []string{expv1.MachinePoolFinalizer},
+					Finalizers:        []string{expv1.MachinePoolFinalizer, metav1.FinalizerDeleteDependents},
 					DeletionTimestamp: &time,
 				},
 				Spec: expv1.MachinePoolSpec{
@@ -574,7 +574,7 @@ func TestRemoveMachinePoolFinalizerAfterDeleteReconcile(t *testing.T) {
 		ObjectMeta: metav1.ObjectMeta{
 			Name:              "delete123",
 			Namespace:         "default",
-			Finalizers:        []string{expv1.MachinePoolFinalizer},
+			Finalizers:        []string{expv1.MachinePoolFinalizer, metav1.FinalizerDeleteDependents},
 			DeletionTimestamp: &dt,
 		},
 		Spec: expv1.MachinePoolSpec{
@@ -601,7 +601,6 @@ func TestRemoveMachinePoolFinalizerAfterDeleteReconcile(t *testing.T) {
 	_, err := mr.Reconcile(reconcile.Request{NamespacedName: key})
 	g.Expect(err).ToNot(HaveOccurred())
 
-	var actual expv1.MachinePool
-	g.Expect(mr.Client.Get(ctx, key, &actual)).To(Succeed())
-	g.Expect(actual.ObjectMeta.Finalizers).To(BeEmpty())
+	g.Expect(mr.Client.Get(ctx, key, m)).To(Succeed())
+	g.Expect(m.ObjectMeta.Finalizers).To(Equal([]string{metav1.FinalizerDeleteDependents}))
 }
diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore
deleted file mode 100644
index 6722cd96e..000000000
--- a/test/e2e/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-*.xml
diff --git a/test/e2e/config/docker-ci.yaml b/test/e2e/config/docker-ci.yaml
index cd5528189..94744c073 100644
--- a/test/e2e/config/docker-ci.yaml
+++ b/test/e2e/config/docker-ci.yaml
@@ -61,11 +61,7 @@ providers:
     targetName: "cluster-template.yaml"
 
 variables:
-  KUBERNETES_VERSION: "v1.18.2"
-  ETCD_VERSION_CURRENT: "3.4.3-0"
-  COREDNS_VERSION_CURRENT: "1.6.7"
-  KUBERNETES_VERSION_UPGRADE_FROM: "v1.17.2"
-  KUBERNETES_VERSION_MINIMUM_CAPI_SUPPORTED: "v1.16.4"
+  KUBERNETES_VERSION: "v1.17.0"
   DOCKER_SERVICE_DOMAIN: "cluster.local"
   DOCKER_SERVICE_CIDRS: "10.128.0.0/12"
   # IMPORTANT! This values should match the one used by the CNI provider
@@ -76,7 +72,6 @@ intervals:
   default/wait-controllers: ["3m", "10s"]
   default/wait-cluster: ["3m", "10s"]
   default/wait-control-plane: ["5m", "10s"]
-  default/wait-worker-nodes: ["5m", "10s"]
+  default/wait-worker-nodes: ["3m", "10s"]
   default/wait-delete-cluster: ["3m", "10s"]
-  default/wait-machine-upgrade: ["20m", "10s"]
-
+  default/wait-machine-upgrade: ["15m", "10s"]
diff --git a/test/e2e/config/docker-dev.yaml b/test/e2e/config/docker-dev.yaml
index 9c5849873..e3a248ab4 100644
--- a/test/e2e/config/docker-dev.yaml
+++ b/test/e2e/config/docker-dev.yaml
@@ -91,11 +91,7 @@ providers:
   - sourcePath: "../data/infrastructure-docker/cluster-template.yaml"
 
 variables:
-  KUBERNETES_VERSION: "v1.18.2"
-  ETCD_VERSION_CURRENT: "3.4.3-0"
-  COREDNS_VERSION_CURRENT: "1.6.7"
-  KUBERNETES_VERSION_UPGRADE_FROM: "v1.17.2"
-  KUBERNETES_VERSION_MINIMUM_CAPI_SUPPORTED: "v1.16.4"
+  KUBERNETES_VERSION: "v1.17.0"
   DOCKER_SERVICE_DOMAIN: "cluster.local"
   DOCKER_SERVICE_CIDRS: "10.128.0.0/12"
   # IMPORTANT! This values should match the one used by the CNI provider
diff --git a/test/e2e/kcp_upgrade.go b/test/e2e/kcp_upgrade.go
index a82b4d681..9fb80b235 100644
--- a/test/e2e/kcp_upgrade.go
+++ b/test/e2e/kcp_upgrade.go
@@ -34,12 +34,6 @@ import (
 	"sigs.k8s.io/cluster-api/util"
 )
 
-const (
-	PreviousKubernetesVersion = "KUBERNETES_VERSION_UPGRADE_FROM"
-	EtcdCurrentVersion        = "ETCD_VERSION_CURRENT"
-	CoreDNSCurrentVersion     = "COREDNS_VERSION_CURRENT"
-)
-
 // KCPUpgradeSpecInput is the input for KCPUpgradeSpec.
 type KCPUpgradeSpecInput struct {
 	E2EConfig             *clusterctl.E2EConfig
@@ -67,15 +61,12 @@ func KCPUpgradeSpec(ctx context.Context, inputGetter func() KCPUpgradeSpecInput)
 		Expect(input.ClusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. input.ClusterctlConfigPath must be an existing file when calling %s spec", specName)
 		Expect(input.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName)
 		Expect(os.MkdirAll(input.ArtifactFolder, 0755)).To(Succeed(), "Invalid argument. input.ArtifactFolder can't be created for %s spec", specName)
-		Expect(input.E2EConfig.Variables).To(HaveKey(PreviousKubernetesVersion))
-		Expect(input.E2EConfig.Variables).To(HaveKey(EtcdCurrentVersion))
-		Expect(input.E2EConfig.Variables).To(HaveKey(CoreDNSCurrentVersion))
 
 		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
 		namespace, cancelWatches = setupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder)
 	})
 
-	It("Should successfully upgrade Kubernetes, DNS, kube-proxy, and etcd in a single control plane cluster", func() {
+	It("Should successfully upgrade Kubernetes, DNS, kube-proxy, and etcd", func() {
 
 		By("Creating a workload cluster")
 		Expect(input.E2EConfig.Variables).To(HaveKey(clusterctl.KubernetesVersion))
@@ -91,8 +82,8 @@ func KCPUpgradeSpec(ctx context.Context, inputGetter func() KCPUpgradeSpecInput)
 				Flavor:                   clusterctl.DefaultFlavor,
 				Namespace:                namespace.Name,
 				ClusterName:              fmt.Sprintf("cluster-%s", util.RandomString(6)),
-				KubernetesVersion:        input.GetPreviousKubernetesVersion(),
-				ControlPlaneMachineCount: pointer.Int64Ptr(1),
+				KubernetesVersion:        input.E2EConfig.GetKubernetesVersion(),
+				ControlPlaneMachineCount: pointer.Int64Ptr(3),
 				WorkerMachineCount:       pointer.Int64Ptr(1),
 			},
 			CNIManifestPath:              input.E2EConfig.GetCNIPath(),
@@ -108,50 +99,8 @@ func KCPUpgradeSpec(ctx context.Context, inputGetter func() KCPUpgradeSpecInput)
 			ControlPlane: controlPlane,
 			//Valid image tags for v1.17.2
 			EtcdImageTag:                "3.4.3-0",
-			DNSImageTag:                 "1.6.7",
-			KubernetesUpgradeVersion:    input.E2EConfig.GetKubernetesVersion(),
-			WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-			WaitForDNSUpgrade:           input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-			WaitForEtcdUpgrade:          input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
-		})
-
-		By("PASSED!")
-	})
-
-	It("Should successfully upgrade Kubernetes, DNS, kube-proxy, and etcd in a HA cluster", func() {
-
-		By("Creating a workload cluster")
-		Expect(input.E2EConfig.Variables).To(HaveKey(clusterctl.KubernetesVersion))
-		Expect(input.E2EConfig.Variables).To(HaveKey(clusterctl.CNIPath))
-
-		cluster, controlPlane, _ = clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
-			ClusterProxy: input.BootstrapClusterProxy,
-			ConfigCluster: clusterctl.ConfigClusterInput{
-				LogFolder:                filepath.Join(input.ArtifactFolder, "clusters", input.BootstrapClusterProxy.GetName()),
-				ClusterctlConfigPath:     input.ClusterctlConfigPath,
-				KubeconfigPath:           input.BootstrapClusterProxy.GetKubeconfigPath(),
-				InfrastructureProvider:   clusterctl.DefaultInfrastructureProvider,
-				Flavor:                   clusterctl.DefaultFlavor,
-				Namespace:                namespace.Name,
-				ClusterName:              fmt.Sprintf("cluster-%s", util.RandomString(6)),
-				KubernetesVersion:        input.GetPreviousKubernetesVersion(),
-				ControlPlaneMachineCount: pointer.Int64Ptr(3),
-				WorkerMachineCount:       pointer.Int64Ptr(1),
-			},
-			CNIManifestPath:              input.E2EConfig.GetCNIPath(),
-			WaitForClusterIntervals:      input.E2EConfig.GetIntervals(specName, "wait-cluster"),
-			WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"),
-			WaitForMachineDeployments:    input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
-		})
-
-		By("Upgrading Kubernetes")
-		framework.UpgradeControlPlaneAndWaitForUpgrade(ctx, framework.UpgradeControlPlaneAndWaitForUpgradeInput{
-			ClusterProxy:                input.BootstrapClusterProxy,
-			Cluster:                     cluster,
-			ControlPlane:                controlPlane,
-			EtcdImageTag:                input.GetEtcdCurrentVersion(),
-			DNSImageTag:                 input.GetCoreDNSCurrentVersion(),
-			KubernetesUpgradeVersion:    input.E2EConfig.GetKubernetesVersion(),
+			DNSImageTag:                 "1.6.6",
+			KubernetesUpgradeVersion:    "v1.17.2",
 			WaitForMachinesToBeUpgraded: input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
 			WaitForDNSUpgrade:           input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
 			WaitForEtcdUpgrade:          input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"),
@@ -165,18 +114,3 @@ func KCPUpgradeSpec(ctx context.Context, inputGetter func() KCPUpgradeSpecInput)
 		dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, cluster, input.E2EConfig.GetIntervals, input.SkipCleanup)
 	})
 }
-
-// GetPreviousKubernetesVersion returns the previous kubernetes version to test an upgrade from
-func (k KCPUpgradeSpecInput) GetPreviousKubernetesVersion() string {
-	return k.E2EConfig.Variables[PreviousKubernetesVersion]
-}
-
-// GetEtcdCurrentVersion returns the version of etcd to upgrade to
-func (k KCPUpgradeSpecInput) GetEtcdCurrentVersion() string {
-	return k.E2EConfig.Variables[EtcdCurrentVersion]
-}
-
-// GetCoreDNSUpgradeVersion returns the version of etcd to upgrade to
-func (k KCPUpgradeSpecInput) GetCoreDNSCurrentVersion() string {
-	return k.E2EConfig.Variables[CoreDNSCurrentVersion]
-}
diff --git a/test/e2e/self_hosted.go b/test/e2e/self_hosted.go
index 450a89754..ee5ca5570 100644
--- a/test/e2e/self_hosted.go
+++ b/test/e2e/self_hosted.go
@@ -34,8 +34,6 @@ import (
 	"sigs.k8s.io/cluster-api/util"
 )
 
-const MinimumCAPISupportedKubernetesVersion = "KUBERNETES_VERSION_MINIMUM_CAPI_SUPPORTED"
-
 // SelfHostedSpecInput is the input for SelfHostedSpec.
 type SelfHostedSpecInput struct {
 	E2EConfig             *clusterctl.E2EConfig
@@ -67,7 +65,6 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 		Expect(input.ClusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. input.ClusterctlConfigPath must be an existing file when calling %s spec", specName)
 		Expect(input.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName)
 		Expect(os.MkdirAll(input.ArtifactFolder, 0755)).To(Succeed(), "Invalid argument. input.ArtifactFolder can't be created for %s spec", specName)
-		Expect(input.E2EConfig.Variables).To(HaveKey(MinimumCAPISupportedKubernetesVersion))
 
 		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
 		namespace, cancelWatches = setupSpecNamespace(context.TODO(), specName, input.BootstrapClusterProxy, input.ArtifactFolder)
@@ -89,7 +86,7 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 				Flavor:                   clusterctl.DefaultFlavor,
 				Namespace:                namespace.Name,
 				ClusterName:              fmt.Sprintf("cluster-%s", util.RandomString(6)),
-				KubernetesVersion:        input.GetMinimumCAPISupportedKubernetesVersion(),
+				KubernetesVersion:        input.E2EConfig.GetKubernetesVersion(),
 				ControlPlaneMachineCount: pointer.Int64Ptr(1),
 				WorkerMachineCount:       pointer.Int64Ptr(1),
 			},
@@ -193,7 +190,3 @@ func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput)
 		dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, cluster, input.E2EConfig.GetIntervals, input.SkipCleanup)
 	})
 }
-
-func (k SelfHostedSpecInput) GetMinimumCAPISupportedKubernetesVersion() string {
-	return k.E2EConfig.Variables[MinimumCAPISupportedKubernetesVersion]
-}
diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go
new file mode 100644
index 000000000..4afa0c8e2
--- /dev/null
+++ b/test/helpers/envtest.go
@@ -0,0 +1,122 @@
+/*
+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 helpers
+
+import (
+	"path"
+	"path/filepath"
+	goruntime "runtime"
+	"time"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	kerrors "k8s.io/apimachinery/pkg/util/errors"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
+	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha3"
+	"sigs.k8s.io/cluster-api/controllers/external"
+	"sigs.k8s.io/cluster-api/util/kubeconfig"
+	"sigs.k8s.io/controller-runtime/pkg/cache"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+)
+
+type TestEnvironment struct {
+	*envtest.Environment
+	manager.Manager
+	client.Client
+
+	Config  *rest.Config
+	scheme  *runtime.Scheme
+	doneMgr chan struct{}
+}
+
+func newTestEnvironment() *TestEnvironment {
+	scheme := scheme.Scheme
+	utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
+	utilruntime.Must(clusterv1.AddToScheme(scheme))
+	utilruntime.Must(bootstrapv1.AddToScheme(scheme))
+
+	_, filename, _, _ := goruntime.Caller(0) //nolint
+	root := path.Join(path.Dir(filename), "..", "..")
+
+	return &TestEnvironment{
+		Environment: &envtest.Environment{
+			ErrorIfCRDPathMissing: true,
+			CRDDirectoryPaths: []string{
+				filepath.Join(root, "config", "crd", "bases"),
+				filepath.Join(root, "controlplane", "kubeadm", "config", "crd", "bases"),
+				filepath.Join(root, "bootstrap", "kubeadm", "config", "crd", "bases"),
+			},
+			CRDs: []runtime.Object{
+				external.TestGenericBootstrapCRD,
+				external.TestGenericBootstrapTemplateCRD,
+				external.TestGenericInfrastructureCRD,
+				external.TestGenericInfrastructureTemplateCRD,
+			},
+		},
+		scheme:  scheme,
+		doneMgr: make(chan struct{}),
+	}
+}
+
+func NewTestEnvironment() (*TestEnvironment, error) {
+	env := newTestEnvironment()
+
+	cfg, err := env.Environment.Start()
+	if err != nil {
+		return env, err
+	}
+
+	mgr, err := manager.New(cfg, manager.Options{
+		Scheme:             env.scheme,
+		MetricsBindAddress: "0",
+		NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
+			syncPeriod := 1 * time.Second
+			opts.Resync = &syncPeriod
+			return cache.New(config, opts)
+		},
+	})
+	if err != nil {
+		if stopErr := env.Stop(); err != nil {
+			return nil, kerrors.NewAggregate([]error{err, stopErr})
+		}
+		return env, err
+	}
+
+	env.Config = cfg
+	env.Manager = mgr
+	env.Client = mgr.GetClient()
+
+	return env, nil
+}
+
+func (t *TestEnvironment) StartManager() error {
+	return t.Manager.Start(t.doneMgr)
+}
+
+func (t *TestEnvironment) Stop() error {
+	close(t.doneMgr)
+	return t.Environment.Stop()
+}
+
+func (t *TestEnvironment) CreateKubeconfigSecret(cluster *clusterv1.Cluster) error {
+	return kubeconfig.CreateEnvTestSecret(t.Client, t.Config, cluster)
+}
diff --git a/test/infrastructure/docker/controllers/dockercluster_controller.go b/test/infrastructure/docker/controllers/dockercluster_controller.go
index 4c0192aeb..2871a095a 100644
--- a/test/infrastructure/docker/controllers/dockercluster_controller.go
+++ b/test/infrastructure/docker/controllers/dockercluster_controller.go
@@ -27,7 +27,6 @@ import (
 	"sigs.k8s.io/cluster-api/test/infrastructure/docker/docker"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -151,18 +150,13 @@ func reconcileDelete(ctx context.Context, dockerCluster *infrav1.DockerCluster,
 
 // SetupWithManager will add watches for this controller
 func (r *DockerClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
-	c, err := ctrl.NewControllerManagedBy(mgr).
+	return ctrl.NewControllerManagedBy(mgr).
 		For(&infrav1.DockerCluster{}).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
-		Build(r)
-	if err != nil {
-		return err
-	}
-	return c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: util.ClusterToInfrastructureMapFunc(infrav1.GroupVersion.WithKind("DockerCluster")),
-		},
-		predicates.ClusterUnpaused(r.Log),
-	)
+		Watches(
+			&source.Kind{Type: &clusterv1.Cluster{}},
+			&handler.EnqueueRequestsFromMapFunc{
+				ToRequests: util.ClusterToInfrastructureMapFunc(infrav1.GroupVersion.WithKind("DockerCluster")),
+			},
+		).
+		Complete(r)
 }
diff --git a/test/infrastructure/docker/controllers/dockermachine_controller.go b/test/infrastructure/docker/controllers/dockermachine_controller.go
index ad8d5c590..cae115234 100644
--- a/test/infrastructure/docker/controllers/dockermachine_controller.go
+++ b/test/infrastructure/docker/controllers/dockermachine_controller.go
@@ -31,7 +31,6 @@ import (
 	"sigs.k8s.io/cluster-api/test/infrastructure/docker/docker"
 	"sigs.k8s.io/cluster-api/util"
 	"sigs.k8s.io/cluster-api/util/patch"
-	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -252,15 +251,8 @@ func (r *DockerMachineReconciler) reconcileDelete(ctx context.Context, machine *
 
 // SetupWithManager will add watches for this controller
 func (r *DockerMachineReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
-	clusterToDockerMachines, err := util.ClusterToObjectsMapper(mgr.GetClient(), &infrav1.DockerMachineList{}, mgr.GetScheme())
-	if err != nil {
-		return err
-	}
-
-	c, err := ctrl.NewControllerManagedBy(mgr).
+	return ctrl.NewControllerManagedBy(mgr).
 		For(&infrav1.DockerMachine{}).
-		WithOptions(options).
-		WithEventFilter(predicates.ResourceNotPaused(r.Log)).
 		Watches(
 			&source.Kind{Type: &clusterv1.Machine{}},
 			&handler.EnqueueRequestsFromMapFunc{
@@ -273,17 +265,8 @@ func (r *DockerMachineReconciler) SetupWithManager(mgr ctrl.Manager, options con
 				ToRequests: handler.ToRequestsFunc(r.DockerClusterToDockerMachines),
 			},
 		).
-		Build(r)
-	if err != nil {
-		return err
-	}
-	return c.Watch(
-		&source.Kind{Type: &clusterv1.Cluster{}},
-		&handler.EnqueueRequestsFromMapFunc{
-			ToRequests: clusterToDockerMachines,
-		},
-		predicates.ClusterUnpausedAndInfrastructureReady(r.Log),
-	)
+		WithOptions(options).
+		Complete(r)
 }
 
 // DockerClusterToDockerMachines is a handler.ToRequestsFunc to be used to enqeue
diff --git a/test/infrastructure/docker/e2e/.gitignore b/test/infrastructure/docker/e2e/.gitignore
deleted file mode 100644
index dbad7374b..000000000
--- a/test/infrastructure/docker/e2e/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-logs
-resources
diff --git a/test/infrastructure/docker/go.sum b/test/infrastructure/docker/go.sum
index 261f6c684..99a7cb227 100644
--- a/test/infrastructure/docker/go.sum
+++ b/test/infrastructure/docker/go.sum
@@ -161,6 +161,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -205,12 +207,16 @@ github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
 github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -397,8 +403,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -474,6 +481,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/util/kubeconfig/testing.go b/util/kubeconfig/testing.go
index 02f82395f..009b2031f 100644
--- a/util/kubeconfig/testing.go
+++ b/util/kubeconfig/testing.go
@@ -27,6 +27,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+// Deprecated: use test/helpers/envtest
 func CreateEnvTestSecret(client client.Client, cfg *rest.Config, cluster *clusterv1.Cluster) error {
 	return client.Create(context.TODO(), GenerateSecret(cluster, FromEnvTestConfig(cfg, cluster)))
 }
diff --git a/util/secret/secret_test.go b/util/secret/secret_test.go
deleted file mode 100644
index 576a08a4d..000000000
--- a/util/secret/secret_test.go
+++ /dev/null
@@ -1,82 +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 secret
-
-import (
-	"testing"
-
-	. "github.com/onsi/gomega"
-)
-
-func TestParseSecretName(t *testing.T) {
-	type args struct {
-		name string
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    string
-		want1   Purpose
-		wantErr bool
-	}{
-		{
-			name: "A secret for the test cluster",
-			args: args{
-				name: "test-kubeconfig",
-			},
-			want:    "test",
-			want1:   Kubeconfig,
-			wantErr: false,
-		},
-		{
-			name: "A secret for the test-capa cluster (cluster name with - in the middle)",
-			args: args{
-				name: "test-capa-ca",
-			},
-			want:    "test-capa",
-			want1:   ClusterCA,
-			wantErr: false,
-		},
-		{
-			name: "Not a Cluster API secret",
-			args: args{
-				name: "foo",
-			},
-			wantErr: true,
-		},
-		{
-			name: "Not a Cluster API secret (secret name with - in the middle)",
-			args: args{
-				name: "foo-bar",
-			},
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-			got, got1, err := ParseSecretName(tt.args.name)
-			if tt.wantErr {
-				g.Expect(err).To(HaveOccurred())
-			} else {
-				g.Expect(err).ToNot(HaveOccurred())
-			}
-			g.Expect(got).To(Equal(tt.want))
-			g.Expect(got1).To(Equal(tt.want1))
-		})
-	}
-}