Repository: crossplaneio/crossplane
Pull request #1146 (2020-01-02 05:03:22)

Implement KubernetesTarget
Signed-off-by: hasheddan <georgedanielmangum@gmail.com>

<!--
Thank you for helping to improve Crossplane!

We strongly recommend you look through our contributor guide at https://git.io/fj2m9
if this is your first time opening a Crossplane pull request. You can find us in
https://slack.crossplane.io/messages/dev if you need any help contributing.
-->

### Description of your changes
<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open Crossplane issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":

Fixes #
-->

This PR implements the new `KubernetesTarget` resource that was proposed in #1101. It adds the resource type and updates `KubernetesApplication` and `KubernetesApplicationResource` to utilize it for scheduling to remote clusters. A checklist of remaining items to be completed is included below:

 In this PR:
- [x] Implement [automatic `KubernetesTarget` creation controller](https://github.com/crossplaneio/crossplane/blob/master/design/one-pager-consuming-k8s-clusters.md#automatic-kubernetestarget-creation-controller)
- [ ] Demonstrate new functionality with a fork of an existing stack

In each stack that provides a Kubernetes cluster managed resource, it will also be necessary to implement a new controller as detailed [here](https://github.com/crossplaneio/crossplane/blob/master/design/one-pager-consuming-k8s-clusters.md#kubernetestarget). It is likely that this will be handled by implementing a new reconciler in `crossplane-runtime` with shared logic that can be used across the stacks.

### How has this code been tested?
<!--
Before reviewers can be confident in the correctness of a pull request,
it needs to tested and shown to be correct. In this section, briefly
describe the testing that has already been done or which is planned.
-->

Currently only through unit tests. As detailed above, a working scenario that will be able to be used by reviewers should be implemented prior to merge of this PR.

### Checklist
<!--
Please run through the below readiness checklist. The first two items are
relevant to every Crossplane pull request.
-->
I have:
- [x] Run `make reviewable` to ensure this PR is ready for review.
- [x] Ensured this PR contains a neat, self documenting set of commits.
- [ ] Updated any relevant [documentation] and [examples].
- [x] Reported all new error conditions into the log or as an event, as
  appropriate.

For more about what we believe makes a pull request complete, see our
[definition of done].

[documentation]: https://github.com/crossplaneio/crossplane/tree/master/docs
[examples]: https://github.com/crossplaneio/crossplane/tree/master/cluster/examples
[definition of done]: https://github.com/crossplaneio/crossplane/tree/master/design/one-pager-definition-of-done.md


upbound-bot: [![80% (+0.02%) vs master 80%](https://jenkinsci.upbound.io//coverage-status-icon/?coverage=0.7995&masterCoverage=0.7993)](https://jenkinsci.upbound.io/job/crossplaneio/job/crossplane/job/build/job/PR-1146/1/)
upbound-bot: [![80% (-0.11%) vs master 80%](https://jenkinsci.upbound.io//coverage-status-icon/?coverage=0.7982&masterCoverage=0.7993)](https://jenkinsci.upbound.io/job/crossplaneio/job/crossplane/job/build/job/PR-1146/2/)
upbound-bot: [![80% (-0.13%) vs master 80%](https://jenkinsci.upbound.io//coverage-status-icon/?coverage=0.798&masterCoverage=0.7993)](https://jenkinsci.upbound.io/job/crossplaneio/job/crossplane/job/build/job/PR-1146/3/)

Patch:
diff --git a/apis/workload/v1alpha1/register.go b/apis/workload/v1alpha1/register.go
index 04e03889c..42f8858ab 100644
--- a/apis/workload/v1alpha1/register.go
+++ b/apis/workload/v1alpha1/register.go
@@ -51,7 +51,15 @@ var (
 	KubernetesApplicationResourceGroupVersionKind = SchemeGroupVersion.WithKind(KubernetesApplicationResourceKind)
 )
 
+// KubernetesTarget type metadata.
+var (
+	KubernetesTargetKind             = reflect.TypeOf(KubernetesTarget{}).Name()
+	KubernetesTargetKindAPIVersion   = KubernetesTargetKind + "." + SchemeGroupVersion.String()
+	KubernetesTargetGroupVersionKind = SchemeGroupVersion.WithKind(KubernetesTargetKind)
+)
+
 func init() {
 	SchemeBuilder.Register(&KubernetesApplication{}, &KubernetesApplicationList{})
 	SchemeBuilder.Register(&KubernetesApplicationResource{}, &KubernetesApplicationResourceList{})
+	SchemeBuilder.Register(&KubernetesTarget{}, &KubernetesTargetList{})
 }
diff --git a/apis/workload/v1alpha1/target_methods.go b/apis/workload/v1alpha1/target_methods.go
new file mode 100644
index 000000000..1e24134e0
--- /dev/null
+++ b/apis/workload/v1alpha1/target_methods.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2019 The Crossplane 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.
+*/
+
+// TODO(hasheddan): generate these methods with angryjet
+// Ref: https://github.com/crossplaneio/crossplane-tools/issues/14
+
+package v1alpha1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+
+	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
+)
+
+// GetCondition of this KubernetesTarget.
+func (tr *KubernetesTarget) GetCondition(ct runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
+	return tr.Status.GetCondition(ct)
+}
+
+// GetResourceReference of this KubernetesTarget.
+func (tr *KubernetesTarget) GetResourceReference() *corev1.ObjectReference {
+	return tr.Spec.ResourceReference
+}
+
+// GetWriteConnectionSecretToReference of this KubernetesTarget.
+func (tr *KubernetesTarget) GetWriteConnectionSecretToReference() *runtimev1alpha1.LocalSecretReference {
+	return tr.Spec.WriteConnectionSecretToReference
+}
+
+// SetConditions of this KubernetesTarget.
+func (tr *KubernetesTarget) SetConditions(c ...runtimev1alpha1.Condition) {
+	tr.Status.SetConditions(c...)
+}
+
+// SetResourceReference of this KubernetesTarget.
+func (tr *KubernetesTarget) SetResourceReference(r *corev1.ObjectReference) {
+	tr.Spec.ResourceReference = r
+}
+
+// SetWriteConnectionSecretToReference of this KubernetesTarget.
+func (tr *KubernetesTarget) SetWriteConnectionSecretToReference(r *runtimev1alpha1.LocalSecretReference) {
+	tr.Spec.WriteConnectionSecretToReference = r
+}
diff --git a/apis/workload/v1alpha1/types.go b/apis/workload/v1alpha1/types.go
index fc1f1be0f..ee1982603 100644
--- a/apis/workload/v1alpha1/types.go
+++ b/apis/workload/v1alpha1/types.go
@@ -58,10 +58,10 @@ type KubernetesApplicationSpec struct {
 	// TODO(muvaf): Only MatchLabels field of LabelSelector is used. Incorporate
 	// MatchExpressions as well when it's available as controller-runtime ListOption
 
-	// ClusterSelector selects the clusters to which this application may be
+	// TargetSelector selects the targets to which this application may be
 	// scheduled. Leave both match labels and expressions empty to match any
-	// cluster.
-	ClusterSelector *metav1.LabelSelector `json:"clusterSelector"`
+	// target.
+	TargetSelector *metav1.LabelSelector `json:"targetSelector"`
 
 	// TODO(negz): Use a validation webhook to ensure the below templates have
 	// unique names.
@@ -79,9 +79,9 @@ type KubernetesApplicationResourceTemplate struct {
 	Spec KubernetesApplicationResourceSpec `json:"spec,omitempty"`
 }
 
-// A KubernetesClusterReference is a reference to a KubernetesCluster resource
+// A KubernetesTargetReference is a reference to a KubernetesTarget resource
 // claim in the same namespace as the referrer.
-type KubernetesClusterReference struct {
+type KubernetesTargetReference struct {
 	// Name of the referent. More info:
 	// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
 	Name string `json:"name"`
@@ -95,8 +95,8 @@ type KubernetesApplicationStatus struct {
 	// State of the application.
 	State KubernetesApplicationState `json:"state,omitempty"`
 
-	// Cluster to which this application has been scheduled.
-	Cluster *KubernetesClusterReference `json:"clusterRef,omitempty"`
+	// Target to which this application has been scheduled.
+	Target *KubernetesTargetReference `json:"targetRef,omitempty"`
 
 	// Desired resources of this application, i.e. the number of resources
 	// that match this application's resource selector.
@@ -112,7 +112,7 @@ type KubernetesApplicationStatus struct {
 
 // A KubernetesApplication defines an application deployed by Crossplane to a
 // Kubernetes cluster, i.e. a portable KubernetesCluster resource claim.
-// +kubebuilder:printcolumn:name="CLUSTER",type="string",JSONPath=".status.clusterRef.name"
+// +kubebuilder:printcolumn:name="CLUSTER",type="string",JSONPath=".status.targetRef.name"
 // +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.state"
 // +kubebuilder:printcolumn:name="DESIRED",type="integer",JSONPath=".status.desiredResources"
 // +kubebuilder:printcolumn:name="SUBMITTED",type="integer",JSONPath=".status.submittedResources"
@@ -198,8 +198,8 @@ type KubernetesApplicationResourceStatus struct {
 	// State of the application.
 	State KubernetesApplicationResourceState `json:"state,omitempty"`
 
-	// Cluster to which this application has been scheduled.
-	Cluster *KubernetesClusterReference `json:"clusterRef,omitempty"`
+	// Target to which this application has been scheduled.
+	Target *KubernetesTargetReference `json:"targetRef,omitempty"`
 
 	// Remote status of the resource templated by this application resource.
 	Remote *RemoteStatus `json:"remote,omitempty"`
@@ -212,7 +212,7 @@ type KubernetesApplicationResourceStatus struct {
 // scheduled KubernetesCluster.
 // +kubebuilder:printcolumn:name="TEMPLATE-KIND",type="string",JSONPath=".spec.template.kind"
 // +kubebuilder:printcolumn:name="TEMPLATE-NAME",type="string",JSONPath=".spec.template.metadata.name"
-// +kubebuilder:printcolumn:name="CLUSTER",type="string",JSONPath=".status.clusterRef.name"
+// +kubebuilder:printcolumn:name="CLUSTER",type="string",JSONPath=".status.targetRef.name"
 // +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.state"
 type KubernetesApplicationResource struct {
 	metav1.TypeMeta   `json:",inline"`
@@ -231,3 +231,24 @@ type KubernetesApplicationResourceList struct {
 	metav1.ListMeta `json:"metadata,omitempty"`
 	Items           []KubernetesApplicationResource `json:"items"`
 }
+
+// +kubebuilder:object:root=true
+
+// A KubernetesTarget is a scheduling target for a Kubernetes Application.
+// +kubebuilder:printcolumn:name="CLUSTER",type="string",JSONPath=".spec.clusterRef.name"
+type KubernetesTarget struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   runtimev1alpha1.TargetSpec   `json:"spec"`
+	Status runtimev1alpha1.TargetStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// KubernetesTargetList contains a list of KubernetesTarget.
+type KubernetesTargetList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []KubernetesTarget `json:"items"`
+}
diff --git a/apis/workload/v1alpha1/zz_generated.deepcopy.go b/apis/workload/v1alpha1/zz_generated.deepcopy.go
index 96fb523f7..bf574e279 100644
--- a/apis/workload/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/workload/v1alpha1/zz_generated.deepcopy.go
@@ -174,9 +174,9 @@ func (in *KubernetesApplicationResourceSpec) DeepCopy() *KubernetesApplicationRe
 func (in *KubernetesApplicationResourceStatus) DeepCopyInto(out *KubernetesApplicationResourceStatus) {
 	*out = *in
 	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
-	if in.Cluster != nil {
-		in, out := &in.Cluster, &out.Cluster
-		*out = new(KubernetesClusterReference)
+	if in.Target != nil {
+		in, out := &in.Target, &out.Target
+		*out = new(KubernetesTargetReference)
 		**out = **in
 	}
 	if in.Remote != nil {
@@ -221,8 +221,8 @@ func (in *KubernetesApplicationSpec) DeepCopyInto(out *KubernetesApplicationSpec
 		*out = new(v1.LabelSelector)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.ClusterSelector != nil {
-		in, out := &in.ClusterSelector, &out.ClusterSelector
+	if in.TargetSelector != nil {
+		in, out := &in.TargetSelector, &out.TargetSelector
 		*out = new(v1.LabelSelector)
 		(*in).DeepCopyInto(*out)
 	}
@@ -249,9 +249,9 @@ func (in *KubernetesApplicationSpec) DeepCopy() *KubernetesApplicationSpec {
 func (in *KubernetesApplicationStatus) DeepCopyInto(out *KubernetesApplicationStatus) {
 	*out = *in
 	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
-	if in.Cluster != nil {
-		in, out := &in.Cluster, &out.Cluster
-		*out = new(KubernetesClusterReference)
+	if in.Target != nil {
+		in, out := &in.Target, &out.Target
+		*out = new(KubernetesTargetReference)
 		**out = **in
 	}
 }
@@ -267,16 +267,75 @@ func (in *KubernetesApplicationStatus) DeepCopy() *KubernetesApplicationStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *KubernetesClusterReference) DeepCopyInto(out *KubernetesClusterReference) {
+func (in *KubernetesTarget) DeepCopyInto(out *KubernetesTarget) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesTarget.
+func (in *KubernetesTarget) DeepCopy() *KubernetesTarget {
+	if in == nil {
+		return nil
+	}
+	out := new(KubernetesTarget)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KubernetesTarget) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KubernetesTargetList) DeepCopyInto(out *KubernetesTargetList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]KubernetesTarget, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesTargetList.
+func (in *KubernetesTargetList) DeepCopy() *KubernetesTargetList {
+	if in == nil {
+		return nil
+	}
+	out := new(KubernetesTargetList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *KubernetesTargetList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KubernetesTargetReference) DeepCopyInto(out *KubernetesTargetReference) {
 	*out = *in
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesClusterReference.
-func (in *KubernetesClusterReference) DeepCopy() *KubernetesClusterReference {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesTargetReference.
+func (in *KubernetesTargetReference) DeepCopy() *KubernetesTargetReference {
 	if in == nil {
 		return nil
 	}
-	out := new(KubernetesClusterReference)
+	out := new(KubernetesTargetReference)
 	in.DeepCopyInto(out)
 	return out
 }
diff --git a/build b/build
index cf6400db1..72f6ac20a 160000
--- a/build
+++ b/build
@@ -1,1 +1,1 @@
-Subproject commit cf6400db14c443f45e7343de49081890276aaadf
+Subproject commit 72f6ac20af11e05ecdf05c7ab1aec9996103bb5a
diff --git a/cluster/charts/crossplane/templates/crds/cache.crossplane.io_redisclusters.yaml b/cluster/charts/crossplane/templates/crds/cache.crossplane.io_redisclusters.yaml
index 5665e1190..5aacfe464 100644
--- a/cluster/charts/crossplane/templates/crds/cache.crossplane.io_redisclusters.yaml
+++ b/cluster/charts/crossplane/templates/crds/cache.crossplane.io_redisclusters.yaml
@@ -223,7 +223,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/compute.crossplane.io_kubernetesclusters.yaml b/cluster/charts/crossplane/templates/crds/compute.crossplane.io_kubernetesclusters.yaml
index 5d35e7ab3..2ef1a1d4f 100644
--- a/cluster/charts/crossplane/templates/crds/compute.crossplane.io_kubernetesclusters.yaml
+++ b/cluster/charts/crossplane/templates/crds/compute.crossplane.io_kubernetesclusters.yaml
@@ -217,7 +217,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/compute.crossplane.io_machineinstances.yaml b/cluster/charts/crossplane/templates/crds/compute.crossplane.io_machineinstances.yaml
index a19337aad..bbd9c9003 100644
--- a/cluster/charts/crossplane/templates/crds/compute.crossplane.io_machineinstances.yaml
+++ b/cluster/charts/crossplane/templates/crds/compute.crossplane.io_machineinstances.yaml
@@ -214,7 +214,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/database.crossplane.io_mysqlinstances.yaml b/cluster/charts/crossplane/templates/crds/database.crossplane.io_mysqlinstances.yaml
index 5cdbe2b81..50836b4df 100644
--- a/cluster/charts/crossplane/templates/crds/database.crossplane.io_mysqlinstances.yaml
+++ b/cluster/charts/crossplane/templates/crds/database.crossplane.io_mysqlinstances.yaml
@@ -220,7 +220,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/database.crossplane.io_postgresqlinstances.yaml b/cluster/charts/crossplane/templates/crds/database.crossplane.io_postgresqlinstances.yaml
index 8cf401068..7b285c56f 100644
--- a/cluster/charts/crossplane/templates/crds/database.crossplane.io_postgresqlinstances.yaml
+++ b/cluster/charts/crossplane/templates/crds/database.crossplane.io_postgresqlinstances.yaml
@@ -220,7 +220,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/storage.crossplane.io_buckets.yaml b/cluster/charts/crossplane/templates/crds/storage.crossplane.io_buckets.yaml
index 2f2d205f4..056fa8376 100644
--- a/cluster/charts/crossplane/templates/crds/storage.crossplane.io_buckets.yaml
+++ b/cluster/charts/crossplane/templates/crds/storage.crossplane.io_buckets.yaml
@@ -235,7 +235,7 @@ spec:
             conditions:
               description: Conditions of the resource.
               items:
-                description: A Condition that may apply to a managed resource.
+                description: A Condition that may apply to a resource.
                 properties:
                   lastTransitionTime:
                     description: LastTransitionTime is the last time this condition
diff --git a/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplicationresources.yaml b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplicationresources.yaml
index 78de93267..f022fa972 100644
--- a/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplicationresources.yaml
+++ b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplicationresources.yaml
@@ -15,7 +15,7 @@ spec:
   - JSONPath: .spec.template.metadata.name
     name: TEMPLATE-NAME
     type: string
-  - JSONPath: .status.clusterRef.name
+  - JSONPath: .status.targetRef.name
     name: CLUSTER
     type: string
   - JSONPath: .status.state
@@ -79,23 +79,14 @@ spec:
           description: KubernetesApplicationResourceStatus represents the observed
             state of a KubernetesApplicationResource.
           properties:
-            clusterRef:
-              description: Cluster to which this application has been scheduled.
-              properties:
-                name:
-                  description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
-                  type: string
-              required:
-              - name
-              type: object
             conditionedStatus:
-              description: A ConditionedStatus reflects the observed status of a managed
-                resource. Only one condition of each type may exist.
+              description: A ConditionedStatus reflects the observed status of a resource.
+                Only one condition of each type may exist.
               properties:
                 conditions:
                   description: Conditions of the resource.
                   items:
-                    description: A Condition that may apply to a managed resource.
+                    description: A Condition that may apply to a resource.
                     properties:
                       lastTransitionTime:
                         description: LastTransitionTime is the last time this condition
@@ -139,6 +130,15 @@ spec:
             state:
               description: State of the application.
               type: string
+            targetRef:
+              description: Target to which this application has been scheduled.
+              properties:
+                name:
+                  description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
+                  type: string
+              required:
+              - name
+              type: object
           type: object
       type: object
   version: v1alpha1
diff --git a/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplications.yaml b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplications.yaml
index fa8e62376..1e57a7009 100644
--- a/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplications.yaml
+++ b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetesapplications.yaml
@@ -9,7 +9,7 @@ metadata:
   name: kubernetesapplications.workload.crossplane.io
 spec:
   additionalPrinterColumns:
-  - JSONPath: .status.clusterRef.name
+  - JSONPath: .status.targetRef.name
     name: CLUSTER
     type: string
   - JSONPath: .status.state
@@ -50,51 +50,6 @@ spec:
           description: A KubernetesApplicationSpec specifies the resources of a Kubernetes
             application.
           properties:
-            clusterSelector:
-              description: ClusterSelector selects the clusters to which this application
-                may be scheduled. Leave both match labels and expressions empty to
-                match any cluster.
-              properties:
-                matchExpressions:
-                  description: matchExpressions is a list of label selector requirements.
-                    The requirements are ANDed.
-                  items:
-                    description: A label selector requirement is a selector that contains
-                      values, a key, and an operator that relates the key and values.
-                    properties:
-                      key:
-                        description: key is the label key that the selector applies
-                          to.
-                        type: string
-                      operator:
-                        description: operator represents a key's relationship to a
-                          set of values. Valid operators are In, NotIn, Exists and
-                          DoesNotExist.
-                        type: string
-                      values:
-                        description: values is an array of string values. If the operator
-                          is In or NotIn, the values array must be non-empty. If the
-                          operator is Exists or DoesNotExist, the values array must
-                          be empty. This array is replaced during a strategic merge
-                          patch.
-                        items:
-                          type: string
-                        type: array
-                    required:
-                    - key
-                    - operator
-                    type: object
-                  type: array
-                matchLabels:
-                  additionalProperties:
-                    type: string
-                  description: matchLabels is a map of {key,value} pairs. A single
-                    {key,value} in the matchLabels map is equivalent to an element
-                    of matchExpressions, whose key field is "key", the operator is
-                    "In", and the values array contains only "value". The requirements
-                    are ANDed.
-                  type: object
-              type: object
             resourceSelector:
               description: ResourceSelector selects the KubernetesApplicationResources
                 that are managed by this KubernetesApplication. Note that a KubernetesApplication
@@ -182,32 +137,68 @@ spec:
                     type: object
                 type: object
               type: array
+            targetSelector:
+              description: TargetSelector selects the targets to which this application
+                may be scheduled. Leave both match labels and expressions empty to
+                match any target.
+              properties:
+                matchExpressions:
+                  description: matchExpressions is a list of label selector requirements.
+                    The requirements are ANDed.
+                  items:
+                    description: A label selector requirement is a selector that contains
+                      values, a key, and an operator that relates the key and values.
+                    properties:
+                      key:
+                        description: key is the label key that the selector applies
+                          to.
+                        type: string
+                      operator:
+                        description: operator represents a key's relationship to a
+                          set of values. Valid operators are In, NotIn, Exists and
+                          DoesNotExist.
+                        type: string
+                      values:
+                        description: values is an array of string values. If the operator
+                          is In or NotIn, the values array must be non-empty. If the
+                          operator is Exists or DoesNotExist, the values array must
+                          be empty. This array is replaced during a strategic merge
+                          patch.
+                        items:
+                          type: string
+                        type: array
+                    required:
+                    - key
+                    - operator
+                    type: object
+                  type: array
+                matchLabels:
+                  additionalProperties:
+                    type: string
+                  description: matchLabels is a map of {key,value} pairs. A single
+                    {key,value} in the matchLabels map is equivalent to an element
+                    of matchExpressions, whose key field is "key", the operator is
+                    "In", and the values array contains only "value". The requirements
+                    are ANDed.
+                  type: object
+              type: object
           required:
-          - clusterSelector
           - resourceSelector
           - resourceTemplates
+          - targetSelector
           type: object
         status:
           description: KubernetesApplicationStatus represents the observed state of
             a KubernetesApplication.
           properties:
-            clusterRef:
-              description: Cluster to which this application has been scheduled.
-              properties:
-                name:
-                  description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
-                  type: string
-              required:
-              - name
-              type: object
             conditionedStatus:
-              description: A ConditionedStatus reflects the observed status of a managed
-                resource. Only one condition of each type may exist.
+              description: A ConditionedStatus reflects the observed status of a resource.
+                Only one condition of each type may exist.
               properties:
                 conditions:
                   description: Conditions of the resource.
                   items:
-                    description: A Condition that may apply to a managed resource.
+                    description: A Condition that may apply to a resource.
                     properties:
                       lastTransitionTime:
                         description: LastTransitionTime is the last time this condition
@@ -250,6 +241,15 @@ spec:
                 desired resources that have been successfully submitted to their scheduled
                 Kubernetes cluster.
               type: integer
+            targetRef:
+              description: Target to which this application has been scheduled.
+              properties:
+                name:
+                  description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
+                  type: string
+              required:
+              - name
+              type: object
           type: object
       type: object
   version: v1alpha1
diff --git a/go.mod b/go.mod
index 31188107c..8998b3933 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crossplaneio/crossplane
 go 1.13
 
 require (
-	github.com/crossplaneio/crossplane-runtime v0.3.0
+	github.com/crossplaneio/crossplane-runtime v0.3.1-0.20200114203641-5ece4af54b32
 	github.com/crossplaneio/crossplane-tools v0.0.0-20191023215726-61fa1eff2a2e
 	github.com/ghodss/yaml v1.0.0
 	github.com/google/go-cmp v0.3.1
diff --git a/go.sum b/go.sum
index 7000171f6..3a3a13b6b 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
-github.com/crossplaneio/crossplane-runtime v0.3.0 h1:d3937o6XhVUcm/bqgUPUjTo/OjVlAjh1c7rx4VPJoV8=
-github.com/crossplaneio/crossplane-runtime v0.3.0/go.mod h1:aVLQmXiPeb/jhGGQFIxx4Rc1CFC6xq2BA6A3neLZIHA=
+github.com/crossplaneio/crossplane-runtime v0.3.1-0.20200114203641-5ece4af54b32 h1:DbviwPKTeGZmmJWcv5CWimCot2sNP8qRzeIOse/KxBM=
+github.com/crossplaneio/crossplane-runtime v0.3.1-0.20200114203641-5ece4af54b32/go.mod h1:aVLQmXiPeb/jhGGQFIxx4Rc1CFC6xq2BA6A3neLZIHA=
 github.com/crossplaneio/crossplane-tools v0.0.0-20191023215726-61fa1eff2a2e h1:8Z29akSDYcbywc/brATpfeE425jBTMZdZQuD9eV6cCk=
 github.com/crossplaneio/crossplane-tools v0.0.0-20191023215726-61fa1eff2a2e/go.mod h1:fzQeWDvZvzaC4N8vPjTubQocGnwzQ4cuZM6949+T43U=
 github.com/dave/jennifer v1.3.0 h1:p3tl41zjjCZTNBytMwrUuiAnherNUZktlhPTKoF/sEk=
diff --git a/pkg/controller/workload/kubernetes/application/application.go b/pkg/controller/workload/kubernetes/application/application.go
index 5fc576029..c3edb4045 100644
--- a/pkg/controller/workload/kubernetes/application/application.go
+++ b/pkg/controller/workload/kubernetes/application/application.go
@@ -53,23 +53,23 @@ type syncer interface {
 }
 
 // CreatePredicate accepts KubernetesApplications that have been scheduled to a
-// KubernetesCluster.
+// KubernetesTarget.
 func CreatePredicate(event event.CreateEvent) bool {
 	wl, ok := event.Object.(*v1alpha1.KubernetesApplication)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster != nil
+	return wl.Status.Target != nil
 }
 
 // UpdatePredicate accepts KubernetesApplications that have been scheduled to a
-// KubernetesCluster.
+// KubernetesTarget.
 func UpdatePredicate(event event.UpdateEvent) bool {
 	wl, ok := event.ObjectNew.(*v1alpha1.KubernetesApplication)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster != nil
+	return wl.Status.Target != nil
 }
 
 // Add the KubernetesApplication scheduler reconciler to the supplied manager.
@@ -197,7 +197,7 @@ func renderTemplate(app *v1alpha1.KubernetesApplication, template *v1alpha1.Kube
 	ar.SetAnnotations(template.GetAnnotations())
 
 	ar.Spec = template.Spec
-	ar.Status.Cluster = app.Status.Cluster
+	ar.Status.Target = app.Status.Target
 	ar.Status.State = v1alpha1.KubernetesApplicationResourceStateScheduled
 
 	return ar
diff --git a/pkg/controller/workload/kubernetes/resource/resource.go b/pkg/controller/workload/kubernetes/resource/resource.go
index 2489d3f9a..7cf2b5a70 100644
--- a/pkg/controller/workload/kubernetes/resource/resource.go
+++ b/pkg/controller/workload/kubernetes/resource/resource.go
@@ -42,7 +42,6 @@ import (
 	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/crossplaneio/crossplane-runtime/pkg/logging"
 	"github.com/crossplaneio/crossplane-runtime/pkg/meta"
-	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
 	"github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
 )
 
@@ -69,23 +68,23 @@ var (
 )
 
 // CreatePredicate accepts KubernetesApplicationResources that have been
-// scheduled to a KubernetesCluster.
+// scheduled to a KubernetesTarget.
 func CreatePredicate(event event.CreateEvent) bool {
 	wl, ok := event.Object.(*v1alpha1.KubernetesApplicationResource)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster != nil
+	return wl.Status.Target != nil
 }
 
 // UpdatePredicate accepts KubernetesApplicationResources that have been
-// scheduled to a KubernetesCluster.
+// scheduled to a KubernetesTarget.
 func UpdatePredicate(event event.UpdateEvent) bool {
 	wl, ok := event.ObjectNew.(*v1alpha1.KubernetesApplicationResource)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster != nil
+	return wl.Status.Target != nil
 }
 
 // Controller is responsible for adding the Instance
@@ -107,14 +106,14 @@ func (c *Controller) SetupWithManager(mgr ctrl.Manager) error {
 		Complete(r)
 }
 
-// A syncer can sync resources with a KubernetesCluster.
+// A syncer can sync resources with a KubernetesTarget.
 type syncer interface {
 	// sync the supplied resource with the external store. Returns true if the
 	// resource requires further reconciliation.
 	sync(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource, secrets []corev1.Secret) reconcile.Result
 }
 
-// A deleter can delete resources from a KubernetesCluster.
+// A deleter can delete resources from a KubernetesTarget.
 type deleter interface {
 	// delete the supplied resource from the external store. Returns true if the
 	// resource requires further reconciliation.
@@ -122,7 +121,7 @@ type deleter interface {
 }
 
 // A syncdeleter can sync and delete KubernetesApplicationResources in a
-// KubernetesCluster.
+// KubernetesTarget.
 type syncdeleter interface {
 	syncer
 	deleter
@@ -401,21 +400,21 @@ type clusterConnecter struct {
 
 func (c *clusterConnecter) config(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource) (*rest.Config, error) {
 	n := types.NamespacedName{Namespace: ar.GetNamespace(), Name: ar.GetName()}
-	if ar.Status.Cluster == nil {
+	if ar.Status.Target == nil {
 		return nil, errors.Errorf("%s %s is not scheduled", v1alpha1.KubernetesApplicationResourceKind, n)
 	}
 
-	n = types.NamespacedName{Namespace: ar.GetNamespace(), Name: ar.Status.Cluster.Name}
-	k := &computev1alpha1.KubernetesCluster{}
+	n = types.NamespacedName{Namespace: ar.GetNamespace(), Name: ar.Status.Target.Name}
+	k := &v1alpha1.KubernetesTarget{}
 	if err := c.kube.Get(ctx, n, k); err != nil {
-		return nil, errors.Wrapf(err, "cannot get %s %s", computev1alpha1.KubernetesClusterKind, n)
+		return nil, errors.Wrapf(err, "cannot get %s %s", v1alpha1.KubernetesTargetKind, n)
 	}
 
-	if k.Spec.WriteConnectionSecretToReference == nil {
-		return nil, errors.Errorf("%s %s has no connection secret", computev1alpha1.KubernetesClusterKind, n)
+	if k.GetWriteConnectionSecretToReference() == nil {
+		return nil, errors.Errorf("%s %s has no connection secret", v1alpha1.KubernetesTargetKind, n)
 	}
 
-	n = types.NamespacedName{Namespace: k.GetNamespace(), Name: k.Spec.WriteConnectionSecretToReference.Name}
+	n = types.NamespacedName{Namespace: k.GetNamespace(), Name: k.GetWriteConnectionSecretToReference().Name}
 	s := &corev1.Secret{}
 	if err := c.kube.Get(ctx, n, s); err != nil {
 		return nil, errors.Wrapf(err, "cannot get secret %s", n)
@@ -449,7 +448,7 @@ func (c *clusterConnecter) config(ctx context.Context, ar *v1alpha1.KubernetesAp
 	return config, nil
 }
 
-// connect returns a syncdeleter backed by a KubernetesCluster.
+// connect returns a syncdeleter backed by a KubernetesTarget.
 // Cluster credentials are read from a Crossplane connection secret.
 func (c *clusterConnecter) connect(ctx context.Context, ar *v1alpha1.KubernetesApplicationResource) (syncdeleter, error) {
 	config, err := c.config(ctx, ar)
diff --git a/pkg/controller/workload/kubernetes/scheduler/scheduler.go b/pkg/controller/workload/kubernetes/scheduler/scheduler.go
index c4f542fe9..348688665 100644
--- a/pkg/controller/workload/kubernetes/scheduler/scheduler.go
+++ b/pkg/controller/workload/kubernetes/scheduler/scheduler.go
@@ -30,7 +30,6 @@ import (
 
 	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/crossplaneio/crossplane-runtime/pkg/logging"
-	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
 	workloadv1alpha1 "github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
 )
 
@@ -47,24 +46,24 @@ type scheduler interface {
 }
 
 type roundRobinScheduler struct {
-	kube             client.Client
-	lastClusterIndex uint64
+	kube            client.Client
+	lastTargetIndex uint64
 }
 
 func (s *roundRobinScheduler) schedule(ctx context.Context, app *workloadv1alpha1.KubernetesApplication) reconcile.Result {
 	app.Status.State = workloadv1alpha1.KubernetesApplicationStatePending
 
-	clusters := &computev1alpha1.KubernetesClusterList{}
-	if err := s.kube.List(ctx, clusters, client.InNamespace(app.GetNamespace()), client.MatchingLabels(app.Spec.ClusterSelector.MatchLabels)); err != nil {
+	clusters := &workloadv1alpha1.KubernetesTargetList{}
+	if err := s.kube.List(ctx, clusters, client.InNamespace(app.GetNamespace()), client.MatchingLabels(app.Spec.TargetSelector.MatchLabels)); err != nil {
 		app.Status.SetConditions(runtimev1alpha1.ReconcileError(err))
 		return reconcile.Result{Requeue: true}
 	}
 
-	// Filter out KubernetesCluster claims that don't specify a connection
+	// Filter out KubernetesTargets that don't specify a connection
 	// secret. We can't run a workload on a cluster that we can't connect to.
-	usable := make([]computev1alpha1.KubernetesCluster, 0)
+	usable := make([]workloadv1alpha1.KubernetesTarget, 0)
 	for _, c := range clusters.Items {
-		if c.Spec.WriteConnectionSecretToReference != nil {
+		if c.GetWriteConnectionSecretToReference() != nil {
 			usable = append(usable, c)
 		}
 	}
@@ -74,12 +73,12 @@ func (s *roundRobinScheduler) schedule(ctx context.Context, app *workloadv1alpha
 		return reconcile.Result{Requeue: true}
 	}
 
-	// Round-robin cluster selection
-	index := int(s.lastClusterIndex % uint64(len(usable)))
-	cluster := usable[index]
-	s.lastClusterIndex++
+	// Round-robin target selection
+	index := int(s.lastTargetIndex % uint64(len(usable)))
+	target := usable[index]
+	s.lastTargetIndex++
 
-	app.Status.Cluster = &workloadv1alpha1.KubernetesClusterReference{Name: cluster.Name}
+	app.Status.Target = &workloadv1alpha1.KubernetesTargetReference{Name: target.Name}
 	app.Status.State = workloadv1alpha1.KubernetesApplicationStateScheduled
 	app.Status.SetConditions(runtimev1alpha1.ReconcileSuccess())
 
@@ -87,23 +86,23 @@ func (s *roundRobinScheduler) schedule(ctx context.Context, app *workloadv1alpha
 }
 
 // CreatePredicate accepts KubernetesApplications that have not yet been
-// scheduled to a KubernetesCluster.
+// scheduled to a KubernetesTarget.
 func CreatePredicate(e event.CreateEvent) bool {
 	wl, ok := e.Object.(*workloadv1alpha1.KubernetesApplication)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster == nil
+	return wl.Status.Target == nil
 }
 
 // UpdatePredicate accepts KubernetesApplications that have not yet been
-// scheduled to a KubernetesCluster.
+// scheduled to a KubernetesTarget.
 func UpdatePredicate(e event.UpdateEvent) bool {
 	wl, ok := e.ObjectNew.(*workloadv1alpha1.KubernetesApplication)
 	if !ok {
 		return false
 	}
-	return wl.Status.Cluster == nil
+	return wl.Status.Target == nil
 }
 
 // Controller is responsible for adding the Scheduler
@@ -125,13 +124,13 @@ func (c *Controller) SetupWithManager(mgr ctrl.Manager) error {
 		Complete(r)
 }
 
-// A Reconciler schedules KubernetesApplications to KubernetesClusters.
+// A Reconciler schedules KubernetesApplications to KubernetesTargets.
 type Reconciler struct {
 	kube      client.Client
 	scheduler scheduler
 }
 
-// Reconcile attempts to schedule a KubernetesApplication to a KubernetesCluster
+// Reconcile attempts to schedule a KubernetesApplication to a KubernetesTarget
 // that matches its cluster selector.
 func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
 	log.V(logging.Debug).Info("reconciling", "kind", workloadv1alpha1.KubernetesApplicationKindAPIVersion, "request", req)
@@ -153,7 +152,7 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error)
 	}
 
 	// Someone already scheduled this application.
-	if app.Status.Cluster != nil {
+	if app.Status.Target != nil {
 		return reconcile.Result{RequeueAfter: requeueOnSuccess}, nil
 	}
 
diff --git a/pkg/controller/workload/kubernetes/target/target.go b/pkg/controller/workload/kubernetes/target/target.go
new file mode 100644
index 000000000..2cd77655f
--- /dev/null
+++ b/pkg/controller/workload/kubernetes/target/target.go
@@ -0,0 +1,126 @@
+/*
+Copyright 2019 The Crossplane 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 target
+
+import (
+	"context"
+	"time"
+
+	"github.com/pkg/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
+	"github.com/crossplaneio/crossplane-runtime/pkg/logging"
+	"github.com/crossplaneio/crossplane-runtime/pkg/meta"
+	"github.com/crossplaneio/crossplane-runtime/pkg/resource"
+	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
+	workloadv1alpha1 "github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
+)
+
+const (
+	controllerName   = "autotarget.workload.crossplane.io"
+	reconcileTimeout = 1 * time.Minute
+
+	errGetKubernetesCluster = "unable to get KubernetesCluster"
+	errCreateOrUpdateTarget = "unable to create or update KubernetesTarget"
+	errTargetConflict       = "cannot establish control of existing KubernetesTarget"
+)
+
+var log = logging.Logger.WithName("controller." + controllerName)
+
+func clusterIsBound(obj runtime.Object) bool {
+	r, ok := obj.(*computev1alpha1.KubernetesCluster)
+	if !ok {
+		return false
+	}
+
+	return r.GetBindingPhase() == runtimev1alpha1.BindingPhaseBound
+}
+
+// Controller is responsible for adding the KubernetesTarget auto-creation
+// controller and its corresponding reconciler to the manager with any runtime configuration.
+type Controller struct{}
+
+// SetupWithManager creates a new Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller
+// and Start it when the Manager is Started.
+func (c *Controller) SetupWithManager(mgr ctrl.Manager) error {
+	r := &Reconciler{
+		kube: mgr.GetClient(),
+	}
+
+	p := resource.NewPredicates(clusterIsBound)
+
+	return ctrl.NewControllerManagedBy(mgr).
+		Named(controllerName).
+		For(&computev1alpha1.KubernetesCluster{}).
+		WithEventFilter(p).
+		Complete(r)
+}
+
+// A Reconciler creates KubernetesTargets for KubernetesClusters.
+type Reconciler struct {
+	kube client.Client
+}
+
+// Reconcile attempts to create a KubernetesTarget for a KubernetesCluster.
+func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
+	log.V(logging.Debug).Info("reconciling", "kind", workloadv1alpha1.KubernetesTargetKindAPIVersion, "request", req)
+
+	ctx, cancel := context.WithTimeout(context.Background(), reconcileTimeout)
+	defer cancel()
+
+	cluster := &computev1alpha1.KubernetesCluster{}
+	if err := r.kube.Get(ctx, req.NamespacedName, cluster); err != nil {
+		return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetKubernetesCluster)
+	}
+
+	// This KubernetesCluster has been deleted. The KubernetesTarget will be
+	// cleaned up by garbage collection.
+	if meta.WasDeleted(cluster) {
+		return reconcile.Result{Requeue: false}, nil
+	}
+
+	target := &workloadv1alpha1.KubernetesTarget{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:            string(cluster.GetUID()),
+			Namespace:       cluster.GetNamespace(),
+			OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.ReferenceTo(cluster, computev1alpha1.KubernetesClusterGroupVersionKind))},
+		},
+	}
+
+	_, err := util.CreateOrUpdate(ctx, r.kube, target, func() error {
+		if c := metav1.GetControllerOf(target); c == nil || c.UID != cluster.GetUID() {
+			return errors.New(errTargetConflict)
+		}
+
+		// The Target secret reference is set to match the KubernetesCluster and
+		// all labels are propagated. Subsequent updates to the secret reference
+		// or labels of the KubernetesCluster will also be propagated to the
+		// Target.
+		target.SetWriteConnectionSecretToReference(cluster.GetWriteConnectionSecretToReference())
+		target.SetLabels(cluster.GetLabels())
+
+		return nil
+	})
+
+	return reconcile.Result{}, errors.Wrap(err, errCreateOrUpdateTarget)
+}
diff --git a/pkg/controller/workload/workload.go b/pkg/controller/workload/workload.go
index 7c44326f3..137dda64a 100644
--- a/pkg/controller/workload/workload.go
+++ b/pkg/controller/workload/workload.go
@@ -22,6 +22,7 @@ import (
 	"github.com/crossplaneio/crossplane/pkg/controller/workload/kubernetes/application"
 	"github.com/crossplaneio/crossplane/pkg/controller/workload/kubernetes/resource"
 	"github.com/crossplaneio/crossplane/pkg/controller/workload/kubernetes/scheduler"
+	"github.com/crossplaneio/crossplane/pkg/controller/workload/kubernetes/target"
 )
 
 // Controllers passes down config and adds individual controllers to the manager.
@@ -41,5 +42,9 @@ func (c *Controllers) SetupWithManager(mgr ctrl.Manager) error {
 		return err
 	}
 
+	if err := (&target.Controller{}).SetupWithManager(mgr); err != nil {
+		return err
+	}
+
 	return nil
 }


Test patch:
diff --git a/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetestargets.yaml b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetestargets.yaml
new file mode 100644
index 000000000..365820ff0
--- /dev/null
+++ b/cluster/charts/crossplane/templates/crds/workload.crossplane.io_kubernetestargets.yaml
@@ -0,0 +1,145 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.4
+  creationTimestamp: null
+  name: kubernetestargets.workload.crossplane.io
+spec:
+  additionalPrinterColumns:
+  - JSONPath: .spec.clusterRef.name
+    name: CLUSTER
+    type: string
+  group: workload.crossplane.io
+  names:
+    kind: KubernetesTarget
+    listKind: KubernetesTargetList
+    plural: kubernetestargets
+    singular: kubernetestarget
+  scope: Namespaced
+  subresources: {}
+  validation:
+    openAPIV3Schema:
+      description: A KubernetesTarget is a scheduling target for a Kubernetes Application.
+      properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: A TargetSpec defines the common fields of objects used for
+            exposing infrastructure to workloads that can be scheduled to.
+          properties:
+            clusterRef:
+              description: A ResourceReference specifies an existing managed resource,
+                in any namespace, which this target should attempt to propagate a
+                connection secret from.
+              properties:
+                apiVersion:
+                  description: API version of the referent.
+                  type: string
+                fieldPath:
+                  description: 'If referring to a piece of an object instead of an
+                    entire object, this string should contain a valid JSON/Go field
+                    access statement, such as desiredState.manifest.containers[2].
+                    For example, if the object reference is to a container within
+                    a pod, this would take on a value like: "spec.containers{name}"
+                    (where "name" refers to the name of the container that triggered
+                    the event) or if no container name is specified "spec.containers[2]"
+                    (container with index 2 in this pod). This syntax is chosen only
+                    to have some well-defined way of referencing a part of an object.
+                    TODO: this design is not final and this field is subject to change
+                    in the future.'
+                  type: string
+                kind:
+                  description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+                  type: string
+                name:
+                  description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
+                  type: string
+                namespace:
+                  description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
+                  type: string
+                resourceVersion:
+                  description: 'Specific resourceVersion to which this reference is
+                    made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
+                  type: string
+                uid:
+                  description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
+                  type: string
+              type: object
+            connectionSecretRef:
+              description: WriteConnectionSecretToReference specifies the name of
+                a Secret, in the same namespace as this target, to which any connection
+                details for this target should be written or already exist. Connection
+                secrets referenced by a target should contain information for connecting
+                to a resource that allows for scheduling of workloads.
+              properties:
+                name:
+                  description: Name of the secret.
+                  type: string
+              required:
+              - name
+              type: object
+          type: object
+        status:
+          description: A TargetStatus defines the observed status a target.
+          properties:
+            conditions:
+              description: Conditions of the resource.
+              items:
+                description: A Condition that may apply to a resource.
+                properties:
+                  lastTransitionTime:
+                    description: LastTransitionTime is the last time this condition
+                      transitioned from one status to another.
+                    format: date-time
+                    type: string
+                  message:
+                    description: A Message containing details about this condition's
+                      last transition from one status to another, if any.
+                    type: string
+                  reason:
+                    description: A Reason for this condition's last transition from
+                      one status to another.
+                    type: string
+                  status:
+                    description: Status of this condition; is it currently True, False,
+                      or Unknown?
+                    type: string
+                  type:
+                    description: Type of this condition. At most one of each condition
+                      type may apply to a resource at any point in time.
+                    type: string
+                required:
+                - lastTransitionTime
+                - reason
+                - status
+                - type
+                type: object
+              type: array
+          type: object
+      required:
+      - spec
+      type: object
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/pkg/controller/workload/kubernetes/application/application_test.go b/pkg/controller/workload/kubernetes/application/application_test.go
index b03d9e1e8..5b60d842a 100644
--- a/pkg/controller/workload/kubernetes/application/application_test.go
+++ b/pkg/controller/workload/kubernetes/application/application_test.go
@@ -34,7 +34,6 @@ import (
 
 	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/crossplaneio/crossplane-runtime/pkg/test"
-	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
 	"github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
 )
 
@@ -62,11 +61,11 @@ var (
 		},
 	}
 
-	cluster = &computev1alpha1.KubernetesCluster{
-		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolCluster"},
+	target = &v1alpha1.KubernetesTarget{
+		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolTarget"},
 	}
 
-	clusterRef = &v1alpha1.KubernetesClusterReference{Name: cluster.GetName()}
+	targetRef = &v1alpha1.KubernetesTargetReference{Name: target.GetName()}
 
 	resourceA = &v1alpha1.KubernetesApplicationResource{
 		ObjectMeta: metav1.ObjectMeta{
@@ -80,8 +79,8 @@ var (
 		},
 		Spec: templateA.Spec,
 		Status: v1alpha1.KubernetesApplicationResourceStatus{
-			State:   v1alpha1.KubernetesApplicationResourceStateScheduled,
-			Cluster: clusterRef,
+			State:  v1alpha1.KubernetesApplicationResourceStateScheduled,
+			Target: targetRef,
 		},
 	}
 )
@@ -104,9 +103,9 @@ func withSubmittedResources(i int) kubeAppModifier {
 	return func(r *v1alpha1.KubernetesApplication) { r.Status.SubmittedResources = i }
 }
 
-func withCluster(name string) kubeAppModifier {
+func withTarget(name string) kubeAppModifier {
 	return func(r *v1alpha1.KubernetesApplication) {
-		r.Status.Cluster = &v1alpha1.KubernetesClusterReference{Name: name}
+		r.Status.Target = &v1alpha1.KubernetesTargetReference{Name: name}
 	}
 }
 
@@ -137,7 +136,7 @@ func TestCreatePredicate(t *testing.T) {
 			event: event.CreateEvent{
 				Object: &v1alpha1.KubernetesApplication{
 					Status: v1alpha1.KubernetesApplicationStatus{
-						Cluster: clusterRef,
+						Target: targetRef,
 					},
 				},
 			},
@@ -179,7 +178,7 @@ func TestUpdatePredicate(t *testing.T) {
 			event: event.UpdateEvent{
 				ObjectNew: &v1alpha1.KubernetesApplication{
 					Status: v1alpha1.KubernetesApplicationStatus{
-						Cluster: clusterRef,
+						Target: targetRef,
 					},
 				},
 			},
@@ -628,7 +627,7 @@ func TestRenderTemplate(t *testing.T) {
 	}{
 		{
 			name:     "Successful",
-			app:      kubeApp(withCluster(cluster.GetName())),
+			app:      kubeApp(withTarget(target.GetName())),
 			template: &templateA,
 			want:     resourceA,
 		},
diff --git a/pkg/controller/workload/kubernetes/resource/resource_test.go b/pkg/controller/workload/kubernetes/resource/resource_test.go
index d265ecc25..50d9d2e30 100644
--- a/pkg/controller/workload/kubernetes/resource/resource_test.go
+++ b/pkg/controller/workload/kubernetes/resource/resource_test.go
@@ -42,7 +42,6 @@ import (
 
 	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/crossplaneio/crossplane-runtime/pkg/test"
-	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
 	"github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
 )
 
@@ -63,16 +62,14 @@ var (
 	}
 	ctx = context.Background()
 
-	cluster = &computev1alpha1.KubernetesCluster{
-		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolCluster"},
-		Spec: computev1alpha1.KubernetesClusterSpec{
-			ResourceClaimSpec: runtimev1alpha1.ResourceClaimSpec{
-				WriteConnectionSecretToReference: &runtimev1alpha1.LocalSecretReference{Name: secret.GetName()},
-			},
+	target = &v1alpha1.KubernetesTarget{
+		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolTarget"},
+		Spec: runtimev1alpha1.TargetSpec{
+			WriteConnectionSecretToReference: &runtimev1alpha1.LocalSecretReference{Name: secret.GetName()},
 		},
 	}
 
-	clusterRef = &v1alpha1.KubernetesClusterReference{Name: cluster.GetName()}
+	targetRef = &v1alpha1.KubernetesTargetReference{Name: target.GetName()}
 
 	apiServerURL, _ = url.Parse("https://example.org")
 	malformedURL    = ":wat:"
@@ -198,9 +195,9 @@ func withDeletionTimestamp(t time.Time) kubeARModifier {
 	}
 }
 
-func withCluster(name string) kubeARModifier {
+func withTarget(name string) kubeARModifier {
 	return func(r *v1alpha1.KubernetesApplicationResource) {
-		r.Status.Cluster = &v1alpha1.KubernetesClusterReference{Name: name}
+		r.Status.Target = &v1alpha1.KubernetesTargetReference{Name: name}
 	}
 }
 
@@ -237,7 +234,7 @@ func TestCreatePredicate(t *testing.T) {
 			event: event.CreateEvent{
 				Object: &v1alpha1.KubernetesApplicationResource{
 					Status: v1alpha1.KubernetesApplicationResourceStatus{
-						Cluster: clusterRef,
+						Target: targetRef,
 					},
 				},
 			},
@@ -279,7 +276,7 @@ func TestUpdatePredicate(t *testing.T) {
 			event: event.UpdateEvent{
 				ObjectNew: &v1alpha1.KubernetesApplicationResource{
 					Status: v1alpha1.KubernetesApplicationResourceStatus{
-						Cluster: clusterRef,
+						Target: targetRef,
 					},
 				},
 			},
@@ -1044,19 +1041,19 @@ func TestConnectConfig(t *testing.T) {
 				kube: &test.MockClient{
 					MockGet: func(_ context.Context, got client.ObjectKey, obj runtime.Object) error {
 						switch actual := obj.(type) {
-						case *computev1alpha1.KubernetesCluster:
+						case *v1alpha1.KubernetesTarget:
 							want := client.ObjectKey{
-								Namespace: cluster.GetNamespace(),
-								Name:      cluster.GetName(),
+								Namespace: target.GetNamespace(),
+								Name:      target.GetName(),
 							}
 							if diff := cmp.Diff(want, got); diff != "" {
 								return errors.Errorf("MockGet(Secret): -want, +got: %s", diff)
 							}
-							*actual = *cluster
+							*actual = *target
 
 						case *corev1.Secret:
 							want := client.ObjectKey{
-								Namespace: cluster.GetNamespace(),
+								Namespace: target.GetNamespace(),
 								Name:      secret.GetName(),
 							}
 							if diff := cmp.Diff(want, got); diff != "" {
@@ -1071,7 +1068,7 @@ func TestConnectConfig(t *testing.T) {
 				},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar: kubeAR(withCluster(cluster.GetName())),
+			ar: kubeAR(withTarget(target.GetName())),
 			wantConfig: &rest.Config{
 				Host:     apiServerURL.String(),
 				Username: string(secret.Data[runtimev1alpha1.ResourceCredentialsSecretUserKey]),
@@ -1094,14 +1091,14 @@ func TestConnectConfig(t *testing.T) {
 				v1alpha1.KubernetesApplicationResourceKind, objectMeta.GetNamespace(), objectMeta.GetName()),
 		},
 		{
-			name: "GetKubernetesClusterFailed",
+			name: "GetKubernetesTargetFailed",
 			connecter: &clusterConnecter{
 				kube:    &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar: kubeAR(withCluster(cluster.GetName())),
+			ar: kubeAR(withTarget(target.GetName())),
 			wantErr: errors.Wrapf(errorBoom, "cannot get %s %s/%s",
-				computev1alpha1.KubernetesClusterKind, cluster.GetNamespace(), cluster.GetName()),
+				v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
 		},
 		{
 			name: "GetConnectionSecretFailed",
@@ -1109,8 +1106,8 @@ func TestConnectConfig(t *testing.T) {
 				kube: &test.MockClient{
 					MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
 						switch actual := obj.(type) {
-						case *computev1alpha1.KubernetesCluster:
-							*actual = *cluster
+						case *v1alpha1.KubernetesTarget:
+							*actual = *target
 						case *corev1.Secret:
 							return errorBoom
 						}
@@ -1119,7 +1116,7 @@ func TestConnectConfig(t *testing.T) {
 				},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar:      kubeAR(withCluster(cluster.GetName())),
+			ar:      kubeAR(withTarget(target.GetName())),
 			wantErr: errors.Wrapf(errorBoom, "cannot get secret %s/%s", secret.GetNamespace(), secret.GetName()),
 		},
 		{
@@ -1128,8 +1125,8 @@ func TestConnectConfig(t *testing.T) {
 				kube: &test.MockClient{
 					MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
 						switch actual := obj.(type) {
-						case *computev1alpha1.KubernetesCluster:
-							*actual = *cluster
+						case *v1alpha1.KubernetesTarget:
+							*actual = *target
 						case *corev1.Secret:
 							s := secret.DeepCopy()
 							s.Data[runtimev1alpha1.ResourceCredentialsSecretEndpointKey] = []byte(malformedURL)
@@ -1140,7 +1137,7 @@ func TestConnectConfig(t *testing.T) {
 				},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar:      kubeAR(withCluster(cluster.GetName())),
+			ar:      kubeAR(withTarget(target.GetName())),
 			wantErr: errors.WithStack(errors.Errorf("cannot parse Kubernetes endpoint as URL: parse %s: missing protocol scheme", malformedURL)),
 		},
 	}
@@ -1173,15 +1170,15 @@ func TestConnect(t *testing.T) {
 			connecter: &clusterConnecter{
 				kube: &test.MockClient{
 					MockGet: func(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
-						if actual, ok := obj.(*computev1alpha1.KubernetesCluster); ok {
-							*actual = *cluster
+						if actual, ok := obj.(*v1alpha1.KubernetesTarget); ok {
+							*actual = *target
 						}
 						return nil
 					},
 				},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar: kubeAR(withCluster(cluster.GetName())),
+			ar: kubeAR(withTarget(target.GetName())),
 
 			// This empty struct is 'identical' to the actual, populated struct
 			// returned by tc.connecter.connect() because we do not compare
@@ -1197,7 +1194,7 @@ func TestConnect(t *testing.T) {
 				kube:    &test.MockClient{MockGet: test.NewMockGetFn(nil)},
 				options: client.Options{Mapper: mockRESTMapper{}},
 			},
-			ar: kubeAR(withCluster(cluster.GetName())),
+			ar: kubeAR(withTarget(target.GetName())),
 
 			// This empty struct is 'identical' to the actual, populated struct
 			// returned by tc.connecter.connect() because we do not compare
@@ -1205,7 +1202,7 @@ func TestConnect(t *testing.T) {
 			// because doing so would mostly be testing controller-runtime's
 			// client.New() code, not ours.
 			wantErr: errors.Wrap(
-				errors.Errorf("%s %s/%s has no connection secret", computev1alpha1.KubernetesClusterKind, cluster.GetNamespace(), cluster.GetName()),
+				errors.Errorf("%s %s/%s has no connection secret", v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
 				"cannot create Kubernetes client configuration"),
 		},
 		{
@@ -1213,9 +1210,9 @@ func TestConnect(t *testing.T) {
 			connecter: &clusterConnecter{
 				kube: &test.MockClient{MockGet: test.NewMockGetFn(errorBoom)},
 			},
-			ar: kubeAR(withCluster(cluster.GetName())),
+			ar: kubeAR(withTarget(target.GetName())),
 			wantErr: errors.Wrapf(errorBoom, "cannot create Kubernetes client configuration: cannot get %s %s/%s",
-				computev1alpha1.KubernetesClusterKind, cluster.GetNamespace(), cluster.GetName()),
+				v1alpha1.KubernetesTargetKind, target.GetNamespace(), target.GetName()),
 		},
 	}
 
diff --git a/pkg/controller/workload/kubernetes/scheduler/scheduler_test.go b/pkg/controller/workload/kubernetes/scheduler/scheduler_test.go
index 53c91efb3..955af31fe 100644
--- a/pkg/controller/workload/kubernetes/scheduler/scheduler_test.go
+++ b/pkg/controller/workload/kubernetes/scheduler/scheduler_test.go
@@ -34,7 +34,6 @@ import (
 
 	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
 	"github.com/crossplaneio/crossplane-runtime/pkg/test"
-	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
 	workloadv1alpha1 "github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
 )
 
@@ -51,21 +50,21 @@ var (
 
 	selectorAll = &metav1.LabelSelector{}
 
-	clusterA = &computev1alpha1.KubernetesCluster{
-		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolClusterA"},
-		Spec: computev1alpha1.KubernetesClusterSpec{ResourceClaimSpec: runtimev1alpha1.ResourceClaimSpec{
+	targetA = &workloadv1alpha1.KubernetesTarget{
+		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolTargetA"},
+		Spec: runtimev1alpha1.TargetSpec{
 			WriteConnectionSecretToReference: &runtimev1alpha1.LocalSecretReference{},
-		}},
+		},
 	}
-	clusterB = &computev1alpha1.KubernetesCluster{
-		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolClusterB"},
-		Spec: computev1alpha1.KubernetesClusterSpec{ResourceClaimSpec: runtimev1alpha1.ResourceClaimSpec{
+	targetB = &workloadv1alpha1.KubernetesTarget{
+		ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "coolTargetB"},
+		Spec: runtimev1alpha1.TargetSpec{
 			WriteConnectionSecretToReference: &runtimev1alpha1.LocalSecretReference{},
-		}},
+		},
 	}
 
-	clusters = &computev1alpha1.KubernetesClusterList{
-		Items: []computev1alpha1.KubernetesCluster{*clusterA, *clusterB},
+	targets = &workloadv1alpha1.KubernetesTargetList{
+		Items: []workloadv1alpha1.KubernetesTarget{*targetA, *targetB},
 	}
 )
 
@@ -85,15 +84,15 @@ func withDeletionTimestamp(t time.Time) kubeAppModifier {
 	}
 }
 
-func withCluster(name string) kubeAppModifier {
+func withTarget(name string) kubeAppModifier {
 	return func(r *workloadv1alpha1.KubernetesApplication) {
-		r.Status.Cluster = &workloadv1alpha1.KubernetesClusterReference{Name: name}
+		r.Status.Target = &workloadv1alpha1.KubernetesTargetReference{Name: name}
 	}
 }
 
-func withClusterSelector(s *metav1.LabelSelector) kubeAppModifier {
+func withTargetSelector(s *metav1.LabelSelector) kubeAppModifier {
 	return func(r *workloadv1alpha1.KubernetesApplication) {
-		r.Spec.ClusterSelector = s
+		r.Spec.TargetSelector = s
 	}
 }
 
@@ -118,7 +117,7 @@ func TestCreatePredicate(t *testing.T) {
 			event: event.CreateEvent{
 				Object: &workloadv1alpha1.KubernetesApplication{
 					Status: workloadv1alpha1.KubernetesApplicationStatus{
-						Cluster: nil,
+						Target: nil,
 					},
 				},
 			},
@@ -129,7 +128,7 @@ func TestCreatePredicate(t *testing.T) {
 			event: event.CreateEvent{
 				Object: &workloadv1alpha1.KubernetesApplication{
 					Status: workloadv1alpha1.KubernetesApplicationStatus{
-						Cluster: &workloadv1alpha1.KubernetesClusterReference{Name: "coolClustetr"},
+						Target: &workloadv1alpha1.KubernetesTargetReference{Name: "coolTarget"},
 					},
 				},
 			},
@@ -164,7 +163,7 @@ func TestUpdatePredicate(t *testing.T) {
 			event: event.UpdateEvent{
 				ObjectNew: &workloadv1alpha1.KubernetesApplication{
 					Status: workloadv1alpha1.KubernetesApplicationStatus{
-						Cluster: nil,
+						Target: nil,
 					},
 				},
 			},
@@ -175,7 +174,7 @@ func TestUpdatePredicate(t *testing.T) {
 			event: event.UpdateEvent{
 				ObjectNew: &workloadv1alpha1.KubernetesApplication{
 					Status: workloadv1alpha1.KubernetesApplicationStatus{
-						Cluster: &workloadv1alpha1.KubernetesClusterReference{Name: "coolCluster"},
+						Target: &workloadv1alpha1.KubernetesTargetReference{Name: "coolTarget"},
 					},
 				},
 			},
@@ -213,46 +212,46 @@ func TestSchedule(t *testing.T) {
 			scheduler: &roundRobinScheduler{
 				kube: &test.MockClient{
 					MockList: func(_ context.Context, obj runtime.Object, _ ...client.ListOption) error {
-						*obj.(*computev1alpha1.KubernetesClusterList) = *clusters
+						*obj.(*workloadv1alpha1.KubernetesTargetList) = *targets
 						return nil
 					},
 				},
 			},
-			app: kubeApp(withClusterSelector(selectorAll)),
+			app: kubeApp(withTargetSelector(selectorAll)),
 			wantApp: kubeApp(
-				withClusterSelector(selectorAll),
-				withCluster(clusterA.GetName()),
+				withTargetSelector(selectorAll),
+				withTarget(targetA.GetName()),
 				withState(workloadv1alpha1.KubernetesApplicationStateScheduled),
 				withConditions(runtimev1alpha1.ReconcileSuccess()),
 			),
 			wantResult: reconcile.Result{Requeue: false},
 		},
 		{
-			name: "ErrorListingClusters",
+			name: "ErrorListingTargets",
 			scheduler: &roundRobinScheduler{
 				kube: &test.MockClient{MockList: test.NewMockListFn(errorBoom)},
 			},
-			app: kubeApp(withClusterSelector(selectorAll)),
+			app: kubeApp(withTargetSelector(selectorAll)),
 			wantApp: kubeApp(
-				withClusterSelector(selectorAll),
+				withTargetSelector(selectorAll),
 				withState(workloadv1alpha1.KubernetesApplicationStatePending),
 				withConditions(runtimev1alpha1.ReconcileError(errorBoom)),
 			),
 			wantResult: reconcile.Result{Requeue: true},
 		},
 		{
-			name: "NoMatchingClusters",
+			name: "NoMatchingTargets",
 			scheduler: &roundRobinScheduler{
 				kube: &test.MockClient{
 					MockList: func(_ context.Context, obj runtime.Object, _ ...client.ListOption) error {
-						*obj.(*computev1alpha1.KubernetesClusterList) = computev1alpha1.KubernetesClusterList{}
+						*obj.(*workloadv1alpha1.KubernetesTargetList) = workloadv1alpha1.KubernetesTargetList{}
 						return nil
 					},
 				},
 			},
-			app: kubeApp(withClusterSelector(selectorAll)),
+			app: kubeApp(withTargetSelector(selectorAll)),
 			wantApp: kubeApp(
-				withClusterSelector(selectorAll),
+				withTargetSelector(selectorAll),
 				withState(workloadv1alpha1.KubernetesApplicationStatePending),
 				withConditions(runtimev1alpha1.ReconcileSuccess()),
 			),
@@ -337,7 +336,7 @@ func TestReconcile(t *testing.T) {
 				kube: &test.MockClient{
 					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
 						*obj.(*workloadv1alpha1.KubernetesApplication) = *(kubeApp(
-							withCluster("coolCluster"),
+							withTarget("coolTarget"),
 						))
 						return nil
 					},
diff --git a/pkg/controller/workload/kubernetes/target/target_test.go b/pkg/controller/workload/kubernetes/target/target_test.go
new file mode 100644
index 000000000..2b1567c18
--- /dev/null
+++ b/pkg/controller/workload/kubernetes/target/target_test.go
@@ -0,0 +1,409 @@
+/*
+Copyright 2019 The Crossplane 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 target
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/pkg/errors"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	runtimev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
+	"github.com/crossplaneio/crossplane-runtime/pkg/test"
+
+	computev1alpha1 "github.com/crossplaneio/crossplane/apis/compute/v1alpha1"
+	workloadv1alpha1 "github.com/crossplaneio/crossplane/apis/workload/v1alpha1"
+)
+
+const (
+	namespace  = "coolNamespace"
+	name       = "coolCluster"
+	uid        = types.UID("definitely-a-uuid")
+	targetName = "definitely-a-uuid"
+)
+
+var (
+	errorBoom  = errors.New("boom")
+	objectMeta = metav1.ObjectMeta{Namespace: namespace, Name: name, UID: uid}
+	targetMeta = metav1.ObjectMeta{Namespace: namespace, Name: targetName}
+)
+
+type kubeClusterModifier func(*computev1alpha1.KubernetesCluster)
+
+func withDeletionTimestamp(t time.Time) kubeClusterModifier {
+	return func(r *computev1alpha1.KubernetesCluster) {
+		r.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: t}
+	}
+}
+
+func withWriteConnectionSecretToRef(s *runtimev1alpha1.LocalSecretReference) kubeClusterModifier {
+	return func(r *computev1alpha1.KubernetesCluster) {
+		r.Spec.WriteConnectionSecretToReference = s
+	}
+}
+
+func withLabels(l map[string]string) kubeClusterModifier {
+	return func(r *computev1alpha1.KubernetesCluster) {
+		r.SetLabels(l)
+	}
+}
+
+func kubeCluster(rm ...kubeClusterModifier) *computev1alpha1.KubernetesCluster {
+	r := &computev1alpha1.KubernetesCluster{ObjectMeta: objectMeta}
+
+	for _, m := range rm {
+		m(r)
+	}
+
+	return r
+}
+
+type kubeTargetModifier func(*workloadv1alpha1.KubernetesTarget)
+
+func withOwnerReferences(o []metav1.OwnerReference) kubeTargetModifier {
+	return func(r *workloadv1alpha1.KubernetesTarget) {
+		r.SetOwnerReferences(o)
+	}
+}
+
+func withConnectionSecretRef(s *runtimev1alpha1.LocalSecretReference) kubeTargetModifier {
+	return func(r *workloadv1alpha1.KubernetesTarget) {
+		r.SetWriteConnectionSecretToReference(s)
+	}
+}
+
+func withTargetLabels(l map[string]string) kubeTargetModifier {
+	return func(r *workloadv1alpha1.KubernetesTarget) {
+		r.SetLabels(l)
+	}
+}
+
+func kubeTarget(rm ...kubeTargetModifier) *workloadv1alpha1.KubernetesTarget {
+	r := &workloadv1alpha1.KubernetesTarget{ObjectMeta: targetMeta}
+
+	for _, m := range rm {
+		m(r)
+	}
+
+	return r
+}
+
+func TestClusterIsBound(t *testing.T) {
+	cases := map[string]struct {
+		obj  runtime.Object
+		want bool
+	}{
+		"Bound": {
+			obj: &computev1alpha1.KubernetesCluster{
+				Status: runtimev1alpha1.ResourceClaimStatus{
+					BindingStatus: runtimev1alpha1.BindingStatus{
+						Phase: runtimev1alpha1.BindingPhaseBound,
+					},
+				},
+			},
+			want: true,
+		},
+		"Unbound": {
+			obj: &computev1alpha1.KubernetesCluster{
+				Status: runtimev1alpha1.ResourceClaimStatus{
+					BindingStatus: runtimev1alpha1.BindingStatus{
+						Phase: runtimev1alpha1.BindingPhaseUnbound,
+					},
+				},
+			},
+			want: false,
+		},
+		"NotAKubernetesCluster": {
+			obj:  nil,
+			want: false,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			got := clusterIsBound(tc.obj)
+			if got != tc.want {
+				t.Errorf("clusterIsBound(...): got %v, want %v", got, tc.want)
+			}
+		})
+	}
+}
+
+func TestReconcile(t *testing.T) {
+	controller := true
+
+	cases := map[string]struct {
+		rec        *Reconciler
+		req        reconcile.Request
+		wantResult reconcile.Result
+		wantErr    error
+	}{
+		"FailedToGetNonExistentKubernetesCluster": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, name)),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{Requeue: false},
+			wantErr:    nil,
+		},
+		"FailedToGetExtantKubernetesCluster": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: test.NewMockGetFn(errorBoom),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{Requeue: false},
+			wantErr:    errors.Wrap(errorBoom, errGetKubernetesCluster),
+		},
+		"KubernetesClusterDeleted": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						*obj.(*computev1alpha1.KubernetesCluster) = *(kubeCluster(withDeletionTimestamp(time.Now())))
+						return nil
+					},
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{Requeue: false},
+			wantErr:    nil,
+		},
+		"FailedToUpdateKubernetesTarget": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						switch key {
+						case client.ObjectKey{Namespace: namespace, Name: name}:
+							*obj.(*computev1alpha1.KubernetesCluster) = *kubeCluster(withWriteConnectionSecretToRef(
+								&runtimev1alpha1.LocalSecretReference{Name: "super-secret"},
+							))
+						case client.ObjectKey{Namespace: namespace, Name: targetName}:
+							*obj.(*workloadv1alpha1.KubernetesTarget) = *kubeTarget(withOwnerReferences(
+								[]metav1.OwnerReference{
+									{
+										UID:        uid,
+										Name:       name,
+										Kind:       computev1alpha1.KubernetesClusterKind,
+										APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+										Controller: &controller,
+									},
+								},
+							), withConnectionSecretRef(&runtimev1alpha1.LocalSecretReference{Name: "wrong-secret"}))
+						}
+						return nil
+					},
+					MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
+						return errorBoom
+					}),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{},
+			wantErr:    errors.Wrap(errorBoom, errCreateOrUpdateTarget),
+		},
+		"KubernetesTargetConflict": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						switch key {
+						case client.ObjectKey{Namespace: namespace, Name: name}:
+							*obj.(*computev1alpha1.KubernetesCluster) = *kubeCluster()
+						case client.ObjectKey{Namespace: namespace, Name: targetName}:
+							*obj.(*workloadv1alpha1.KubernetesTarget) = *kubeTarget(withOwnerReferences(
+								[]metav1.OwnerReference{
+									{
+										UID:        "wrong-uid",
+										Controller: &controller,
+									},
+								},
+							))
+						}
+						return nil
+					},
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{},
+			wantErr:    errors.Wrap(errors.New(errTargetConflict), errCreateOrUpdateTarget),
+		},
+		"SuccessfulCreateTarget": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						clusterKey := client.ObjectKey{Namespace: namespace, Name: name}
+						if key == clusterKey {
+							*obj.(*computev1alpha1.KubernetesCluster) = *kubeCluster(withWriteConnectionSecretToRef(
+								&runtimev1alpha1.LocalSecretReference{Name: "super-secret"},
+							), withLabels(map[string]string{"dev": "true"}))
+							return nil
+						}
+
+						return kerrors.NewNotFound(schema.GroupResource{}, "")
+					},
+					MockCreate: test.NewMockCreateFn(nil, func(got runtime.Object) error {
+						want := kubeTarget(withOwnerReferences(
+							[]metav1.OwnerReference{
+								{
+									UID:        uid,
+									Name:       name,
+									Kind:       computev1alpha1.KubernetesClusterKind,
+									APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+									Controller: &controller,
+								},
+							},
+						),
+							withConnectionSecretRef(&runtimev1alpha1.LocalSecretReference{Name: "super-secret"}),
+							withTargetLabels(map[string]string{"dev": "true"}))
+						if diff := cmp.Diff(want, got); diff != "" {
+							t.Errorf("-want, +got:\n%s", diff)
+						}
+						return nil
+					}),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{},
+			wantErr:    nil,
+		},
+		"SuccessfulUpdateExistingFilledTarget": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						switch key {
+						case client.ObjectKey{Namespace: namespace, Name: name}:
+							*obj.(*computev1alpha1.KubernetesCluster) = *kubeCluster(withWriteConnectionSecretToRef(
+								&runtimev1alpha1.LocalSecretReference{Name: "super-secret"},
+							), withLabels(map[string]string{"dev": "true"}))
+						case client.ObjectKey{Namespace: namespace, Name: targetName}:
+							*obj.(*workloadv1alpha1.KubernetesTarget) = *kubeTarget(withOwnerReferences(
+								[]metav1.OwnerReference{
+									{
+										UID:        uid,
+										Name:       name,
+										Kind:       computev1alpha1.KubernetesClusterKind,
+										APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+										Controller: &controller,
+									},
+								},
+							),
+								withConnectionSecretRef(&runtimev1alpha1.LocalSecretReference{Name: "super-secret"}),
+								withTargetLabels(map[string]string{"not": "dev"}))
+						}
+						return nil
+					},
+					MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
+						want := kubeTarget(withOwnerReferences(
+							[]metav1.OwnerReference{
+								{
+									UID:        uid,
+									Name:       name,
+									Kind:       computev1alpha1.KubernetesClusterKind,
+									APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+									Controller: &controller,
+								},
+							},
+						),
+							withConnectionSecretRef(&runtimev1alpha1.LocalSecretReference{Name: "super-secret"}),
+							withTargetLabels(map[string]string{"dev": "true"}))
+						if diff := cmp.Diff(want, got); diff != "" {
+							t.Errorf("-want, +got:\n%s", diff)
+						}
+						return nil
+					}),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{},
+			wantErr:    nil,
+		},
+		"SuccessfulUpdateExistingEmptyTarget": {
+			rec: &Reconciler{
+				kube: &test.MockClient{
+					MockGet: func(_ context.Context, key client.ObjectKey, obj runtime.Object) error {
+						switch key {
+						case client.ObjectKey{Namespace: namespace, Name: name}:
+							*obj.(*computev1alpha1.KubernetesCluster) = *kubeCluster(withWriteConnectionSecretToRef(
+								&runtimev1alpha1.LocalSecretReference{Name: "super-secret"},
+							), withLabels(map[string]string{"dev": "true"}))
+						case client.ObjectKey{Namespace: namespace, Name: targetName}:
+							*obj.(*workloadv1alpha1.KubernetesTarget) = *kubeTarget(withOwnerReferences(
+								[]metav1.OwnerReference{
+									{
+										UID:        uid,
+										Name:       name,
+										Kind:       computev1alpha1.KubernetesClusterKind,
+										APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+										Controller: &controller,
+									},
+								},
+							))
+						}
+						return nil
+					},
+					MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
+						want := kubeTarget(withOwnerReferences(
+							[]metav1.OwnerReference{
+								{
+									UID:        uid,
+									Name:       name,
+									Kind:       computev1alpha1.KubernetesClusterKind,
+									APIVersion: computev1alpha1.SchemeGroupVersion.String(),
+									Controller: &controller,
+								},
+							},
+						),
+							withConnectionSecretRef(&runtimev1alpha1.LocalSecretReference{Name: "super-secret"}),
+							withTargetLabels(map[string]string{"dev": "true"}))
+						if diff := cmp.Diff(want, got); diff != "" {
+							t.Errorf("-want, +got:\n%s", diff)
+						}
+						return nil
+					}),
+				},
+			},
+			req:        reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace, Name: name}},
+			wantResult: reconcile.Result{},
+			wantErr:    nil,
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			gotResult, gotErr := tc.rec.Reconcile(tc.req)
+
+			if diff := cmp.Diff(tc.wantErr, gotErr, test.EquateErrors()); diff != "" {
+				t.Errorf("tc.rec.Reconcile(...): want error != got error:\n%s", diff)
+			}
+
+			if diff := cmp.Diff(tc.wantResult, gotResult); diff != "" {
+				t.Errorf("tc.rec.Reconcile(...): -want, +got:\n%s", diff)
+			}
+		})
+	}
+}