Repository: kubernetes-sigs/cluster-api
Pull request #3129 (2020-06-02 16:44:32)

🌱 Integrate shared remote cluster watching into MHC
<!-- please add a icon to the title of this PR (see https://sigs.k8s.io/cluster-api/VERSIONING.md), and delete this line and similar ones -->
<!-- the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, minor or feature additions), 🐛 (:bug:, patch and bugfixes), 📖 (:book:, documentation or proposals), or 🏃 (:running:, other) -->

**What this PR does / why we need it**:

This PR sets up a ClusterCacheTracker and reconciler in the main and passes the tracker to the MHC so that it can use that to track remote Nodes rather than doing it itself

I've also added a test which is meant to check the remote tracking functionality, but I hit a bump, with the normal timeout, it passes whether the tracker is there or not because we do a full resync every second. So I've set it with a very short timeout (500ms), I'm concerned this may flake in CI. 

Does anyone happen to know why the tests set up this way so that there's a full sync every second? What are the implications of possible extending that? 

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/3129#" title="Author self-approved">JoelSpeed</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **justinsb**
You can assign the PR to them by writing `/assign @justinsb` 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":["justinsb"]} -->
vincepri: 🎉 🎉 🎉 🎉 🎉 🎉 🎉 
vincepri: @JoelSpeed The resync time is useful to get fast turnaround for changes, we went down that path a while ago because the cache kept staying behind.

We can try to raise it, and see if that has a significant impact on the job duration
JoelSpeed: I'll have a quick look at changing it tomorrow, I imagine it is probably a side effect of controllers not watching things that they need to correctly right? If they watched everything that affects them they should reconcile as soon as updates are made
vincepri: Yeah, it was a problem during v1alpha2 days, we might be in a better place today — I'll give it a quick try locally and see how it goes
vincepri: /retitle 🌱 Integrate shared remote cluster watching into MHC

vincepri: /milestone v0.3.7
vincepri: @JoelSpeed If we rebase on master, we should be able to reduce the timeout of that particular test, given that the resync has been removed
JoelSpeed: > @JoelSpeed If we rebase on master, we should be able to reduce the timeout of that particular test, given that the resync has been removed

Rebased on master, can you elaborate on what you think I can change? I'm not following this comment
k8s-ci-robot: @JoelSpeed: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/cluster-api/pull/3129#" title="Author self-approved">JoelSpeed</a>*, *<a href="https://github.com/kubernetes-sigs/cluster-api/pull/3129#pullrequestreview-425377952" title="Approved">vincepri</a>*

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

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

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

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

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

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

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-apidiff | 7c6c6949cc9b527c0dac952b92fceeb061b64586 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-apidiff/1268919726487113734) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 7c6c6949cc9b527c0dac952b92fceeb061b64586 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-make/1268919726487113731) | `/test pull-cluster-api-make`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-make | 7c6c6949cc9b527c0dac952b92fceeb061b64586 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-make/1268919726487113731) | `/test pull-cluster-api-make`
pull-cluster-api-apidiff | 929cfe4c8c5cf47573e36d50eec5bb731c575397 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-apidiff/1268920482766262273) | `/test pull-cluster-api-apidiff`

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-cluster-api-apidiff | 929cfe4c8c5cf47573e36d50eec5bb731c575397 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-apidiff/1268920482766262273) | `/test pull-cluster-api-apidiff`
pull-cluster-api-make | 929cfe4c8c5cf47573e36d50eec5bb731c575397 | [link](https://prow.k8s.io/view/gcs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_cluster-api/3129/pull-cluster-api-make/1268920482766262275) | `/test pull-cluster-api-make`

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

<details>

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

Patch:
diff --git a/cmd/clusterctl/client/cluster/cert_manager.go b/cmd/clusterctl/client/cluster/cert_manager.go
index c3679424f..c5366c64d 100644
--- a/cmd/clusterctl/client/cluster/cert_manager.go
+++ b/cmd/clusterctl/client/cluster/cert_manager.go
@@ -27,7 +27,6 @@ import (
 	manifests "sigs.k8s.io/cluster-api/cmd/clusterctl/config"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
 	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -181,7 +180,7 @@ func (cm *certManagerClient) getManifestObjs() ([]unstructured.Unstructured, err
 		return nil, err
 	}
 
-	objs, err := utilyaml.ToUnstructured(yaml)
+	objs, err := util.ToUnstructured(yaml)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse yaml for cert-manager manifest")
 	}
diff --git a/cmd/clusterctl/client/cluster/inventory.go b/cmd/clusterctl/client/cluster/inventory.go
index 3b6a0ae7b..67f8663d9 100644
--- a/cmd/clusterctl/client/cluster/inventory.go
+++ b/cmd/clusterctl/client/cluster/inventory.go
@@ -27,8 +27,8 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/config"
+	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
 	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -129,7 +129,7 @@ func (p *inventoryClient) EnsureCustomResourceDefinitions() error {
 	}
 
 	// Transform the yaml in a list of objects.
-	objs, err := utilyaml.ToUnstructured(yaml)
+	objs, err := util.ToUnstructured(yaml)
 	if err != nil {
 		return errors.Wrap(err, "failed to parse yaml for clusterctl inventory CRDs")
 	}
diff --git a/cmd/clusterctl/client/repository/components.go b/cmd/clusterctl/client/repository/components.go
index 32afee0c0..422fb66ff 100644
--- a/cmd/clusterctl/client/repository/components.go
+++ b/cmd/clusterctl/client/repository/components.go
@@ -33,7 +33,6 @@ import (
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
 )
 
 const (
@@ -172,7 +171,7 @@ func (c *components) Yaml() ([]byte, error) {
 	objs = append(objs, c.sharedObjs...)
 	objs = append(objs, c.instanceObjs...)
 
-	return utilyaml.FromUnstructured(objs)
+	return util.FromUnstructured(objs)
 }
 
 // ComponentsOptions is the inputs needed by the NewComponents
@@ -205,7 +204,7 @@ func NewComponents(provider config.Provider, configClient config.Client, rawyaml
 	}
 
 	// Transform the yaml in a list of objects, so following transformation can work on typed objects (instead of working on a string/slice of bytes)
-	objs, err := utilyaml.ToUnstructured(yaml)
+	objs, err := util.ToUnstructured(yaml)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse yaml")
 	}
diff --git a/cmd/clusterctl/client/repository/template.go b/cmd/clusterctl/client/repository/template.go
index 19ddc6142..08d08f49d 100644
--- a/cmd/clusterctl/client/repository/template.go
+++ b/cmd/clusterctl/client/repository/template.go
@@ -20,7 +20,7 @@ import (
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
+	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
 )
 
 // Template wraps a YAML file that defines the cluster objects (Cluster, Machines etc.).
@@ -66,7 +66,7 @@ func (t *template) Objs() []unstructured.Unstructured {
 }
 
 func (t *template) Yaml() ([]byte, error) {
-	return utilyaml.FromUnstructured(t.objs)
+	return util.FromUnstructured(t.objs)
 }
 
 // NewTemplate returns a new objects embedding a cluster template YAML file.
@@ -86,7 +86,7 @@ func NewTemplate(rawYaml []byte, configVariablesClient config.VariablesClient, t
 	}
 
 	// Transform the yaml in a list of objects, so following transformation can work on typed objects (instead of working on a string/slice of bytes).
-	objs, err := utilyaml.ToUnstructured(yaml)
+	objs, err := util.ToUnstructured(yaml)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse yaml")
 	}
diff --git a/cmd/clusterctl/internal/util/yaml.go b/cmd/clusterctl/internal/util/yaml.go
new file mode 100644
index 000000000..53f78a555
--- /dev/null
+++ b/cmd/clusterctl/internal/util/yaml.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2019 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 util
+
+import (
+	"bufio"
+	"bytes"
+	"io"
+
+	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
+	"sigs.k8s.io/cluster-api/util"
+	"sigs.k8s.io/yaml"
+)
+
+var yamlSeparator = []byte("---")
+
+// JoinYaml takes a list of YAML files and join them ensuring
+// each YAML that the yaml separator goes on a new line by adding \n where necessary
+func JoinYaml(yamls ...[]byte) []byte {
+	var cr = []byte("\n")
+	var b [][]byte //nolint
+	for _, y := range yamls {
+		if !bytes.HasPrefix(y, cr) {
+			y = append(cr, y...)
+		}
+		if !bytes.HasSuffix(y, cr) {
+
+			y = append(y, cr...)
+		}
+		b = append(b, y)
+	}
+
+	r := bytes.Join(b, yamlSeparator)
+	r = bytes.TrimPrefix(r, cr)
+	r = bytes.TrimSuffix(r, cr)
+
+	return r
+}
+
+// ToUnstructured takes a YAML and converts it to a list of Unstructured objects
+func ToUnstructured(rawyaml []byte) ([]unstructured.Unstructured, error) {
+	var ret []unstructured.Unstructured
+
+	reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(rawyaml)))
+	count := 1
+	for {
+		// Read one YAML document at a time, until io.EOF is returned
+		b, err := reader.Read()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return nil, errors.Wrapf(err, "failed to read yaml")
+		}
+		if len(b) == 0 {
+			break
+		}
+
+		var m map[string]interface{}
+		if err := yaml.Unmarshal(b, &m); err != nil {
+			return nil, errors.Wrapf(err, "failed to unmarshal the %s yaml document: %q", util.Ordinalize(count), string(b))
+		}
+
+		var u unstructured.Unstructured
+		u.SetUnstructuredContent(m)
+
+		// Ignore empty objects.
+		// Empty objects are generated if there are weird things in manifest files like e.g. two --- in a row without a yaml doc in the middle
+		if u.Object == nil {
+			continue
+		}
+
+		ret = append(ret, u)
+		count++
+	}
+
+	return ret, nil
+}
+
+// FromUnstructured takes a list of Unstructured objects and converts it into a YAML
+func FromUnstructured(objs []unstructured.Unstructured) ([]byte, error) {
+	var ret [][]byte //nolint
+	for _, o := range objs {
+		content, err := yaml.Marshal(o.UnstructuredContent())
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to marshal yaml for %s, %s/%s", o.GroupVersionKind(), o.GetNamespace(), o.GetName())
+		}
+		ret = append(ret, content)
+	}
+
+	return JoinYaml(ret...), nil
+}
diff --git a/controllers/machinehealthcheck_controller.go b/controllers/machinehealthcheck_controller.go
index f6de9b728..daae1ca00 100644
--- a/controllers/machinehealthcheck_controller.go
+++ b/controllers/machinehealthcheck_controller.go
@@ -19,7 +19,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"sync"
 	"time"
 
 	"github.com/go-logr/logr"
@@ -65,14 +64,13 @@ const (
 
 // MachineHealthCheckReconciler reconciles a MachineHealthCheck object
 type MachineHealthCheckReconciler struct {
-	Client client.Client
-	Log    logr.Logger
-
-	controller        controller.Controller
-	recorder          record.EventRecorder
-	scheme            *runtime.Scheme
-	clusterCaches     map[client.ObjectKey]cache.Cache
-	clusterCachesLock sync.RWMutex
+	Client  client.Client
+	Log     logr.Logger
+	Tracker *remote.ClusterCacheTracker
+
+	controller controller.Controller
+	recorder   record.EventRecorder
+	scheme     *runtime.Scheme
 }
 
 func (r *MachineHealthCheckReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
@@ -117,7 +115,6 @@ func (r *MachineHealthCheckReconciler) SetupWithManager(mgr ctrl.Manager, option
 	r.controller = controller
 	r.recorder = mgr.GetEventRecorderFor("machinehealthcheck-controller")
 	r.scheme = mgr.GetScheme()
-	r.clusterCaches = make(map[client.ObjectKey]cache.Cache)
 	return nil
 }
 
@@ -203,7 +200,7 @@ func (r *MachineHealthCheckReconciler) reconcile(ctx context.Context, cluster *c
 		return ctrl.Result{}, err
 	}
 
-	if err := r.watchClusterNodes(ctx, r.Client, cluster); err != nil {
+	if err := r.watchClusterNodes(ctx, cluster); err != nil {
 		logger.Error(err, "Error watching nodes on target cluster")
 		return ctrl.Result{}, err
 	}
@@ -376,54 +373,21 @@ func (r *MachineHealthCheckReconciler) getMachineFromNode(nodeName string) (*clu
 	return &machineList.Items[0], nil
 }
 
-func (r *MachineHealthCheckReconciler) watchClusterNodes(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) error {
-	key := util.ObjectKey(cluster)
-	if _, ok := r.getClusterCache(key); ok {
-		// watch was already set up for this cluster
+func (r *MachineHealthCheckReconciler) watchClusterNodes(ctx context.Context, cluster *clusterv1.Cluster) error {
+	// If there is no tracker, don't watch remote nodes
+	if r.Tracker == nil {
 		return nil
 	}
 
-	return r.createClusterCache(ctx, c, key)
-}
-
-func (r *MachineHealthCheckReconciler) getClusterCache(key client.ObjectKey) (cache.Cache, bool) {
-	r.clusterCachesLock.RLock()
-	defer r.clusterCachesLock.RUnlock()
-
-	c, ok := r.clusterCaches[key]
-	return c, ok
-}
-
-func (r *MachineHealthCheckReconciler) createClusterCache(ctx context.Context, c client.Client, key client.ObjectKey) error {
-	r.clusterCachesLock.Lock()
-	defer r.clusterCachesLock.Unlock()
-
-	// Double check the key still doesn't exist under write lock
-	if _, ok := r.clusterCaches[key]; ok {
-		// An informer was created while waiting for the lock
-		return nil
+	if err := r.Tracker.Watch(ctx, remote.WatchInput{
+		Cluster:      util.ObjectKey(cluster),
+		Watcher:      r.controller,
+		Kind:         &corev1.Node{},
+		CacheOptions: cache.Options{},
+		EventHandler: &handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.nodeToMachineHealthCheck)},
+	}); err != nil {
+		return err
 	}
-
-	config, err := remote.RESTConfig(ctx, c, key)
-	if err != nil {
-		return errors.Wrap(err, "error fetching remote cluster config")
-	}
-
-	clusterCache, err := cache.New(config, cache.Options{})
-	if err != nil {
-		return errors.Wrap(err, "error creating cache for remote cluster")
-	}
-	go clusterCache.Start(ctx.Done())
-
-	err = r.controller.Watch(
-		source.NewKindWithCache(&corev1.Node{}, clusterCache),
-		&handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.nodeToMachineHealthCheck)},
-	)
-	if err != nil {
-		return errors.Wrap(err, "error watching nodes on target cluster")
-	}
-
-	r.clusterCaches[key] = clusterCache
 	return nil
 }
 
diff --git a/controllers/remote/cluster_cache.go b/controllers/remote/cluster_cache.go
index 7e82d4636..d20308c39 100644
--- a/controllers/remote/cluster_cache.go
+++ b/controllers/remote/cluster_cache.go
@@ -366,34 +366,21 @@ func healthCheckPath(sourceCfg *rest.Config, requestTimeout time.Duration, path
 // ClusterCacheReconciler is responsible for stopping remote cluster caches when
 // the cluster for the remote cache is being deleted.
 type ClusterCacheReconciler struct {
-	log     logr.Logger
-	client  client.Client
-	tracker *ClusterCacheTracker
+	Log     logr.Logger
+	Client  client.Client
+	Tracker *ClusterCacheTracker
 }
 
-func NewClusterCacheReconciler(
-	log logr.Logger,
-	mgr ctrl.Manager,
-	controllerOptions controller.Options,
-	cct *ClusterCacheTracker,
-) (*ClusterCacheReconciler, error) {
-	r := &ClusterCacheReconciler{
-		log:     log,
-		client:  mgr.GetClient(),
-		tracker: cct,
-	}
-
-	// Watch Clusters so we can stop and remove caches when Clusters are deleted.
+func (r *ClusterCacheReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
 	_, err := ctrl.NewControllerManagedBy(mgr).
 		For(&clusterv1.Cluster{}).
-		WithOptions(controllerOptions).
+		WithOptions(options).
 		Build(r)
 
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to create cluster cache manager controller")
+		return errors.Wrap(err, "failed setting up with a controller manager")
 	}
-
-	return r, nil
+	return nil
 }
 
 // Reconcile reconciles Clusters and removes ClusterCaches for any Cluster that cannot be retrieved from the
@@ -401,12 +388,12 @@ func NewClusterCacheReconciler(
 func (r *ClusterCacheReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
 	ctx := context.Background()
 
-	log := r.log.WithValues("namespace", req.Namespace, "name", req.Name)
+	log := r.Log.WithValues("namespace", req.Namespace, "name", req.Name)
 	log.V(4).Info("Reconciling")
 
 	var cluster clusterv1.Cluster
 
-	err := r.client.Get(ctx, req.NamespacedName, &cluster)
+	err := r.Client.Get(ctx, req.NamespacedName, &cluster)
 	if err == nil {
 		log.V(4).Info("Cluster still exists")
 		return reconcile.Result{}, nil
@@ -417,7 +404,7 @@ func (r *ClusterCacheReconciler) Reconcile(req reconcile.Request) (reconcile.Res
 
 	log.V(4).Info("Cluster no longer exists")
 
-	c := r.tracker.getClusterCache(req.NamespacedName)
+	c := r.Tracker.getClusterCache(req.NamespacedName)
 	if c == nil {
 		log.V(4).Info("No current cluster cache exists - nothing to do")
 		return reconcile.Result{}, nil
@@ -426,10 +413,10 @@ func (r *ClusterCacheReconciler) Reconcile(req reconcile.Request) (reconcile.Res
 	log.V(4).Info("Stopping cluster cache")
 	c.Stop()
 
-	r.tracker.deleteClusterCache(req.NamespacedName)
+	r.Tracker.deleteClusterCache(req.NamespacedName)
 
 	log.V(4).Info("Deleting watches for cluster cache")
-	r.tracker.deleteWatchesForCluster(req.NamespacedName)
+	r.Tracker.deleteWatchesForCluster(req.NamespacedName)
 
 	return reconcile.Result{}, nil
 }
diff --git a/controlplane/kubeadm/config/default/kustomization.yaml b/controlplane/kubeadm/config/default/kustomization.yaml
index 36a864aa0..17f25ac03 100644
--- a/controlplane/kubeadm/config/default/kustomization.yaml
+++ b/controlplane/kubeadm/config/default/kustomization.yaml
@@ -6,6 +6,3 @@ resources:
 bases:
 - ../rbac
 - ../manager
-
-patchesStrategicMerge:
-- manager_role_aggregation_patch.yaml
diff --git a/controlplane/kubeadm/config/default/manager_role_aggregation_patch.yaml b/controlplane/kubeadm/config/default/manager_role_aggregation_patch.yaml
deleted file mode 100644
index 25399dfe0..000000000
--- a/controlplane/kubeadm/config/default/manager_role_aggregation_patch.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: manager-role
-  labels:
-    kubeadm.controlplane.cluster.x-k8s.io/aggregate-to-manager: "true"
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  name: manager-rolebinding
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: aggregated-manager-role
diff --git a/controlplane/kubeadm/config/rbac/aggregated_role.yaml b/controlplane/kubeadm/config/rbac/aggregated_role.yaml
deleted file mode 100644
index 7c07f5a52..000000000
--- a/controlplane/kubeadm/config/rbac/aggregated_role.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  name: aggregated-manager-role
-aggregationRule:
-  clusterRoleSelectors:
-  - matchLabels:
-      kubeadm.controlplane.cluster.x-k8s.io/aggregate-to-manager: "true"
-rules: []
diff --git a/controlplane/kubeadm/config/rbac/kustomization.yaml b/controlplane/kubeadm/config/rbac/kustomization.yaml
index 7c1d8d647..817f1fe61 100644
--- a/controlplane/kubeadm/config/rbac/kustomization.yaml
+++ b/controlplane/kubeadm/config/rbac/kustomization.yaml
@@ -9,4 +9,3 @@ resources:
 - auth_proxy_service.yaml
 - auth_proxy_role.yaml
 - auth_proxy_role_binding.yaml
-- aggregated_role.yaml
diff --git a/docs/book/src/clusterctl/provider-contract.md b/docs/book/src/clusterctl/provider-contract.md
index abe1f9e85..d3aa83c63 100644
--- a/docs/book/src/clusterctl/provider-contract.md
+++ b/docs/book/src/clusterctl/provider-contract.md
@@ -173,7 +173,6 @@ providers.
 |CAPV          | cluster.x-k8s.io/provider=infrastructure-vsphere   |
 |CAPD          | cluster.x-k8s.io/provider=infrastructure-docker    |
 |CAPM3         | cluster.x-k8s.io/provider=infrastructure-metal3    |
-|CAPP          | cluster.x-k8s.io/provider=infrastructure-packet     |
 |CAPZ          | cluster.x-k8s.io/provider=infrastructure-azure     |
 
 ### Workload cluster templates
diff --git a/docs/book/src/user/quick-start.md b/docs/book/src/user/quick-start.md
index 3da74edc0..090e91cae 100644
--- a/docs/book/src/user/quick-start.md
+++ b/docs/book/src/user/quick-start.md
@@ -127,7 +127,7 @@ See [`clusterctl init`](../clusterctl/commands/init.md) for more details.
 Depending on the infrastructure provider you are planning to use, some additional prerequisites should be satisfied
 before getting started with Cluster API.
 
-{{#tabs name:"tab-installation-infrastructure" tabs:"AWS,Azure,Docker,GCP,vSphere,OpenStack,Metal3,Packet"}}
+{{#tabs name:"tab-installation-infrastructure" tabs:"AWS,Azure,Docker,GCP,vSphere,OpenStack,Metal3"}}
 {{#tab AWS}}
 
 Download the latest binary of `clusterawsadm` from the [AWS provider releases] and make sure to place it in your path.
@@ -229,21 +229,6 @@ clusterctl init --infrastructure openstack
 Please visit the [Metal3 project][Metal3 getting started guide].
 
 {{#/tab }}
-{{#tab Packet}}
-
-In order to initialize the Packet Provider you have to expose the environment
-variable `PACKET_API_KEY`. This variable is used to authorize the infrastructure
-provider manager against the Packet API. You can retrieve your token directly
-from the [Packet Portal](https://app.packet.net/).
-
-```bash
-export PACKET_API_KEY="34ts3g4s5g45gd45dhdh"
-
-clusterctl init --infrastructure packet
-```
-
-{{#/tab }}
-
 {{#/tabs }}
 
 
@@ -313,7 +298,7 @@ discover the list of variables required by a cluster templates.
 Depending on the infrastructure provider you are planning to use, some additional prerequisites should be satisfied
 before configuring a cluster with Cluster API.
 
-{{#tabs name:"tab-configuration-infrastructure" tabs:"AWS,Azure,Docker,GCP,vSphere,OpenStack,Metal3,Packet"}}
+{{#tabs name:"tab-configuration-infrastructure" tabs:"AWS,Azure,Docker,GCP,vSphere,OpenStack,Metal3"}}
 {{#tab AWS}}
 
 ```bash
@@ -419,28 +404,6 @@ A full configuration reference can be found in [configuration.md](https://github
 
 Please visit the [Metal3 getting started guide].
 
-{{#/tab }}
-{{#tab Packet}}
-
-There are a couple of required environment variables that you have to expose in
-order to get a well tuned and function workload, they are all listed here:
-
-```bash
-# The project where your cluster will be placed to.
-# You have to get out from Packet Portal if you don't have one already.
-export PROJECT_ID="5yd4thd-5h35-5hwk-1111-125gjej40930"
-# The facility where you want your cluster to be provisioned
-export FACILITY="ewr1"
-# The operatin system used to provision the device
-export NODE_OS="ubuntu_18_04"
-# The ssh key name you loaded in Packet Portal
-export SSH_KEY="my-ssh"
-export POD_CIDR="172.25.0.0/16"
-export SERVICE_CIDR="172.26.0.0/16"
-export MASTER_NODE_TYPE="t1.small"
-export WORKER_NODE_TYPE="t1.small"
-```
-
 {{#/tab }}
 {{#/tabs }}
 
@@ -520,8 +483,8 @@ kubectl --namespace=default get secret/capi-quickstart-kubeconfig -o jsonpath={.
 
 Calico is used here as an example.
 
-{{#tabs name:"tab-deploy-cni" tabs:"AWS|Docker|GCP|vSphere|OpenStack|Metal3|Packet,Azure"}}
-{{#tab AWS|Docker|GCP|vSphere|OpenStack|Metal3|Packet}}
+{{#tabs name:"tab-deploy-cni" tabs:"AWS|Docker|GCP|vSphere|OpenStack|Metal3,Azure"}}
+{{#tab AWS|Docker|GCP|vSphere|OpenStack|Metal3}}
 
 ```bash
 kubectl --kubeconfig=./capi-quickstart.kubeconfig \
@@ -576,7 +539,6 @@ See the [clusterctl] documentation for more detail about clusterctl supported ac
 [kubectl]: https://kubernetes.io/docs/tasks/tools/install-kubectl/
 [management cluster]: ../reference/glossary.md#management-cluster
 [Metal3 getting started guide]: https://github.com/metal3-io/cluster-api-provider-metal3/
-[Packet getting started guide]: https://github.com/packethost/cluster-api-provider-packet#using
 [provider components]: ../reference/glossary.md#provider-components
 [vSphere getting started guide]: https://github.com/kubernetes-sigs/cluster-api-provider-vsphere/
 [workload cluster]: ../reference/glossary.md#workload-cluster
diff --git a/docs/book/theme/css/general.css b/docs/book/theme/css/general.css
index ea6f4f3e4..13579f1e1 100644
--- a/docs/book/theme/css/general.css
+++ b/docs/book/theme/css/general.css
@@ -535,8 +535,7 @@ cite.literate-source > a::before {
 .tabset > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4),
 .tabset > input:nth-child(9):checked ~ .tab-panels > .tab-panel:nth-child(5),
 .tabset > input:nth-child(11):checked ~ .tab-panels > .tab-panel:nth-child(6),
-.tabset > input:nth-child(13):checked ~ .tab-panels > .tab-panel:nth-child(7),
-.tabset > input:nth-child(15):checked ~ .tab-panels > .tab-panel:nth-child(8){
+.tabset > input:nth-child(13):checked ~ .tab-panels > .tab-panel:nth-child(7) {
   display: block;
 }
 
diff --git a/go.mod b/go.mod
index a38b73a1f..f2ab225e1 100644
--- a/go.mod
+++ b/go.mod
@@ -11,27 +11,23 @@ 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/protobuf v1.3.5 // indirect
-	github.com/google/go-cmp v0.4.1
+	github.com/google/go-cmp v0.4.0
 	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.9 // indirect
 	github.com/onsi/ginkgo v1.12.0
 	github.com/onsi/gomega v1.9.0
-	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
 	github.com/pkg/errors v0.9.1
-	github.com/prometheus/client_golang v1.5.1
+	github.com/prometheus/client_golang v1.5.0
 	github.com/prometheus/client_model v0.2.0
-	github.com/prometheus/procfs v0.0.11 // indirect
 	github.com/spf13/cobra v0.0.6
 	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/net v0.0.0-20200301022130-244492dfa37a
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
-	google.golang.org/appengine v1.6.6 // indirect
+	google.golang.org/appengine v1.6.1 // indirect
 	google.golang.org/grpc v1.26.0
 	k8s.io/api v0.17.2
 	k8s.io/apiextensions-apiserver v0.17.2
@@ -41,8 +37,9 @@ require (
 	k8s.io/cluster-bootstrap v0.17.2
 	k8s.io/component-base v0.17.2
 	k8s.io/klog v1.0.0
+	k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c // indirect
 	k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab
-	sigs.k8s.io/controller-runtime v0.5.5
+	sigs.k8s.io/controller-runtime v0.5.3
 	sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802
 	sigs.k8s.io/yaml v1.2.0
 )
diff --git a/go.sum b/go.sum
index 59a7de4f9..a843eb344 100644
--- a/go.sum
+++ b/go.sum
@@ -194,8 +194,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -204,8 +202,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@@ -245,8 +241,6 @@ 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 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=
@@ -254,8 +248,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 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.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
-github.com/imdario/mergo v0.3.9/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=
@@ -344,8 +336,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
-github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
@@ -363,8 +355,8 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
-github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A=
+github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -380,8 +372,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
-github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -468,6 +458,7 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 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=
@@ -532,11 +523,11 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -562,6 +553,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws=
@@ -579,8 +571,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -651,6 +643,8 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM=
+k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab h1:I3f2hcBrepGRXI1z4sukzAb8w1R4eqbsHrAsx06LGYM=
 k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
@@ -659,12 +653,13 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
 modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
 modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
 modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
-sigs.k8s.io/controller-runtime v0.5.5 h1:rDFXh0T6J6gmBmiDOJwNXef3os9fubmyn6aKAx2NI3s=
-sigs.k8s.io/controller-runtime v0.5.5/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A=
+sigs.k8s.io/controller-runtime v0.5.3 h1:HGL1kpWxe6WiiN4yCmYA5SFtvUMQAkOwV+Ylh7M87E4=
+sigs.k8s.io/controller-runtime v0.5.3/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A=
 sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802 h1:L6/8hETA7jvdx3xBcbDifrIN2xaYHE7tA58n+Kdp2Zw=
 sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802/go.mod h1:HIZ3PWUezpklcjkqpFbnYOqaqsAE1JeCTEwkgvPLXjk=
 sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/main.go b/main.go
index d6818c496..a21a13c11 100644
--- a/main.go
+++ b/main.go
@@ -33,6 +33,7 @@ import (
 	clusterv1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/cmd/version"
 	"sigs.k8s.io/cluster-api/controllers"
+	"sigs.k8s.io/cluster-api/controllers/remote"
 	expv1alpha3 "sigs.k8s.io/cluster-api/exp/api/v1alpha3"
 	expcontrollers "sigs.k8s.io/cluster-api/exp/controllers"
 	"sigs.k8s.io/cluster-api/feature"
@@ -194,6 +195,25 @@ func setupReconcilers(mgr ctrl.Manager) {
 		return
 	}
 
+	// Set up a ClusterCacheTracker and ClusterCacheReconciler to provide to controllers
+	// requiring a connection to a remote cluster
+	tracker, err := remote.NewClusterCacheTracker(
+		ctrl.Log.WithName("remote").WithName("ClusterCacheTracker"),
+		mgr,
+	)
+	if err != nil {
+		setupLog.Error(err, "unable to create cluster cache tracker")
+		os.Exit(1)
+	}
+	if err := (&remote.ClusterCacheReconciler{
+		Client:  mgr.GetClient(),
+		Log:     ctrl.Log.WithName("remote").WithName("ClusterCacheReconciler"),
+		Tracker: tracker,
+	}).SetupWithManager(mgr, concurrency(clusterConcurrency)); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "ClusterCacheReconciler")
+		os.Exit(1)
+	}
+
 	if err := (&controllers.ClusterReconciler{
 		Client: mgr.GetClient(),
 		Log:    ctrl.Log.WithName("controllers").WithName("Cluster"),
@@ -233,8 +253,9 @@ func setupReconcilers(mgr ctrl.Manager) {
 		}
 	}
 	if err := (&controllers.MachineHealthCheckReconciler{
-		Client: mgr.GetClient(),
-		Log:    ctrl.Log.WithName("controllers").WithName("MachineHealthCheck"),
+		Client:  mgr.GetClient(),
+		Log:     ctrl.Log.WithName("controllers").WithName("MachineHealthCheck"),
+		Tracker: tracker,
 	}).SetupWithManager(mgr, concurrency(machineHealthCheckConcurrency)); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "MachineHealthCheck")
 		os.Exit(1)
diff --git a/util/conditions/patch.go b/util/conditions/patch.go
index 5181a940d..8e2f4707d 100644
--- a/util/conditions/patch.go
+++ b/util/conditions/patch.go
@@ -89,7 +89,8 @@ func (p Patch) Apply(source Setter) error {
 	for _, conditionPatch := range p {
 		switch conditionPatch.Op {
 		case AddConditionPatch:
-			// If the condition is already on source, check if source and target agree on the change; if not, this is a conflict.
+			// If the condition is already on source, check if source and target agree on the change;
+			// if not, this is a conflict.
 			if sourceCondition := Get(source, conditionPatch.Target.Type); sourceCondition != nil {
 				// If source and target agree on the change, then it is a conflict.
 				if !hasSameState(sourceCondition, conditionPatch.Target) {
@@ -138,8 +139,3 @@ func (p Patch) Apply(source Setter) error {
 	}
 	return nil
 }
-
-// IsZero returns true if the patch has no changes.
-func (p Patch) IsZero() bool {
-	return len(p) == 0
-}
diff --git a/util/conditions/setter.go b/util/conditions/setter.go
index 2ed67abe4..0341a0e18 100644
--- a/util/conditions/setter.go
+++ b/util/conditions/setter.go
@@ -19,7 +19,6 @@ package conditions
 import (
 	"fmt"
 	"sort"
-	"time"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -45,7 +44,7 @@ func Set(to Setter, condition *clusterv1.Condition) {
 	conditions := to.GetConditions()
 	newConditions := make(clusterv1.Conditions, 0, len(conditions))
 	exists := false
-	condition.LastTransitionTime = metav1.NewTime(time.Now().Truncate(time.Second))
+	condition.LastTransitionTime = metav1.Now()
 	for i := range conditions {
 		existingCondition := conditions[i]
 		if existingCondition.Type == condition.Type {
diff --git a/util/patch/options.go b/util/patch/options.go
deleted file mode 100644
index 4b63264a3..000000000
--- a/util/patch/options.go
+++ /dev/null
@@ -1,39 +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 patch
-
-// Option is some configuration that modifies options for a patch request.
-type Option interface {
-	// ApplyToHelper applies this configuration to the given Helper options.
-	ApplyToHelper(*HelperOptions)
-}
-
-// HelperOptions contains options for patch options.
-type HelperOptions struct {
-	// IncludeStatusObservedGeneration sets the status.observedGeneration field
-	// on the incoming object to match metadata.generation, only if there is a change.
-	IncludeStatusObservedGeneration bool
-}
-
-// WithStatusObservedGeneration sets the status.observedGeneration field
-// on the incoming object to match metadata.generation, only if there is a change.
-type WithStatusObservedGeneration struct{}
-
-// ApplyToHelper applies this configuration to the given an List options.
-func (w WithStatusObservedGeneration) ApplyToHelper(in *HelperOptions) {
-	in.IncludeStatusObservedGeneration = true
-}
diff --git a/util/patch/patch.go b/util/patch/patch.go
index b52a0d6dd..91550c39e 100644
--- a/util/patch/patch.go
+++ b/util/patch/patch.go
@@ -18,289 +18,117 @@ package patch
 
 import (
 	"context"
-	"encoding/json"
 	"reflect"
-	"time"
 
 	"github.com/pkg/errors"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	kerrors "k8s.io/apimachinery/pkg/util/errors"
-	"k8s.io/apimachinery/pkg/util/wait"
-	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-// Helper is a utility for ensuring the proper patching of objects.
+// Helper is a utility for ensuring the proper Patching of resources
+// and their status
 type Helper struct {
-	opts *HelperOptions
-
-	client       client.Client
-	beforeObject runtime.Object
-	before       *unstructured.Unstructured
-	after        *unstructured.Unstructured
-	changes      map[string]bool
-
-	isConditionsSetter bool
+	client        client.Client
+	before        map[string]interface{}
+	hasStatus     bool
+	beforeStatus  interface{}
+	resourcePatch client.Patch
+	statusPatch   client.Patch
 }
 
 // NewHelper returns an initialized Helper
-func NewHelper(obj runtime.Object, crClient client.Client, opts ...Option) (*Helper, error) {
-	// Return early if the object is nil.
-	// If you're wondering why we need reflection to do this check, see https://golang.org/doc/faq#nil_error.
-	// TODO(vincepri): Remove this check and let it panic if used improperly in a future minor release.
-	if obj == nil || (reflect.ValueOf(obj).IsValid() && reflect.ValueOf(obj).IsNil()) {
-		return nil, errors.Errorf("expected non-nil object")
+func NewHelper(resource runtime.Object, crClient client.Client) (*Helper, error) {
+	if resource == nil {
+		return nil, errors.Errorf("expected non-nil resource")
 	}
 
-	// Calculate the options to pass to the helper.
-	options := &HelperOptions{}
-	for _, opt := range opts {
-		opt.ApplyToHelper(options)
+	// If the object is already unstructured, we need to perform a deepcopy first
+	// because the `DefaultUnstructuredConverter.ToUnstructured` function returns
+	// the underlying unstructured object map without making a copy.
+	if _, ok := resource.(runtime.Unstructured); ok {
+		resource = resource.DeepCopyObject()
 	}
 
-	// Convert the object to unstructured to compare against our before copy.
-	raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject())
+	// Convert the resource to unstructured for easier comparison later.
+	before, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource)
 	if err != nil {
 		return nil, err
 	}
-	unstructuredObj := &unstructured.Unstructured{Object: raw}
-
-	// Check if the object satisfies the Cluster API conditions contract.
-	_, canInterfaceConditions := obj.(conditions.Setter)
-
-	return &Helper{
-		opts:               options,
-		client:             crClient,
-		before:             unstructuredObj,
-		beforeObject:       obj.DeepCopyObject(),
-		isConditionsSetter: canInterfaceConditions,
-	}, nil
-}
-
-// Patch will attempt to patch the given object, including its status.
-func (h *Helper) Patch(ctx context.Context, obj runtime.Object) error {
-	if obj == nil {
-		return errors.Errorf("expected non-nil object")
-	}
 
-	// Convert the object to unstructured to compare against our before copy.
-	raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj.DeepCopyObject())
+	hasStatus := false
+	// attempt to extract the status from the resource for easier comparison later
+	beforeStatus, ok, err := unstructured.NestedFieldCopy(before, "status")
 	if err != nil {
-		return err
-	}
-	h.after = &unstructured.Unstructured{Object: raw}
-
-	// Determine if the object has status.
-	if unstructuredHasStatus(h.after) {
-		if h.opts.IncludeStatusObservedGeneration {
-			// Set status.observedGeneration if we're asked to do so.
-			if err := unstructured.SetNestedField(h.after.Object, h.after.GetGeneration(), "status", "observedGeneration"); err != nil {
-				return err
-			}
-
-			// Restore the changes back to the original object.
-			if err := runtime.DefaultUnstructuredConverter.FromUnstructured(h.after.Object, obj); err != nil {
-				return err
-			}
-		}
+		return nil, err
 	}
-
-	// Calculate and store the top-level field changes (e.g. "metadata", "spec", "status") we have before/after.
-	h.changes, err = h.calculateChanges(obj)
-	if err != nil {
-		return err
+	if ok {
+		hasStatus = true
+		// if the resource contains a status remove it from our unstructured copy
+		// to avoid unnecessary patching later
+		unstructured.RemoveNestedField(before, "status")
 	}
 
-	// Issue patches and return errors in an aggregate.
-	return kerrors.NewAggregate([]error{
-		h.patch(ctx, obj),
-		h.patchStatus(ctx, obj),
-		h.patchStatusConditions(ctx, obj),
-	})
+	return &Helper{
+		client:        crClient,
+		before:        before,
+		beforeStatus:  beforeStatus,
+		hasStatus:     hasStatus,
+		resourcePatch: client.MergeFrom(resource.DeepCopyObject()),
+		statusPatch:   client.MergeFrom(resource.DeepCopyObject()),
+	}, nil
 }
 
-// patch issues a patch for metadata and spec.
-func (h *Helper) patch(ctx context.Context, obj runtime.Object) error {
-	if !h.shouldPatch("metadata") && !h.shouldPatch("spec") {
-		return nil
-	}
-	beforeObject, afterObject, err := h.calculatePatch(obj, specPatch)
-	if err != nil {
-		return err
+// Patch will attempt to patch the given resource and its status
+func (h *Helper) Patch(ctx context.Context, resource runtime.Object) error {
+	if resource == nil {
+		return errors.Errorf("expected non-nil resource")
 	}
-	return h.client.Patch(ctx, afterObject, client.MergeFrom(beforeObject))
-}
 
-// patchStatus issues a patch if the status has changed.
-func (h *Helper) patchStatus(ctx context.Context, obj runtime.Object) error {
-	if !h.shouldPatch("status") {
-		return nil
+	// If the object is already unstructured, we need to perform a deepcopy first
+	// because the `DefaultUnstructuredConverter.ToUnstructured` function returns
+	// the underlying unstructured object map without making a copy.
+	if _, ok := resource.(runtime.Unstructured); ok {
+		resource = resource.DeepCopyObject()
 	}
-	beforeObject, afterObject, err := h.calculatePatch(obj, statusPatch)
-	if err != nil {
-		return err
-	}
-	return h.client.Status().Patch(ctx, afterObject, client.MergeFrom(beforeObject))
-}
 
-// patchStatusConditions issues a patch if there are any changes to the conditions slice under
-// the status subresource. This is a special case and it's handled separately given that
-// we allow different controllers to act on conditions of the same object.
-//
-// This method has an internal backoff loop. When a conflict is detected, the method
-// asks the Client for the a new version of the object we're trying to patch.
-//
-// Condition changes are then applied to the latest version of the object, and if there are
-// no unresolvable conflicts, the patch is sent again.
-func (h *Helper) patchStatusConditions(ctx context.Context, obj runtime.Object) error {
-	// Nothing to do if the object isn't a condition patcher.
-	if !h.isConditionsSetter {
-		return nil
-	}
-
-	// Create the condition patch that's used later in client.Status.Patch().
-	beforeObject, afterObject, err := h.calculatePatch(obj, statusConditionsPatch)
+	// Convert the resource to unstructured to compare against our before copy.
+	after, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource)
 	if err != nil {
 		return err
 	}
-	conditionsPatch := client.MergeFromWithOptions(beforeObject, client.MergeFromWithOptimisticLock{})
-
-	// Make sure our before/after objects satisfy the proper interface before continuing.
-	//
-	// NOTE: The checks and error below are done so that we don't panic if any of the objects don't satisfy the
-	// interface any longer, although this shouldn't happen because we already check when creating the patcher.
-	before, ok := beforeObject.(conditions.Setter)
-	if !ok {
-		return errors.Errorf("object %s doesn't satisfy conditions.Setter, cannot patch", before.GetObjectKind())
-	}
-	after, ok := afterObject.(conditions.Setter)
-	if !ok {
-		return errors.Errorf("object %s doesn't satisfy conditions.Setter, cannot patch", after.GetObjectKind())
-	}
-
-	// Store the diff from the before/after object, and return early if there are no changes.
-	diff := conditions.NewPatch(
-		before,
-		after,
-	)
-	if diff.IsZero() {
-		return nil
-	}
 
-	// Make a copy of the object and store the key used if we have conflicts.
-	key, err := client.ObjectKeyFromObject(after)
+	hasStatus := false
+	// attempt to extract the status from the resource to compare against our
+	// beforeStatus copy
+	afterStatus, ok, err := unstructured.NestedFieldCopy(after, "status")
 	if err != nil {
 		return err
 	}
-
-	// Define and start a backoff loop to handle conflicts
-	// between controllers working on the same object.
-	//
-	// This has been copied from https://github.com/kubernetes/kubernetes/blob/release-1.16/pkg/controller/controller_utils.go#L86-L88.
-	backoff := wait.Backoff{
-		Steps:    5,
-		Duration: 100 * time.Millisecond,
-		Jitter:   1.0,
+	if ok {
+		hasStatus = true
+		// if the resource contains a status remove it from our unstructured copy
+		// to avoid uneccsary patching.
+		unstructured.RemoveNestedField(after, "status")
 	}
 
-	// Start the backoff loop and return errors if any.
-	return wait.ExponentialBackoff(backoff, func() (bool, error) {
-		// Issue a patch.
-		err := h.client.Status().Patch(ctx, after, conditionsPatch)
-		switch {
-		// Handle conflicts.
-		case apierrors.IsConflict(err):
-			// Get a new copy of the object.
-			if err := h.client.Get(ctx, key, after); err != nil {
-				return false, err
-			}
-
-			// Reset the condition patch before merging conditions.
-			conditionsPatch = client.MergeFromWithOptions(after.DeepCopyObject(), client.MergeFromWithOptimisticLock{})
+	var errs []error
 
-			// Set the condition patch previously created on the new object.
-			if err := diff.Apply(after); err != nil {
-				return false, err
-			}
-
-			// Requeue.
-			return false, nil
-		case err != nil:
-			return false, err
+	if !reflect.DeepEqual(h.before, after) {
+		// only issue a Patch if the before and after resources (minus status) differ
+		if err := h.client.Patch(ctx, resource.DeepCopyObject(), h.resourcePatch); err != nil {
+			errs = append(errs, err)
 		}
-
-		return true, nil
-	})
-}
-
-// calculatePatch returns the before/after objects to be given in a controller-runtime patch, scoped down to the absolute necessary.
-func (h *Helper) calculatePatch(afterObj runtime.Object, focus patchType) (runtime.Object, runtime.Object, error) {
-	// Make a copy of the unstructured objects first.
-	before := h.before.DeepCopy()
-	after := h.after.DeepCopy()
-
-	// Let's loop on the copies of our before/after and remove all the keys we don't need.
-	for _, v := range []*unstructured.Unstructured{before, after} {
-		// Ranges over the keys of the unstructured object, think of this as the very top level of an object
-		// when submitting a yaml to kubectl or a client.
-		//
-		// These would be keys like `apiVersion`, `kind`, `metadata`, `spec`, `status`, etc.
-		for key := range v.Object {
-			// If the current key isn't something we absolutetly need (see the map for reference),
-			// and it's not our current focus, then we should remove it.
-			if key != focus.Key() && !preserveUnstructuredKeys[key] {
-				unstructured.RemoveNestedField(v.Object, key)
-				continue
-			}
-			// If we've determined that we're able to interface with conditions.Setter interface,
-			// when dealing with the status patch, we need to remove it from the list of changes,
-			// so the other method can take care of it.
-			if h.isConditionsSetter && focus == statusPatch {
-				unstructured.RemoveNestedField(v.Object, "status", "conditions")
-			}
-		}
-	}
-
-	// We've now applied all modifications to local unstructured objects,
-	// make copies of the original objects and convert them back.
-	beforeObj := h.beforeObject.DeepCopyObject()
-	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(before.Object, beforeObj); err != nil {
-		return nil, nil, err
-	}
-	afterObj = afterObj.DeepCopyObject()
-	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(after.Object, afterObj); err != nil {
-		return nil, nil, err
 	}
 
-	return beforeObj, afterObj, nil
-}
-
-func (h *Helper) shouldPatch(in string) bool {
-	return h.changes[in]
-}
-
-// calculate changes tries to build a patch from the before/after objects we have
-// and store in a map which top-level fields (e.g. `metadata`, `spec`, `status`, etc.) have changed.
-func (h *Helper) calculateChanges(after runtime.Object) (map[string]bool, error) {
-	// Calculate patch data.
-	patch := client.MergeFrom(h.beforeObject)
-	diff, err := patch.Data(after)
-	if err != nil {
-		return nil, errors.Wrapf(err, "failed to calculate patch data")
-	}
-
-	// Unmarshal patch data into a local map.
-	patchDiff := map[string]interface{}{}
-	if err := json.Unmarshal(diff, &patchDiff); err != nil {
-		return nil, errors.Wrapf(err, "failed to unmarshal patch data into a map")
+	if (h.hasStatus || hasStatus) && !reflect.DeepEqual(h.beforeStatus, afterStatus) {
+		// only issue a Status Patch if the resource has a status and the beforeStatus
+		// and afterStatus copies differ
+		if err := h.client.Status().Patch(ctx, resource.DeepCopyObject(), h.statusPatch); err != nil {
+			errs = append(errs, err)
+		}
 	}
 
-	// Return the map.
-	res := make(map[string]bool, len(patchDiff))
-	for key := range patchDiff {
-		res[key] = true
-	}
-	return res, nil
+	return kerrors.NewAggregate(errs)
 }
diff --git a/util/patch/utils.go b/util/patch/utils.go
deleted file mode 100644
index 492d29475..000000000
--- a/util/patch/utils.go
+++ /dev/null
@@ -1,48 +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 patch
-
-import (
-	"strings"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-)
-
-type patchType string
-
-func (p patchType) Key() string {
-	return strings.Split(string(p), ".")[0]
-}
-
-const (
-	specPatch             patchType = "spec"
-	statusPatch           patchType = "status"
-	statusConditionsPatch patchType = "status.conditions"
-)
-
-var (
-	preserveUnstructuredKeys = map[string]bool{
-		"kind":       true,
-		"apiVersion": true,
-		"metadata":   true,
-	}
-)
-
-func unstructuredHasStatus(u *unstructured.Unstructured) bool {
-	_, ok := u.Object["status"]
-	return ok
-}
diff --git a/util/util.go b/util/util.go
index 858d67c55..5ad7be694 100644
--- a/util/util.go
+++ b/util/util.go
@@ -307,11 +307,7 @@ func ClusterToInfrastructureMapFunc(gvk schema.GroupVersionKind) handler.ToReque
 // GetOwnerMachine returns the Machine object owning the current resource.
 func GetOwnerMachine(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*clusterv1.Machine, error) {
 	for _, ref := range obj.OwnerReferences {
-		gv, err := schema.ParseGroupVersion(ref.APIVersion)
-		if err != nil {
-			return nil, err
-		}
-		if ref.Kind == "Machine" && gv.Group == clusterv1.GroupVersion.Group {
+		if ref.Kind == "Machine" && ref.APIVersion == clusterv1.GroupVersion.String() {
 			return GetMachineByName(ctx, c, obj.Namespace, ref.Name)
 		}
 	}
diff --git a/util/yaml/yaml.go b/util/yaml/yaml.go
index 72961122a..d897d1dd9 100644
--- a/util/yaml/yaml.go
+++ b/util/yaml/yaml.go
@@ -28,11 +28,9 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/runtime/serializer/streaming"
-	apiyaml "k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/apimachinery/pkg/util/yaml"
 	"k8s.io/client-go/kubernetes/scheme"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/yaml"
 )
 
 func ExtractClusterReferences(out *ParseOutput, c *clusterv1.Cluster) (res []*unstructured.Unstructured) {
@@ -153,7 +151,7 @@ func Parse(input ParseInput) (*ParseOutput, error) {
 }
 
 type yamlDecoder struct {
-	reader  *apiyaml.YAMLReader
+	reader  *yaml.YAMLReader
 	decoder runtime.Decoder
 	close   func() error
 }
@@ -181,87 +179,8 @@ func (d *yamlDecoder) Close() error {
 
 func NewYAMLDecoder(r io.ReadCloser) streaming.Decoder {
 	return &yamlDecoder{
-		reader:  apiyaml.NewYAMLReader(bufio.NewReader(r)),
+		reader:  yaml.NewYAMLReader(bufio.NewReader(r)),
 		decoder: scheme.Codecs.UniversalDeserializer(),
 		close:   r.Close,
 	}
 }
-
-// ToUnstructured takes a YAML and converts it to a list of Unstructured objects
-func ToUnstructured(rawyaml []byte) ([]unstructured.Unstructured, error) {
-	var ret []unstructured.Unstructured
-
-	reader := apiyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(rawyaml)))
-	count := 1
-	for {
-		// Read one YAML document at a time, until io.EOF is returned
-		b, err := reader.Read()
-		if err != nil {
-			if err == io.EOF {
-				break
-			}
-			return nil, errors.Wrapf(err, "failed to read yaml")
-		}
-		if len(b) == 0 {
-			break
-		}
-
-		var m map[string]interface{}
-		if err := yaml.Unmarshal(b, &m); err != nil {
-			return nil, errors.Wrapf(err, "failed to unmarshal the %s yaml document: %q", util.Ordinalize(count), string(b))
-		}
-
-		var u unstructured.Unstructured
-		u.SetUnstructuredContent(m)
-
-		// Ignore empty objects.
-		// Empty objects are generated if there are weird things in manifest files like e.g. two --- in a row without a yaml doc in the middle
-		if u.Object == nil {
-			continue
-		}
-
-		ret = append(ret, u)
-		count++
-	}
-
-	return ret, nil
-}
-
-// JoinYaml takes a list of YAML files and join them ensuring
-// each YAML that the yaml separator goes on a new line by adding \n where necessary
-func JoinYaml(yamls ...[]byte) []byte {
-	var yamlSeparator = []byte("---")
-
-	var cr = []byte("\n")
-	var b [][]byte //nolint
-	for _, y := range yamls {
-		if !bytes.HasPrefix(y, cr) {
-			y = append(cr, y...)
-		}
-		if !bytes.HasSuffix(y, cr) {
-
-			y = append(y, cr...)
-		}
-		b = append(b, y)
-	}
-
-	r := bytes.Join(b, yamlSeparator)
-	r = bytes.TrimPrefix(r, cr)
-	r = bytes.TrimSuffix(r, cr)
-
-	return r
-}
-
-// FromUnstructured takes a list of Unstructured objects and converts it into a YAML
-func FromUnstructured(objs []unstructured.Unstructured) ([]byte, error) {
-	var ret [][]byte //nolint
-	for _, o := range objs {
-		content, err := yaml.Marshal(o.UnstructuredContent())
-		if err != nil {
-			return nil, errors.Wrapf(err, "failed to marshal yaml for %s, %s/%s", o.GroupVersionKind(), o.GetNamespace(), o.GetName())
-		}
-		ret = append(ret, content)
-	}
-
-	return JoinYaml(ret...), nil
-}


Test patch:
diff --git a/cmd/clusterctl/client/init_test.go b/cmd/clusterctl/client/init_test.go
index 018012c0e..3847ae1e6 100644
--- a/cmd/clusterctl/client/init_test.go
+++ b/cmd/clusterctl/client/init_test.go
@@ -26,7 +26,7 @@ import (
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
+	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
 )
 
 func Test_clusterctlClient_InitImages(t *testing.T) {
@@ -618,7 +618,7 @@ func componentsYAML(ns string) []byte {
 		"metadata:\n" +
 		"  name: manager")
 
-	return utilyaml.JoinYaml(namespaceYaml, podYaml)
+	return util.JoinYaml(namespaceYaml, podYaml)
 }
 
 func templateYAML(ns string, clusterName string) []byte {
diff --git a/cmd/clusterctl/client/repository/components_client_test.go b/cmd/clusterctl/client/repository/components_client_test.go
index ced53933a..6ab6bd734 100644
--- a/cmd/clusterctl/client/repository/components_client_test.go
+++ b/cmd/clusterctl/client/repository/components_client_test.go
@@ -26,7 +26,7 @@ import (
 	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
-	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
+	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
 )
 
 const (
@@ -96,7 +96,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v1.0.0",
@@ -119,7 +119,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v1.0.0",
@@ -142,7 +142,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(namespaceYaml, controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v1.0.0",
@@ -180,7 +180,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v1.0.0",
@@ -196,7 +196,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v1.0.0",
@@ -219,7 +219,7 @@ func Test_componentsClient_Get(t *testing.T) {
 				repository: test.NewFakeRepository().
 					WithPaths("root", "components.yaml").
 					WithDefaultVersion("v1.0.0").
-					WithFile("v1.0.0", "components.yaml", utilyaml.JoinYaml(controllerYaml, configMapYaml)),
+					WithFile("v1.0.0", "components.yaml", util.JoinYaml(controllerYaml, configMapYaml)),
 			},
 			args: args{
 				version:           "v2.0.0",
diff --git a/cmd/clusterctl/internal/util/yaml_test.go b/cmd/clusterctl/internal/util/yaml_test.go
new file mode 100644
index 000000000..f08026a1c
--- /dev/null
+++ b/cmd/clusterctl/internal/util/yaml_test.go
@@ -0,0 +1,157 @@
+/*
+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 util
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+)
+
+func TestToUnstructured(t *testing.T) {
+	type args struct {
+		rawyaml []byte
+	}
+	tests := []struct {
+		name          string
+		args          args
+		wantObjsCount int
+		wantErr       bool
+		err           string
+	}{
+		{
+			name: "single object",
+			args: args{
+				rawyaml: []byte("apiVersion: v1\n" +
+					"kind: ConfigMap\n"),
+			},
+			wantObjsCount: 1,
+			wantErr:       false,
+		},
+		{
+			name: "multiple objects are detected",
+			args: args{
+				rawyaml: []byte("apiVersion: v1\n" +
+					"kind: ConfigMap\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Secret\n"),
+			},
+			wantObjsCount: 2,
+			wantErr:       false,
+		},
+		{
+			name: "empty object are dropped",
+			args: args{
+				rawyaml: []byte("---\n" + //empty objects before
+					"---\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: ConfigMap\n" +
+					"---\n" + // empty objects in the middle
+					"---\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Secret\n" +
+					"---\n" + //empty objects after
+					"---\n" +
+					"---\n"),
+			},
+			wantObjsCount: 2,
+			wantErr:       false,
+		},
+		{
+			name: "--- in the middle of objects are ignored",
+			args: args{
+				[]byte("apiVersion: v1\n" +
+					"kind: ConfigMap\n" +
+					"data: \n" +
+					" key: |\n" +
+					"  ··Several lines of text,\n" +
+					"  ··with some --- \n" +
+					"  ---\n" +
+					"  ··in the middle\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Secret\n"),
+			},
+			wantObjsCount: 2,
+			wantErr:       false,
+		},
+		{
+			name: "returns error for invalid yaml",
+			args: args{
+				rawyaml: []byte("apiVersion: v1\n" +
+					"kind: ConfigMap\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"foobar\n" +
+					"kind: Secret\n"),
+			},
+			wantErr: true,
+			err:     "failed to unmarshal the 2nd yaml document",
+		},
+		{
+			name: "returns error for invalid yaml",
+			args: args{
+				rawyaml: []byte("apiVersion: v1\n" +
+					"kind: ConfigMap\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Pod\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Deployment\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"foobar\n" +
+					"kind: ConfigMap\n"),
+			},
+			wantErr: true,
+			err:     "failed to unmarshal the 4th yaml document",
+		},
+		{
+			name: "returns error for invalid yaml",
+			args: args{
+				rawyaml: []byte("apiVersion: v1\n" +
+					"foobar\n" +
+					"kind: ConfigMap\n" +
+					"---\n" +
+					"apiVersion: v1\n" +
+					"kind: Secret\n"),
+			},
+			wantErr: true,
+			err:     "failed to unmarshal the 1st yaml document",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			got, err := ToUnstructured(tt.args.rawyaml)
+			if tt.wantErr {
+				g.Expect(err).To(HaveOccurred())
+				if len(tt.err) != 0 {
+					g.Expect(err.Error()).To(ContainSubstring(tt.err))
+				}
+				return
+			}
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(got).To(HaveLen(tt.wantObjsCount))
+		})
+	}
+}
diff --git a/controllers/external/testing.go b/controllers/external/testing.go
index bed8d4b49..762588ce2 100644
--- a/controllers/external/testing.go
+++ b/controllers/external/testing.go
@@ -58,10 +58,6 @@ var (
 									Type:                   "object",
 									XPreserveUnknownFields: pointer.BoolPtr(true),
 								},
-								"status": {
-									Type:                   "object",
-									XPreserveUnknownFields: pointer.BoolPtr(true),
-								},
 							},
 						},
 					},
diff --git a/controllers/machinehealthcheck_controller_test.go b/controllers/machinehealthcheck_controller_test.go
index dcda73fc8..caa22b06f 100644
--- a/controllers/machinehealthcheck_controller_test.go
+++ b/controllers/machinehealthcheck_controller_test.go
@@ -50,6 +50,7 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 
 	var clusterName = "test-cluster"
 	var clusterKubeconfigName = "test-cluster-kubeconfig"
+	var clusterUID types.UID
 	var namespaceName string
 
 	BeforeEach(func() {
@@ -63,6 +64,7 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 		By("Creating the Cluster")
 		testCluster.Namespace = namespaceName
 		Expect(testEnv.Create(ctx, testCluster)).To(Succeed())
+		clusterUID = testCluster.UID
 
 		By("Creating the remote Cluster kubeconfig")
 		Expect(testEnv.CreateKubeconfigSecret(testCluster)).To(Succeed())
@@ -216,6 +218,17 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 			}, timeout).Should(Succeed())
 		}
 
+		// getMHCStatus is a function to be used in Eventually() matchers to check the MHC status
+		getMHCStatus := func(namespace, name string) func() clusterv1.MachineHealthCheckStatus {
+			return func() clusterv1.MachineHealthCheckStatus {
+				mhc := &clusterv1.MachineHealthCheck{}
+				if err := testEnv.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, mhc); err != nil {
+					return clusterv1.MachineHealthCheckStatus{}
+				}
+				return mhc.Status
+			}
+		}
+
 		type reconcileTestCase struct {
 			mhc                 func() *clusterv1.MachineHealthCheck
 			nodes               func() []*corev1.Node
@@ -316,13 +329,7 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				Expect(testEnv.Create(ctx, mhc)).To(Succeed())
 
 				By("Verifying the status has been updated")
-				Eventually(func() clusterv1.MachineHealthCheckStatus {
-					mhc := &clusterv1.MachineHealthCheck{}
-					if err := testEnv.Get(ctx, types.NamespacedName{Namespace: namespaceName, Name: rtc.mhc().Name}, mhc); err != nil {
-						return clusterv1.MachineHealthCheckStatus{}
-					}
-					return mhc.Status
-				}, timeout).Should(Equal(rtc.expectedStatus))
+				Eventually(getMHCStatus(namespaceName, rtc.mhc().Name), timeout).Should(Equal(rtc.expectedStatus))
 
 				// Status has been updated, a reconcile has occurred, should no longer need async assertions
 
@@ -442,6 +449,54 @@ var _ = Describe("MachineHealthCheck Reconciler", func() {
 				expectedStatus:      clusterv1.MachineHealthCheckStatus{ExpectedMachines: 0, CurrentHealthy: 0},
 			}),
 		)
+
+		Context("when a remote Node is modified", func() {
+			It("should react to the updated Node", func() {
+				By("Creating a Node")
+				remoteNode := newTestNode("remote-node-1")
+				remoteNode.Status.Conditions = []corev1.NodeCondition{healthyNodeCondition}
+				Expect(testEnv.Create(ctx, remoteNode)).To(Succeed())
+
+				By("Creating a Machine")
+				// Set up the Machine to reduce events triggered by other controllers updating the Machine
+				clusterOR := metav1.OwnerReference{APIVersion: clusterv1.GroupVersion.String(), Kind: "Cluster", Name: clusterName, UID: clusterUID}
+				remoteMachine := newTestMachine("remote-machine-1", namespaceName, clusterName, remoteNode.Name, labels)
+				remoteMachine.SetOwnerReferences([]metav1.OwnerReference{machineSetOR, clusterOR})
+				now := metav1.NewTime(time.Now())
+				remoteMachine.SetFinalizers([]string{"machine.cluster.x-k8s.io"})
+				remoteMachine.Status.LastUpdated = &now
+				remoteMachine.Status.Phase = "Provisioned"
+				createMachine(remoteMachine)
+
+				By("Creating a MachineHealthCheck")
+				mhc := newTestMachineHealthCheck("remote-test-mhc", namespaceName, clusterName, labels)
+				maxUnhealthy := intstr.Parse("1")
+				mhc.Spec.MaxUnhealthy = &maxUnhealthy
+				mhc.Default()
+				Expect(testEnv.Create(ctx, mhc)).To(Succeed())
+
+				By("Verifying the status has been updated, and the machine is currently healthy")
+				Eventually(getMHCStatus(namespaceName, mhc.Name), timeout).Should(Equal(clusterv1.MachineHealthCheckStatus{ExpectedMachines: 1, CurrentHealthy: 1}))
+				// Make sure the status is stable before making any changes, this allows in-flight reconciles to finish
+				Consistently(getMHCStatus(namespaceName, mhc.Name), 100*time.Millisecond).Should(Equal(clusterv1.MachineHealthCheckStatus{ExpectedMachines: 1, CurrentHealthy: 1}))
+
+				By("Updating the node to make it unhealthy")
+				Eventually(func() error {
+					node := &corev1.Node{}
+					if err := testEnv.Get(ctx, util.ObjectKey(remoteNode), node); err != nil {
+						return err
+					}
+					node.Status.Conditions = []corev1.NodeCondition{unhealthyNodeCondition}
+					if err := testEnv.Status().Update(ctx, node); err != nil {
+						return err
+					}
+					return nil
+				}, timeout).Should(Succeed())
+
+				By("Verifying the status has been updated, and the machine is now unhealthy")
+				Eventually(getMHCStatus(namespaceName, mhc.Name), timeout).Should(Equal(clusterv1.MachineHealthCheckStatus{ExpectedMachines: 1, CurrentHealthy: 0}))
+			})
+		})
 	})
 })
 
diff --git a/controllers/remote/cluster_cache_reconciler_test.go b/controllers/remote/cluster_cache_reconciler_test.go
index 78aa33753..0ecdd1bcd 100644
--- a/controllers/remote/cluster_cache_reconciler_test.go
+++ b/controllers/remote/cluster_cache_reconciler_test.go
@@ -129,13 +129,12 @@ var _ = Describe("ClusterCache Reconciler suite", func() {
 			Expect(k8sClient.Create(ctx, testNamespace)).To(Succeed())
 
 			By("Starting the ClusterCacheReconciler")
-			r, err := NewClusterCacheReconciler(
-				&log.NullLogger{},
-				mgr,
-				controller.Options{},
-				cct,
-			)
-			Expect(err).ToNot(HaveOccurred())
+			r := &ClusterCacheReconciler{
+				Log:     &log.NullLogger{},
+				Client:  mgr.GetClient(),
+				Tracker: cct,
+			}
+			Expect(r.SetupWithManager(mgr, controller.Options{})).To(Succeed())
 
 			By("Creating clusters to test with")
 			clusterRequest1, clusterCache1 = createAndWatchCluster("cluster-1")
diff --git a/controllers/suite_test.go b/controllers/suite_test.go
index 8f7a7c706..eb678b3e8 100644
--- a/controllers/suite_test.go
+++ b/controllers/suite_test.go
@@ -30,6 +30,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
 
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/log"
+	"sigs.k8s.io/cluster-api/controllers/remote"
 	"sigs.k8s.io/cluster-api/test/helpers"
 	// +kubebuilder:scaffold:imports
 )
@@ -66,6 +67,20 @@ var _ = BeforeSuite(func(done Done) {
 	testEnv, err = helpers.NewTestEnvironment()
 	Expect(err).NotTo(HaveOccurred())
 
+	// Set up a ClusterCacheTracker and ClusterCacheReconciler to provide to controllers
+	// requiring a connection to a remote cluster
+	tracker, err := remote.NewClusterCacheTracker(
+		log.Log,
+		testEnv.Manager,
+	)
+	Expect(err).ToNot(HaveOccurred())
+
+	Expect((&remote.ClusterCacheReconciler{
+		Client:  testEnv,
+		Log:     log.Log,
+		Tracker: tracker,
+	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
+
 	clusterReconciler = &ClusterReconciler{
 		Client:   testEnv,
 		Log:      log.Log,
@@ -90,6 +105,7 @@ var _ = BeforeSuite(func(done Done) {
 	Expect((&MachineHealthCheckReconciler{
 		Client:   testEnv,
 		Log:      log.Log,
+		Tracker:  tracker,
 		recorder: testEnv.GetEventRecorderFor("machinehealthcheck-controller"),
 	}).SetupWithManager(testEnv.Manager, controller.Options{MaxConcurrentReconciles: 1})).To(Succeed())
 
diff --git a/test/infrastructure/docker/api/v1alpha3/condition_consts.go b/test/infrastructure/docker/api/v1alpha3/condition_consts.go
deleted file mode 100644
index ab1fb554c..000000000
--- a/test/infrastructure/docker/api/v1alpha3/condition_consts.go
+++ /dev/null
@@ -1,65 +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 v1alpha3
-
-import clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-
-// Conditions and condition Reasons for the DockerMachine object
-
-const ConditionsCount = 2
-
-const (
-	// ContainerProvisionedCondition documents the status of the provisioning of the container
-	// generated by a DockerMachine.
-	//
-	// NOTE: When the container provisioning starts the process completes almost immediately and within
-	// the same reconciliation, so the user will always see a transition from Wait to Provisioned without
-	// having evidence that the operation is started/is in progress.
-	ContainerProvisionedCondition clusterv1.ConditionType = "ContainerProvisioned"
-
-	// WaitingForClusterInfrastructureReason (Severity=Info) documents a DockerMachine waiting for the cluster
-	// infrastructure to be ready before starting to create the container that provides the DockerMachine
-	// infrastructure.
-	WaitingForClusterInfrastructureReason = "WaitingForClusterInfrastructure"
-
-	// WaitingForBootstrapDataReason (Severity=Info) documents a DockerMachine waiting for the bootstrap
-	// script to be ready before starting to create the container that provides the DockerMachine infrastructure.
-	WaitingForBootstrapDataReason = "WaitingForBootstrapData"
-
-	// ContainerProvisioningFailedReason (Severity=Warning) documents a DockerMachine controller detecting
-	// an error while provisioning the container that provides the DockerMachine infrastructure; those kind of
-	// errors are usually transient and failed provisioning are automatically re-tried by the controller.
-	ContainerProvisioningFailedReason = "ContainerProvisioningFailed"
-)
-
-const (
-	// BootstrapExecSucceededCondition provide an observation of the DockerMachine bootstrap process.
-	// The condition gets generated after ContainerProvisionedCondition is True.
-	//
-	// NOTE as a difference from other providers, container provisioning and bootstrap are directly managed
-	// by the DockerMachine controller (not by cloud-init).
-	BootstrapExecSucceededCondition clusterv1.ConditionType = "BootstrapExecSucceeded"
-
-	// BootstrappingReason documents (Severity=Info) a DockerMachine currently executing the bootstrap
-	// script that creates the Kubernetes node on the newly provisioned machine infrastructure.
-	BootstrappingReason = "Bootstrapping"
-
-	// BootstrapFailedReason documents (Severity=Warning) a DockerMachine controller detecting an error while
-	// bootstrapping the Kubernetes node on the machine just provisioned; those kind of errors are usually
-	// transient and failed bootstrap are automatically re-tried by the controller.
-	BootstrapFailedReason = "BootstrapFailed"
-)
diff --git a/test/infrastructure/docker/api/v1alpha3/dockermachine_types.go b/test/infrastructure/docker/api/v1alpha3/dockermachine_types.go
index f5d60d915..d821f96c5 100644
--- a/test/infrastructure/docker/api/v1alpha3/dockermachine_types.go
+++ b/test/infrastructure/docker/api/v1alpha3/dockermachine_types.go
@@ -18,7 +18,6 @@ package v1alpha3
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 )
 
 const (
@@ -80,10 +79,6 @@ type DockerMachineStatus struct {
 	// added to the load balancer
 	// +optional
 	LoadBalancerConfigured bool `json:"loadBalancerConfigured,omitempty"`
-
-	// Conditions defines current service state of the DockerMachine.
-	// +optional
-	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:resource:path=dockermachines,scope=Namespaced,categories=cluster-api
@@ -100,14 +95,6 @@ type DockerMachine struct {
 	Status DockerMachineStatus `json:"status,omitempty"`
 }
 
-func (c *DockerMachine) GetConditions() clusterv1.Conditions {
-	return c.Status.Conditions
-}
-
-func (c *DockerMachine) SetConditions(conditions clusterv1.Conditions) {
-	c.Status.Conditions = conditions
-}
-
 // +kubebuilder:object:root=true
 
 // DockerMachineList contains a list of DockerMachine
diff --git a/test/infrastructure/docker/api/v1alpha3/zz_generated.deepcopy.go b/test/infrastructure/docker/api/v1alpha3/zz_generated.deepcopy.go
index 8c351345a..1ca2eeab5 100644
--- a/test/infrastructure/docker/api/v1alpha3/zz_generated.deepcopy.go
+++ b/test/infrastructure/docker/api/v1alpha3/zz_generated.deepcopy.go
@@ -150,7 +150,7 @@ func (in *DockerMachine) DeepCopyInto(out *DockerMachine) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
-	in.Status.DeepCopyInto(&out.Status)
+	out.Status = in.Status
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DockerMachine.
@@ -236,13 +236,6 @@ func (in *DockerMachineSpec) DeepCopy() *DockerMachineSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DockerMachineStatus) DeepCopyInto(out *DockerMachineStatus) {
 	*out = *in
-	if in.Conditions != nil {
-		in, out := &in.Conditions, &out.Conditions
-		*out = make(apiv1alpha3.Conditions, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DockerMachineStatus.
diff --git a/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachines.yaml b/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachines.yaml
index 5f16c8af6..6f4cc6101 100644
--- a/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachines.yaml
+++ b/test/infrastructure/docker/config/crd/bases/infrastructure.cluster.x-k8s.io_dockermachines.yaml
@@ -82,50 +82,6 @@ spec:
           status:
             description: DockerMachineStatus defines the observed state of DockerMachine
             properties:
-              conditions:
-                description: Conditions defines current service state of the DockerMachine.
-                items:
-                  description: Condition defines an observation of a Cluster API resource
-                    operational state.
-                  properties:
-                    lastTransitionTime:
-                      description: Last time the condition transitioned from one status
-                        to another. This should be when the underlying condition changed.
-                        If that is not known, then using the time when the API field
-                        changed is acceptable.
-                      format: date-time
-                      type: string
-                    message:
-                      description: A human readable message indicating details about
-                        the transition. This field may be empty.
-                      type: string
-                    reason:
-                      description: The reason for the condition's last transition
-                        in CamelCase. The specific API may choose whether or not this
-                        field is considered a guaranteed API. This field may not be
-                        empty.
-                      type: string
-                    severity:
-                      description: Severity provides an explicit classification of
-                        Reason code, so the users or machines can immediately understand
-                        the current situation and act accordingly. The Severity field
-                        MUST be set only when Status=False.
-                      type: string
-                    status:
-                      description: Status of the condition, one of True, False, Unknown.
-                      type: string
-                    type:
-                      description: Type of condition in CamelCase or in foo.example.com/CamelCase.
-                        Many .condition.type values are consistent across resources
-                        like Available, but because arbitrary conditions can be useful
-                        (see .node.status.conditions), the ability to deconflict is
-                        important.
-                      type: string
-                  required:
-                  - status
-                  - type
-                  type: object
-                type: array
               loadBalancerConfigured:
                 description: LoadBalancerConfigured denotes that the machine has been
                   added to the load balancer
diff --git a/test/infrastructure/docker/controllers/dockermachine_controller.go b/test/infrastructure/docker/controllers/dockermachine_controller.go
index f19e3728c..8cea9544d 100644
--- a/test/infrastructure/docker/controllers/dockermachine_controller.go
+++ b/test/infrastructure/docker/controllers/dockermachine_controller.go
@@ -30,7 +30,6 @@ import (
 	infrav1 "sigs.k8s.io/cluster-api/test/infrastructure/docker/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/test/infrastructure/docker/docker"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -96,6 +95,12 @@ func (r *DockerMachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
 
 	log = log.WithValues("cluster", cluster.Name)
 
+	// Make sure infrastructure is ready
+	if !cluster.Status.InfrastructureReady {
+		log.Info("Waiting for DockerCluster Controller to create cluster infrastructure")
+		return ctrl.Result{}, nil
+	}
+
 	// Fetch the Docker Cluster.
 	dockerCluster := &infrav1.DockerCluster{}
 	dockerClusterName := client.ObjectKey{
@@ -109,31 +114,6 @@ func (r *DockerMachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
 
 	log = log.WithValues("docker-cluster", dockerCluster.Name)
 
-	// Initialize the patch helper
-	patchHelper, err := patch.NewHelper(dockerMachine, r)
-	if err != nil {
-		return ctrl.Result{}, err
-	}
-	// Always attempt to Patch the DockerMachine object and status after each reconciliation.
-	defer func() {
-		// always update the readyCondition; the summary is represented using the "1 of x completed" notation.
-		conditions.SetSummary(dockerMachine, conditions.WithStepCounter(infrav1.ConditionsCount))
-
-		if err := patchHelper.Patch(ctx, dockerMachine); err != nil {
-			log.Error(err, "failed to patch DockerMachine")
-			if rerr == nil {
-				rerr = err
-			}
-		}
-	}()
-
-	// Check if the infrastructure is ready, otherwise return and wait for the cluster object to be updated
-	if !cluster.Status.InfrastructureReady {
-		log.Info("Waiting for DockerCluster Controller to create cluster infrastructure")
-		conditions.MarkFalse(dockerMachine, infrav1.ContainerProvisionedCondition, infrav1.WaitingForClusterInfrastructureReason, clusterv1.ConditionSeverityInfo, "")
-		return ctrl.Result{}, nil
-	}
-
 	// Create a helper for managing the docker container hosting the machine.
 	externalMachine, err := docker.NewMachine(cluster.Name, machine.Name, dockerMachine.Spec.CustomImage, log)
 	if err != nil {
@@ -149,6 +129,21 @@ func (r *DockerMachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
 		return ctrl.Result{}, errors.Wrapf(err, "failed to create helper for managing the externalLoadBalancer")
 	}
 
+	// Initialize the patch helper
+	patchHelper, err := patch.NewHelper(dockerMachine, r)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	// Always attempt to Patch the DockerMachine object and status after each reconciliation.
+	defer func() {
+		if err := patchHelper.Patch(ctx, dockerMachine); err != nil {
+			log.Error(err, "failed to patch DockerMachine")
+			if rerr == nil {
+				rerr = err
+			}
+		}
+	}()
+
 	// Handle deleted machines
 	if !dockerMachine.ObjectMeta.DeletionTimestamp.IsZero() {
 		return r.reconcileDelete(ctx, machine, dockerMachine, externalMachine, externalLoadBalancer)
@@ -165,16 +160,14 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, machine *
 	// if the machine is already provisioned, return
 	if dockerMachine.Spec.ProviderID != nil {
 		// ensure ready state is set.
-		// This is required after move, because status is not moved to the target cluster.
+		// This is required after move, bacuse status is not moved to the target cluster.
 		dockerMachine.Status.Ready = true
-		conditions.MarkTrue(dockerMachine, infrav1.ContainerProvisionedCondition)
 		return ctrl.Result{}, nil
 	}
 
 	// Make sure bootstrap data is available and populated.
 	if machine.Spec.Bootstrap.DataSecretName == nil {
 		log.Info("Waiting for the Bootstrap provider controller to set bootstrap data")
-		conditions.MarkFalse(dockerMachine, infrav1.ContainerProvisionedCondition, infrav1.WaitingForBootstrapDataReason, clusterv1.ConditionSeverityInfo, "")
 		return ctrl.Result{}, nil
 	}
 
@@ -192,30 +185,18 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, machine *
 			if err := externalMachine.Delete(ctx); err != nil {
 				log.Info("Failed to cleanup machine")
 			}
-			dockerMachine.Status.LoadBalancerConfigured = false
-			conditions.MarkFalse(dockerMachine, infrav1.ContainerProvisionedCondition, infrav1.ContainerProvisioningFailedReason, clusterv1.ConditionSeverityWarning, "Re-provisioning")
-			conditions.Delete(dockerMachine, infrav1.BootstrapExecSucceededCondition)
-
 			res = ctrl.Result{RequeueAfter: 10 * time.Second}
 			retErr = nil
 		}
 	}()
 
-	// Create the machine if not existing yet
-	if !externalMachine.Exists() {
-		if err := externalMachine.Create(ctx, role, machine.Spec.Version, dockerMachine.Spec.ExtraMounts); err != nil {
-			return ctrl.Result{}, errors.Wrap(err, "failed to create worker DockerMachine")
-		}
+	if err := externalMachine.Create(ctx, role, machine.Spec.Version, dockerMachine.Spec.ExtraMounts); err != nil {
+		return ctrl.Result{}, errors.Wrap(err, "failed to create worker DockerMachine")
 	}
 
-	// Update the ContainerProvisionedCondition condition
-	conditions.MarkTrue(dockerMachine, infrav1.ContainerProvisionedCondition)
-
 	// Preload images into the container
-	if len(dockerMachine.Spec.PreLoadImages) > 0 {
-		if err := externalMachine.PreloadLoadImages(ctx, dockerMachine.Spec.PreLoadImages); err != nil {
-			return ctrl.Result{}, errors.Wrap(err, "failed to pre-load images into the DockerMachine")
-		}
+	if err := externalMachine.PreloadLoadImages(ctx, dockerMachine.Spec.PreLoadImages); err != nil {
+		return ctrl.Result{}, errors.Wrap(err, "failed to pre-load images into the DockerMachine")
 	}
 
 	// if the machine is a control plane update the load balancer configuration
@@ -228,48 +209,34 @@ func (r *DockerMachineReconciler) reconcileNormal(ctx context.Context, machine *
 		dockerMachine.Status.LoadBalancerConfigured = true
 	}
 
-	// At, this stage, we are ready for bootstrap. However, if the BootstrapExecSucceededCondition is missing we add it and we
-	// requeue so the user can see the change of state before the bootstrap actually starts.
-	// NOTE: usually controller should not rely on status they are setting, but on the observed state; however
-	// in this case we are doing this because we explicitly want to give a feedback to users.
-	if !conditions.Has(dockerMachine, infrav1.BootstrapExecSucceededCondition) {
-		conditions.MarkFalse(dockerMachine, infrav1.BootstrapExecSucceededCondition, infrav1.BootstrappingReason, clusterv1.ConditionSeverityInfo, "")
-		return ctrl.Result{Requeue: true}, nil
-	}
-
 	// if the machine isn't bootstrapped, only then run bootstrap scripts
 	if !dockerMachine.Spec.Bootstrapped {
 		bootstrapData, err := r.getBootstrapData(ctx, machine)
 		if err != nil {
 			r.Log.Error(err, "failed to get bootstrap data")
-			return ctrl.Result{}, err
+			return ctrl.Result{}, nil
 		}
 
 		timeoutctx, cancel := context.WithTimeout(ctx, 3*time.Minute)
 		defer cancel()
 		// Run the bootstrap script. Simulates cloud-init.
 		if err := externalMachine.ExecBootstrap(timeoutctx, bootstrapData); err != nil {
-			conditions.MarkFalse(dockerMachine, infrav1.BootstrapExecSucceededCondition, infrav1.BootstrapFailedReason, clusterv1.ConditionSeverityWarning, "Repeating bootstrap")
 			return ctrl.Result{}, errors.Wrap(err, "failed to exec DockerMachine bootstrap")
 		}
 		dockerMachine.Spec.Bootstrapped = true
 	}
 
-	// Update the BootstrapExecSucceededCondition condition
-	conditions.MarkTrue(dockerMachine, infrav1.BootstrapExecSucceededCondition)
-
 	// Usually a cloud provider will do this, but there is no docker-cloud provider.
-	// Requeue if there is an error, as this is likely momentary load balancer
+	// Requeue after 1s if there is an error, as this is likely momentary load balancer
 	// state changes during control plane provisioning.
 	if err := externalMachine.SetNodeProviderID(ctx); err != nil {
 		r.Log.Error(err, "failed to patch the Kubernetes node with the machine providerID")
-		return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
+		return ctrl.Result{RequeueAfter: time.Second}, nil
 	}
 	// Set ProviderID so the Cluster API Machine Controller can pull it
 	providerID := externalMachine.ProviderID()
 	dockerMachine.Spec.ProviderID = &providerID
 	dockerMachine.Status.Ready = true
-	conditions.MarkTrue(dockerMachine, infrav1.ContainerProvisionedCondition)
 
 	return ctrl.Result{}, nil
 }
diff --git a/test/infrastructure/docker/docker/machine.go b/test/infrastructure/docker/docker/machine.go
index 7dbc98892..472e0a1be 100644
--- a/test/infrastructure/docker/docker/machine.go
+++ b/test/infrastructure/docker/docker/machine.go
@@ -89,11 +89,6 @@ func NewMachine(cluster, machine, image string, logger logr.Logger) (*Machine, e
 	}, nil
 }
 
-// Exists returns true if the container for this machine exists.
-func (m *Machine) Exists() bool {
-	return m.container != nil
-}
-
 // ContainerName return the name of the container for this machine
 func (m *Machine) ContainerName() string {
 	return machineContainerName(m.cluster, m.machine)
diff --git a/test/infrastructure/docker/go.mod b/test/infrastructure/docker/go.mod
index 83ea02c76..f67925657 100644
--- a/test/infrastructure/docker/go.mod
+++ b/test/infrastructure/docker/go.mod
@@ -11,7 +11,7 @@ require (
 	k8s.io/client-go v0.17.2
 	k8s.io/klog v1.0.0
 	sigs.k8s.io/cluster-api v0.3.3
-	sigs.k8s.io/controller-runtime v0.5.5
+	sigs.k8s.io/controller-runtime v0.5.3
 	sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802
 	sigs.k8s.io/yaml v1.2.0
 )
diff --git a/test/infrastructure/docker/go.sum b/test/infrastructure/docker/go.sum
index b243e722f..cd857bea2 100644
--- a/test/infrastructure/docker/go.sum
+++ b/test/infrastructure/docker/go.sum
@@ -168,8 +168,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -177,8 +175,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
@@ -209,16 +205,12 @@ 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.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
-github.com/imdario/mergo v0.3.9/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=
@@ -298,8 +290,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
-github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
@@ -316,8 +308,8 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
-github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.5.0 h1:Ctq0iGpCmr3jeP77kbF2UxgvRwzWWz+4Bh9/vJTyg1A=
+github.com/prometheus/client_golang v1.5.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -333,8 +325,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
-github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -403,6 +393,7 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 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=
@@ -466,11 +457,11 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -496,6 +487,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -512,8 +504,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -578,6 +570,8 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM=
+k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab h1:I3f2hcBrepGRXI1z4sukzAb8w1R4eqbsHrAsx06LGYM=
 k8s.io/utils v0.0.0-20200229041039-0a110f9eb7ab/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
@@ -586,12 +580,13 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
 modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
 modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
 modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
-sigs.k8s.io/controller-runtime v0.5.5 h1:rDFXh0T6J6gmBmiDOJwNXef3os9fubmyn6aKAx2NI3s=
-sigs.k8s.io/controller-runtime v0.5.5/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A=
+sigs.k8s.io/controller-runtime v0.5.3 h1:HGL1kpWxe6WiiN4yCmYA5SFtvUMQAkOwV+Ylh7M87E4=
+sigs.k8s.io/controller-runtime v0.5.3/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A=
 sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802 h1:L6/8hETA7jvdx3xBcbDifrIN2xaYHE7tA58n+Kdp2Zw=
 sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802/go.mod h1:HIZ3PWUezpklcjkqpFbnYOqaqsAE1JeCTEwkgvPLXjk=
 sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
+sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/util/patch/patch_test.go b/util/patch/patch_test.go
index 0bd07ba04..814ce15f5 100644
--- a/util/patch/patch_test.go
+++ b/util/patch/patch_test.go
@@ -17,608 +17,232 @@ limitations under the License.
 package patch
 
 import (
-	"reflect"
+	"context"
 	"testing"
 
-	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
-	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/utils/pointer"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
 	"sigs.k8s.io/cluster-api/controllers/external"
-	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-var _ = Describe("Patch Helper", func() {
-
-	It("Should patch an unstructured object", func() {
-		obj := &unstructured.Unstructured{
-			Object: map[string]interface{}{
-				"kind":       "BootstrapMachine",
-				"apiVersion": "bootstrap.cluster.x-k8s.io/v1alpha3",
-				"metadata": map[string]interface{}{
-					"generateName": "test-bootstrap-",
-					"namespace":    "default",
-				},
+func TestHelperUnstructuredPatch(t *testing.T) {
+	g := NewWithT(t)
+	ctx := context.TODO()
+
+	obj := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"kind":       "BootstrapConfig",
+			"apiVersion": "bootstrap.cluster.x-k8s.io/v1alpha3",
+			"metadata": map[string]interface{}{
+				"name":      "test-bootstrap",
+				"namespace": "default",
 			},
-		}
-
-		Context("adding an owner reference, preserving its status", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the unstructured object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.GetName(), Namespace: obj.GetNamespace()}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-			obj.Object["status"] = map[string]interface{}{
+			"status": map[string]interface{}{
 				"ready": true,
-			}
-			Expect(testEnv.Status().Update(ctx, obj)).To(Succeed())
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
+			},
+		},
+	}
+	fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme)
+	g.Expect(fakeClient.Create(ctx, obj)).To(Succeed())
+
+	h, err := NewHelper(obj, fakeClient)
+	g.Expect(err).NotTo(HaveOccurred())
+
+	refs := []metav1.OwnerReference{
+		{
+			APIVersion: "cluster.x-k8s.io/v1alpha3",
+			Kind:       "Cluster",
+			Name:       "test",
+		},
+	}
+	obj.SetOwnerReferences(refs)
+
+	g.Expect(h.Patch(ctx, obj)).To(Succeed())
+
+	// Make sure that the status has been preserved.
+	ready, err := external.IsReady(obj)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(ready).To(BeTrue())
+
+	// Make sure that the object has been patched properly.
+	afterObj := obj.DeepCopy()
+	g.Expect(fakeClient.Get(ctx, client.ObjectKey{Namespace: "default", Name: "test-bootstrap"}, afterObj)).To(Succeed())
+	g.Expect(afterObj.GetOwnerReferences()).To(Equal(refs))
+}
 
-			By("Modifying the OwnerReferences")
-			refs := []metav1.OwnerReference{
-				{
-					APIVersion: "cluster.x-k8s.io/v1alpha3",
-					Kind:       "Cluster",
-					Name:       "test",
-					UID:        types.UID("fake-uid"),
+func TestHelperPatch(t *testing.T) {
+
+	tests := []struct {
+		name    string
+		before  runtime.Object
+		after   runtime.Object
+		wantErr bool
+	}{
+		{
+			name: "Only remove finalizer update",
+			before: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+					Finalizers: []string{
+						clusterv1.ClusterFinalizer,
+					},
 				},
-			}
-			obj.SetOwnerReferences(refs)
-
-			By("Patching the unstructured object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating that the status has been preserved")
-			ready, err := external.IsReady(obj)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(ready).To(BeTrue())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return reflect.DeepEqual(obj.GetOwnerReferences(), objAfter.GetOwnerReferences())
-			}, timeout).Should(BeTrue())
-		})
-	})
-
-	Describe("Should patch conditions", func() {
-		Specify("on a corev1.Node object", func() {
-			conditionTime := metav1.Date(2015, 1, 1, 12, 0, 0, 0, metav1.Now().Location())
-
-			obj := &corev1.Node{
+			},
+			after: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+			},
+		},
+		{
+			name: "Only status update",
+			before: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+			},
+			after: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+				Status: clusterv1.ClusterStatus{
+					InfrastructureReady: true,
+				},
+			},
+		},
+		{
+			name: "Only spec update",
+			before: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+			},
+			after: &clusterv1.Cluster{
 				ObjectMeta: metav1.ObjectMeta{
-					GenerateName: "node-patch-test-",
-					Annotations: map[string]string{
-						"test": "1",
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+				Spec: clusterv1.ClusterSpec{
+					InfrastructureRef: &corev1.ObjectReference{
+						Kind:      "test-kind",
+						Name:      "test-ref",
+						Namespace: "test-namespace",
 					},
 				},
-			}
-
-			By("Creating a Node object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.GetName()}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Appending a new condition")
-			condition := corev1.NodeCondition{
-				Type:               "CustomCondition",
-				Status:             corev1.ConditionTrue,
-				LastHeartbeatTime:  conditionTime,
-				LastTransitionTime: conditionTime,
-				Reason:             "reason",
-				Message:            "message",
-			}
-			obj.Status.Conditions = append(obj.Status.Conditions, condition)
-
-			By("Patching the Node")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				Expect(testEnv.Get(ctx, key, objAfter)).To(Succeed())
-
-				ok, _ := ContainElement(condition).Match(objAfter.Status.Conditions)
-				return ok
-			}, timeout).Should(BeTrue())
-		})
-
-		Describe("on a clusterv1.Cluster object", func() {
-			obj := &clusterv1.Cluster{
+			},
+		},
+		{
+			name: "Both spec and status update",
+			before: &clusterv1.Cluster{
 				ObjectMeta: metav1.ObjectMeta{
-					GenerateName: "test-",
-					Namespace:    "default",
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
 				},
-			}
-
-			Specify("should mark it ready", func() {
-				obj := obj.DeepCopy()
-
-				By("Creating the object")
-				Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-				key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-				defer func() {
-					Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-				}()
-
-				By("Creating a new patch helper")
-				patcher, err := NewHelper(obj, testEnv)
-				Expect(err).NotTo(HaveOccurred())
-
-				By("Marking Ready=True")
-				conditions.MarkTrue(obj, clusterv1.ReadyCondition)
-
-				By("Patching the object")
-				Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-				By("Validating the object has been updated")
-				Eventually(func() bool {
-					objAfter := obj.DeepCopy()
-					if err := testEnv.Get(ctx, key, objAfter); err != nil {
-						return false
-					}
-					return reflect.DeepEqual(obj.Status.Conditions, objAfter.Status.Conditions)
-				}, timeout).Should(BeTrue())
-			})
-
-			Specify("should recover if there is a resolvable conflict", func() {
-				obj := obj.DeepCopy()
-
-				By("Creating the object")
-				Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-				key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-				defer func() {
-					Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-				}()
-				objCopy := obj.DeepCopy()
-
-				By("Marking a custom condition to be false")
-				conditions.MarkFalse(objCopy, clusterv1.ConditionType("TestCondition"), "reason", clusterv1.ConditionSeverityInfo, "message")
-				Expect(testEnv.Status().Update(ctx, objCopy)).To(Succeed())
-
-				By("Validating that the local object's resource version is behind")
-				Expect(obj.ResourceVersion).ToNot(Equal(objCopy.ResourceVersion))
-
-				By("Creating a new patch helper")
-				patcher, err := NewHelper(obj, testEnv)
-				Expect(err).NotTo(HaveOccurred())
-
-				By("Marking Ready=True")
-				conditions.MarkTrue(obj, clusterv1.ReadyCondition)
-
-				By("Patching the object")
-				Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-				By("Validating the object has been updated")
-				Eventually(func() bool {
-					objAfter := obj.DeepCopy()
-					if err := testEnv.Get(ctx, key, objAfter); err != nil {
-						return false
-					}
-					ok, _ := ContainElements(obj.Status.Conditions[0], objCopy.Status.Conditions[0]).Match(objAfter.Status.Conditions)
-					return ok
-				}, timeout).Should(BeTrue())
-			})
-
-			Specify("should recover if there is a resolvable conflict, incl. patch spec and status", func() {
-				obj := obj.DeepCopy()
-
-				By("Creating the object")
-				Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-				key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-				defer func() {
-					Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-				}()
-				objCopy := obj.DeepCopy()
-
-				By("Marking a custom condition to be false")
-				conditions.MarkFalse(objCopy, clusterv1.ConditionType("TestCondition"), "reason", clusterv1.ConditionSeverityInfo, "message")
-				Expect(testEnv.Status().Update(ctx, objCopy)).To(Succeed())
-
-				By("Validating that the local object's resource version is behind")
-				Expect(obj.ResourceVersion).ToNot(Equal(objCopy.ResourceVersion))
-
-				By("Creating a new patch helper")
-				patcher, err := NewHelper(obj, testEnv)
-				Expect(err).NotTo(HaveOccurred())
-
-				By("Changing the object spec, status, and adding Ready=True condition")
-				obj.Spec.Paused = true
-				obj.Spec.ControlPlaneEndpoint.Host = "test://endpoint"
-				obj.Spec.ControlPlaneEndpoint.Port = 8443
-				obj.Status.Phase = "custom-phase"
-				conditions.MarkTrue(obj, clusterv1.ReadyCondition)
-
-				By("Patching the object")
-				Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-				By("Validating the object has been updated")
-				Eventually(func() bool {
-					objAfter := obj.DeepCopy()
-					if err := testEnv.Get(ctx, key, objAfter); err != nil {
-						return false
-					}
-
-					ok, _ := ContainElements(obj.Status.Conditions[0], objCopy.Status.Conditions[0]).Match(objAfter.Status.Conditions)
-					return ok &&
-						obj.Spec.Paused == objAfter.Spec.Paused &&
-						obj.Spec.ControlPlaneEndpoint == objAfter.Spec.ControlPlaneEndpoint &&
-						obj.Status.Phase == objAfter.Status.Phase
-				}, timeout).Should(BeTrue())
-			})
-
-			Specify("should return an error if there is an unresolvable conflict", func() {
-				obj := obj.DeepCopy()
-
-				By("Creating the object")
-				Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-				key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-				defer func() {
-					Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-				}()
-				objCopy := obj.DeepCopy()
-
-				By("Marking a custom condition to be false")
-				conditions.MarkFalse(objCopy, clusterv1.ReadyCondition, "reason", clusterv1.ConditionSeverityInfo, "message")
-				Expect(testEnv.Status().Update(ctx, objCopy)).To(Succeed())
-
-				By("Validating that the local object's resource version is behind")
-				Expect(obj.ResourceVersion).ToNot(Equal(objCopy.ResourceVersion))
-
-				By("Creating a new patch helper")
-				patcher, err := NewHelper(obj, testEnv)
-				Expect(err).NotTo(HaveOccurred())
-
-				By("Marking Ready=True")
-				conditions.MarkTrue(obj, clusterv1.ReadyCondition)
-
-				By("Patching the object")
-				Expect(patcher.Patch(ctx, obj)).ToNot(Succeed())
-
-				By("Validating the object has not been updated")
-				Eventually(func() bool {
-					objAfter := obj.DeepCopy()
-					if err := testEnv.Get(ctx, key, objAfter); err != nil {
-						return false
-					}
-					ok, _ := ContainElement(objCopy.Status.Conditions[0]).Match(objAfter.Status.Conditions)
-					return ok
-				}, timeout).Should(BeTrue())
-			})
-
-		})
-	})
-
-	Describe("Should patch a clusterv1.Cluster", func() {
-		obj := &clusterv1.Cluster{
-			ObjectMeta: metav1.ObjectMeta{
-				GenerateName: "test-",
-				Namespace:    "default",
 			},
-		}
-
-		Specify("add a finalizers", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Adding a finalizer")
-			obj.Finalizers = append(obj.Finalizers, clusterv1.ClusterFinalizer)
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return reflect.DeepEqual(obj.Finalizers, objAfter.Finalizers)
-			}, timeout).Should(BeTrue())
-		})
-
-		Specify("removing finalizers", func() {
-			obj := obj.DeepCopy()
-			obj.Finalizers = append(obj.Finalizers, clusterv1.ClusterFinalizer)
-
-			By("Creating the object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Removing the finalizers")
-			obj.SetFinalizers(nil)
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return len(objAfter.Finalizers) == 0
-			}, timeout).Should(BeTrue())
-		})
-
-		Specify("updating spec", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Updating the object spec")
-			obj.Spec.Paused = true
-			obj.Spec.InfrastructureRef = &corev1.ObjectReference{
-				Kind:      "test-kind",
-				Name:      "test-ref",
-				Namespace: "test-namespace",
-			}
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return objAfter.Spec.Paused == true &&
-					reflect.DeepEqual(obj.Spec.InfrastructureRef, objAfter.Spec.InfrastructureRef)
-			}, timeout).Should(BeTrue())
-		})
-
-		Specify("updating status", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Updating the object status")
-			obj.Status.InfrastructureReady = true
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-				return reflect.DeepEqual(objAfter.Status, obj.Status)
-			}, timeout).Should(BeTrue())
-		})
-
-		Specify("updating both spec and status", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv)
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Updating the object spec")
-			obj.Spec.Paused = true
-			obj.Spec.InfrastructureRef = &corev1.ObjectReference{
-				Kind:      "test-kind",
-				Name:      "test-ref",
-				Namespace: "test-namespace",
-			}
-
-			By("Updating the object status")
-			obj.Status.InfrastructureReady = true
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return reflect.DeepEqual(obj.Status, objAfter.Status) &&
-					reflect.DeepEqual(obj.Spec, objAfter.Spec)
-			}, timeout).Should(BeTrue())
-		})
-	})
-
-	It("Should update Status.ObservedGeneration when using WithStatusObservedGeneration option", func() {
-		obj := &clusterv1.MachineSet{
-			ObjectMeta: metav1.ObjectMeta{
-				GenerateName: "test-ms",
-				Namespace:    "test-namespace",
+			after: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+				Spec: clusterv1.ClusterSpec{
+					InfrastructureRef: &corev1.ObjectReference{
+						Kind:      "test-kind",
+						Name:      "test-ref",
+						Namespace: "test-namespace",
+					},
+				},
+				Status: clusterv1.ClusterStatus{
+					InfrastructureReady: true,
+				},
 			},
-			Spec: clusterv1.MachineSetSpec{
-				ClusterName: "test1",
-				Template: clusterv1.MachineTemplateSpec{
-					Spec: clusterv1.MachineSpec{
-						ClusterName: "test1",
+		},
+		{
+			name: "Only add finalizer update",
+			before: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+				},
+			},
+			after: &clusterv1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-cluster",
+					Namespace: "test-namespace",
+					Finalizers: []string{
+						clusterv1.ClusterFinalizer,
 					},
 				},
 			},
-		}
-
-		Context("when updating spec", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the MachineSet object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv, WithStatusObservedGeneration{})
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Updating the object spec")
-			obj.Spec.Replicas = pointer.Int32Ptr(10)
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return reflect.DeepEqual(obj.Spec, objAfter.Spec) &&
-					obj.GetGeneration() == objAfter.Status.ObservedGeneration
-			}, timeout).Should(BeTrue())
-		})
-
-		Context("when updating spec, status, and metadata", func() {
-			obj := obj.DeepCopy()
+		},
+		{
+			name: "Only add ownerref update to unstructured object",
+			before: &unstructured.Unstructured{
+				Object: map[string]interface{}{
+					"kind":       "BootstrapConfig",
+					"apiVersion": "bootstrap.cluster.x-k8s.io/v1alpha3",
+					"metadata": map[string]interface{}{
+						"name":      "test-bootstrap",
+						"namespace": "default",
+					},
+				},
+			},
+			after: &unstructured.Unstructured{
+				Object: map[string]interface{}{
+					"kind":       "BootstrapConfig",
+					"apiVersion": "bootstrap.cluster.x-k8s.io/v1alpha3",
+					"metadata": map[string]interface{}{
+						"name":      "test-bootstrap",
+						"namespace": "default",
+						"ownerReferences": []interface{}{
+							map[string]interface{}{
+								"kind":       "TestOwner",
+								"apiVersion": "test.cluster.x-k8s.io/v1alpha3",
+								"name":       "test",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
 
-			By("Creating the MachineSet object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
+			g.Expect(clusterv1.AddToScheme(scheme.Scheme)).To(Succeed())
 
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv, WithStatusObservedGeneration{})
-			Expect(err).NotTo(HaveOccurred())
+			ctx := context.Background()
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme)
 
-			By("Updating the object spec")
-			obj.Spec.Replicas = pointer.Int32Ptr(10)
+			beforeCopy := tt.before.DeepCopyObject()
+			g.Expect(fakeClient.Create(ctx, beforeCopy)).To(Succeed())
 
-			By("Updating the object status")
-			obj.Status.AvailableReplicas = 6
-			obj.Status.ReadyReplicas = 6
+			h, err := NewHelper(beforeCopy, fakeClient)
+			g.Expect(err).NotTo(HaveOccurred())
 
-			By("Updating the object metadata")
-			obj.ObjectMeta.Annotations = map[string]string{
-				"test1": "annotation",
+			afterCopy := tt.after.DeepCopyObject()
+			err = h.Patch(ctx, afterCopy)
+			if tt.wantErr {
+				g.Expect(err).To(HaveOccurred())
+			} else {
+				g.Expect(err).NotTo(HaveOccurred())
 			}
 
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return reflect.DeepEqual(obj.Spec, objAfter.Spec) &&
-					reflect.DeepEqual(obj.Status, objAfter.Status) &&
-					obj.GetGeneration() == objAfter.Status.ObservedGeneration
-			}, timeout).Should(BeTrue())
-		})
-
-		Context("without any changes", func() {
-			obj := obj.DeepCopy()
-
-			By("Creating the MachineSet object")
-			Expect(testEnv.Create(ctx, obj)).ToNot(HaveOccurred())
-			key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
-			defer func() {
-				Expect(testEnv.Delete(ctx, obj)).To(Succeed())
-			}()
-			obj.Status.ObservedGeneration = obj.GetGeneration()
-			lastGeneration := obj.GetGeneration()
-			Expect(testEnv.Status().Update(ctx, obj))
-
-			By("Creating a new patch helper")
-			patcher, err := NewHelper(obj, testEnv, WithStatusObservedGeneration{})
-			Expect(err).NotTo(HaveOccurred())
-
-			By("Patching the object")
-			Expect(patcher.Patch(ctx, obj)).To(Succeed())
-
-			By("Validating the object has been updated")
-			Eventually(func() bool {
-				objAfter := obj.DeepCopy()
-				if err := testEnv.Get(ctx, key, objAfter); err != nil {
-					return false
-				}
-
-				return lastGeneration == objAfter.Status.ObservedGeneration
-			}, timeout).Should(BeTrue())
+			g.Expect(afterCopy).To(Equal(tt.after))
 		})
-	})
-})
-
-func TestNewHelperNil(t *testing.T) {
-	var x *appsv1.Deployment
-	g := NewWithT(t)
-	_, err := NewHelper(x, nil)
-	g.Expect(err).ToNot(BeNil())
-	_, err = NewHelper(nil, nil)
-	g.Expect(err).ToNot(BeNil())
+	}
 }
diff --git a/util/patch/suite_test.go b/util/patch/suite_test.go
deleted file mode 100644
index 22991fc1b..000000000
--- a/util/patch/suite_test.go
+++ /dev/null
@@ -1,80 +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 patch
-
-import (
-	"context"
-	"testing"
-	"time"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-
-	"k8s.io/klog"
-	"k8s.io/klog/klogr"
-	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
-
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/log"
-	"sigs.k8s.io/cluster-api/test/helpers"
-	// +kubebuilder:scaffold:imports
-)
-
-// These tests use Ginkgo (BDD-style Go testing framework). Refer to
-// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
-
-func init() {
-	klog.InitFlags(nil)
-	log.SetLogger(klogr.New())
-}
-
-const (
-	timeout = time.Second * 3
-)
-
-var (
-	testEnv *helpers.TestEnvironment
-	ctx     = context.Background()
-)
-
-func TestPatch(t *testing.T) {
-	RegisterFailHandler(Fail)
-
-	RunSpecsWithDefaultAndCustomReporters(t,
-		"Controller Suite",
-		[]Reporter{printer.NewlineReporter{}})
-}
-
-var _ = BeforeSuite(func(done Done) {
-	By("bootstrapping test environment")
-	var err error
-	testEnv, err = helpers.NewTestEnvironment()
-	Expect(err).NotTo(HaveOccurred())
-
-	By("starting the manager")
-	go func() {
-		Expect(testEnv.StartManager()).To(Succeed())
-	}()
-
-	close(done)
-}, 60)
-
-var _ = AfterSuite(func() {
-	if testEnv != nil {
-		By("tearing down the test environment")
-		Expect(testEnv.Stop()).To(Succeed())
-	}
-})
diff --git a/util/util_test.go b/util/util_test.go
index dfb64b614..021c185c3 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -27,7 +27,6 @@ import (
 	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"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -486,36 +485,6 @@ func TestGetOwnerMachineSuccessByName(t *testing.T) {
 	g.Expect(machine).NotTo(BeNil())
 }
 
-func TestGetOwnerMachineSuccessByNameFromDifferentVersion(t *testing.T) {
-	g := NewWithT(t)
-
-	scheme := runtime.NewScheme()
-	g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed())
-
-	myMachine := &clusterv1.Machine{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "my-machine",
-			Namespace: "my-ns",
-		},
-	}
-
-	c := fake.NewFakeClientWithScheme(scheme, myMachine)
-	objm := metav1.ObjectMeta{
-		OwnerReferences: []metav1.OwnerReference{
-			{
-				Kind:       "Machine",
-				APIVersion: clusterv1.GroupVersion.Group + "/v1alpha2",
-				Name:       "my-machine",
-			},
-		},
-		Namespace: "my-ns",
-		Name:      "my-resource-owned-by-machine",
-	}
-	machine, err := GetOwnerMachine(context.TODO(), c, objm)
-	g.Expect(err).NotTo(HaveOccurred())
-	g.Expect(machine).NotTo(BeNil())
-}
-
 func TestGetMachinesForCluster(t *testing.T) {
 	g := NewWithT(t)
 
diff --git a/util/yaml/yaml_test.go b/util/yaml/yaml_test.go
index 65e91ab6f..83cd97956 100644
--- a/util/yaml/yaml_test.go
+++ b/util/yaml/yaml_test.go
@@ -18,13 +18,11 @@ package yaml
 
 import (
 	"io/ioutil"
+	kerrors "k8s.io/apimachinery/pkg/util/errors"
 	"os"
 	"testing"
 
 	. "github.com/onsi/gomega"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	kerrors "k8s.io/apimachinery/pkg/util/errors"
 )
 
 const validCluster = `
@@ -316,154 +314,3 @@ func createTempFile(contents string) (filename string, reterr error) {
 	_, _ = f.WriteString(contents)
 	return f.Name(), nil
 }
-
-func TestToUnstructured(t *testing.T) {
-	type args struct {
-		rawyaml []byte
-	}
-	tests := []struct {
-		name          string
-		args          args
-		wantObjsCount int
-		wantErr       bool
-		err           string
-	}{
-		{
-			name: "single object",
-			args: args{
-				rawyaml: []byte("apiVersion: v1\n" +
-					"kind: ConfigMap\n"),
-			},
-			wantObjsCount: 1,
-			wantErr:       false,
-		},
-		{
-			name: "multiple objects are detected",
-			args: args{
-				rawyaml: []byte("apiVersion: v1\n" +
-					"kind: ConfigMap\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Secret\n"),
-			},
-			wantObjsCount: 2,
-			wantErr:       false,
-		},
-		{
-			name: "empty object are dropped",
-			args: args{
-				rawyaml: []byte("---\n" + //empty objects before
-					"---\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: ConfigMap\n" +
-					"---\n" + // empty objects in the middle
-					"---\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Secret\n" +
-					"---\n" + //empty objects after
-					"---\n" +
-					"---\n"),
-			},
-			wantObjsCount: 2,
-			wantErr:       false,
-		},
-		{
-			name: "--- in the middle of objects are ignored",
-			args: args{
-				[]byte("apiVersion: v1\n" +
-					"kind: ConfigMap\n" +
-					"data: \n" +
-					" key: |\n" +
-					"  ··Several lines of text,\n" +
-					"  ··with some --- \n" +
-					"  ---\n" +
-					"  ··in the middle\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Secret\n"),
-			},
-			wantObjsCount: 2,
-			wantErr:       false,
-		},
-		{
-			name: "returns error for invalid yaml",
-			args: args{
-				rawyaml: []byte("apiVersion: v1\n" +
-					"kind: ConfigMap\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"foobar\n" +
-					"kind: Secret\n"),
-			},
-			wantErr: true,
-			err:     "failed to unmarshal the 2nd yaml document",
-		},
-		{
-			name: "returns error for invalid yaml",
-			args: args{
-				rawyaml: []byte("apiVersion: v1\n" +
-					"kind: ConfigMap\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Pod\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Deployment\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"foobar\n" +
-					"kind: ConfigMap\n"),
-			},
-			wantErr: true,
-			err:     "failed to unmarshal the 4th yaml document",
-		},
-		{
-			name: "returns error for invalid yaml",
-			args: args{
-				rawyaml: []byte("apiVersion: v1\n" +
-					"foobar\n" +
-					"kind: ConfigMap\n" +
-					"---\n" +
-					"apiVersion: v1\n" +
-					"kind: Secret\n"),
-			},
-			wantErr: true,
-			err:     "failed to unmarshal the 1st yaml document",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			got, err := ToUnstructured(tt.args.rawyaml)
-			if tt.wantErr {
-				g.Expect(err).To(HaveOccurred())
-				if len(tt.err) != 0 {
-					g.Expect(err.Error()).To(ContainSubstring(tt.err))
-				}
-				return
-			}
-			g.Expect(err).ToNot(HaveOccurred())
-			g.Expect(got).To(HaveLen(tt.wantObjsCount))
-		})
-	}
-}
-
-func TestFromUnstructured(t *testing.T) {
-	rawyaml := []byte("apiVersion: v1\n" +
-		"kind: ConfigMap")
-
-	unstructuredObj := unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": "v1",
-			"kind":       "ConfigMap",
-		},
-	}
-
-	convertedyaml, err := FromUnstructured([]unstructured.Unstructured{unstructuredObj})
-	g := NewWithT(t)
-	g.Expect(err).ToNot(HaveOccurred())
-	g.Expect(string(rawyaml)).To(Equal(string(convertedyaml)))
-}