Repository: kubernetes-sigs/cluster-api
Pull request #4102 (2021-01-21 19:48:11)

🌱 Cherry-pick: Add optional parameter for using repolist
<!-- 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:, feature additions), 🐛 (:bug:, patch and bugfixes), 📖 (:book:, documentation or proposals), or 🌱 (:seedling:, minor or other) -->

**What this PR does / why we need it**:
Cherry-pick of: https://github.com/kubernetes-sigs/cluster-api/pull/4069

Enables providers using release-3.0 use this library to do testing using repolist (required for Windows)

Being used in https://github.com/kubernetes-sigs/cluster-api-provider-azure/pull/1119

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #



Patch:
diff --git a/cmd/clusterctl/client/client.go b/cmd/clusterctl/client/client.go
index a579f2d4c..496ef1c21 100644
--- a/cmd/clusterctl/client/client.go
+++ b/cmd/clusterctl/client/client.go
@@ -21,7 +21,6 @@ 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/client/repository"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/tree"
 )
 
 // Client is exposes the clusterctl high-level client library.
@@ -66,9 +65,6 @@ type Client interface {
 	// ProcessYAML provides a direct way to process a yaml and inspect its
 	// variables.
 	ProcessYAML(options ProcessYAMLOptions) (YamlPrinter, error)
-
-	// DescribeCluster returns the object tree representing the status of a Cluster API cluster.
-	DescribeCluster(options DescribeClusterOptions) (*tree.ObjectTree, error)
 }
 
 // YamlPrinter exposes methods that prints the processed template and
diff --git a/cmd/clusterctl/client/describe.go b/cmd/clusterctl/client/describe.go
deleted file mode 100644
index 78bea56c6..000000000
--- a/cmd/clusterctl/client/describe.go
+++ /dev/null
@@ -1,79 +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 client
-
-import (
-	"context"
-
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/tree"
-)
-
-// DescribeClusterOptions carries the options supported by DescribeCluster.
-type DescribeClusterOptions struct {
-	// Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty,
-	// default rules for kubeconfig discovery will be used.
-	Kubeconfig Kubeconfig
-
-	// Namespace where the workload cluster is located. If unspecified, the current namespace will be used.
-	Namespace string
-
-	// ClusterName to be used for the workload cluster.
-	ClusterName string
-
-	// ShowOtherConditions is a list of comma separated kind or kind/name for which we should add the ShowObjectConditionsAnnotation
-	// to signal to the presentation layer to show all the conditions for the objects.
-	ShowOtherConditions string
-
-	// DisableNoEcho disable hiding MachineInfrastructure or BootstrapConfig objects if the object's ready condition is true
-	// or it has the same Status, Severity and Reason of the parent's object ready condition (it is an echo)
-	DisableNoEcho bool
-
-	// DisableGrouping disable grouping machines objects in case the ready condition
-	// has the same Status, Severity and Reason
-	DisableGrouping bool
-}
-
-// DescribeCluster returns the object tree representing the status of a Cluster API cluster.
-func (c *clusterctlClient) DescribeCluster(options DescribeClusterOptions) (*tree.ObjectTree, error) {
-	// gets access to the management cluster
-	cluster, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig})
-	if err != nil {
-		return nil, err
-	}
-
-	// If the option specifying the Namespace is empty, try to detect it.
-	if options.Namespace == "" {
-		currentNamespace, err := cluster.Proxy().CurrentNamespace()
-		if err != nil {
-			return nil, err
-		}
-		options.Namespace = currentNamespace
-	}
-
-	// Fetch the Cluster client.
-	client, err := cluster.Proxy().NewClient()
-	if err != nil {
-		return nil, err
-	}
-
-	// Gets the object tree representing the status of a Cluster API cluster.
-	return tree.Discovery(context.TODO(), client, options.Namespace, options.ClusterName, tree.DiscoverOptions{
-		ShowOtherConditions: options.ShowOtherConditions,
-		DisableNoEcho:       options.DisableNoEcho,
-		DisableGrouping:     options.DisableGrouping,
-	})
-}
diff --git a/cmd/clusterctl/client/tree/annotations.go b/cmd/clusterctl/client/tree/annotations.go
deleted file mode 100644
index 03bdaa9ab..000000000
--- a/cmd/clusterctl/client/tree/annotations.go
+++ /dev/null
@@ -1,131 +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 tree
-
-import (
-	"strconv"
-
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-const (
-	// ShowObjectConditionsAnnotation documents that the presentation layer should show all the conditions for the object.
-	ShowObjectConditionsAnnotation = "tree.cluster.x-k8s.io.io/show-conditions"
-
-	// ObjectMetaNameAnnotation contains the meta name that should be used for the object in the presentation layer,
-	// e.g. control plane for KCP.
-	ObjectMetaNameAnnotation = "tree.cluster.x-k8s.io.io/meta-name"
-
-	// VirtualObjectAnnotation documents that the object does not correspond to any real object, but instead is
-	// a virtual object introduced to provide a better representation of the cluster status, e.g. workers.
-	VirtualObjectAnnotation = "tree.cluster.x-k8s.io.io/virtual-object"
-
-	// GroupingObjectAnnotation is an annotation that should be applied to a node in order to trigger the grouping action
-	// when adding the node's children. e.g. if you have a control-plane node, and you apply this annotation, then
-	// the control-plane machines added as a children of this node will be grouped in case the ready condition
-	// has the same Status, Severity and Reason.
-	GroupingObjectAnnotation = "tree.cluster.x-k8s.io.io/grouping-object"
-
-	// GroupObjectAnnotation is an annotation that documents that a node is the result of a grouping operation, and
-	// thus the node is representing group of sibling nodes, e.g. a group of machines.
-	GroupObjectAnnotation = "tree.cluster.x-k8s.io.io/group-object"
-
-	// GroupItemsAnnotation contains the list of names for the objects included in a group object.
-	GroupItemsAnnotation = "tree.cluster.x-k8s.io.io/group-items"
-
-	// GroupItemsSeparator is the separator used in the GroupItemsAnnotation
-	GroupItemsSeparator = ", "
-)
-
-// GetMetaName returns the object meta name that should be used for the object in the presentation layer, if defined.
-func GetMetaName(obj controllerutil.Object) string {
-	if val, ok := getAnnotation(obj, ObjectMetaNameAnnotation); ok {
-		return val
-	}
-	return ""
-}
-
-// IsGroupingObject returns true in case the object is responsible to trigger the grouping action
-// when adding the object's children. e.g. A control-plane object, could be responsible of grouping
-// the control-plane machines while added as a children objects.
-func IsGroupingObject(obj controllerutil.Object) bool {
-	if val, ok := getBoolAnnotation(obj, GroupingObjectAnnotation); ok {
-		return val
-	}
-	return false
-}
-
-// IsGroupObject return true if the object is the result of a grouping operation, and
-// thus the object is representing group of sibling object, e.g. a group of machines.
-func IsGroupObject(obj controllerutil.Object) bool {
-	if val, ok := getBoolAnnotation(obj, GroupObjectAnnotation); ok {
-		return val
-	}
-	return false
-}
-
-// GetGroupItems return the list of names for the objects included in a group object.
-func GetGroupItems(obj controllerutil.Object) string {
-	if val, ok := getAnnotation(obj, GroupItemsAnnotation); ok {
-		return val
-	}
-	return ""
-}
-
-// IsVirtualObject return true if the object does not correspond to any real object, but instead it is
-// a virtual object introduced to provide a better representation of the cluster status.
-func IsVirtualObject(obj controllerutil.Object) bool {
-	if val, ok := getBoolAnnotation(obj, VirtualObjectAnnotation); ok {
-		return val
-	}
-	return false
-}
-
-// IsShowConditionsObject returns true if the presentation layer should show all the conditions for the object.
-func IsShowConditionsObject(obj controllerutil.Object) bool {
-	if val, ok := getBoolAnnotation(obj, ShowObjectConditionsAnnotation); ok {
-		return val
-	}
-	return false
-}
-
-func getAnnotation(obj controllerutil.Object, annotation string) (string, bool) {
-	if obj == nil {
-		return "", false
-	}
-	val, ok := obj.GetAnnotations()[annotation]
-	return val, ok
-}
-
-func getBoolAnnotation(obj controllerutil.Object, annotation string) (bool, bool) {
-	val, ok := getAnnotation(obj, annotation)
-	if ok {
-		if boolVal, err := strconv.ParseBool(val); err == nil {
-			return boolVal, true
-		}
-	}
-	return false, false
-}
-
-func addAnnotation(obj controllerutil.Object, annotation, value string) {
-	annotations := obj.GetAnnotations()
-	if annotations == nil {
-		annotations = map[string]string{}
-	}
-	annotations[annotation] = value
-	obj.SetAnnotations(annotations)
-}
diff --git a/cmd/clusterctl/client/tree/discovery.go b/cmd/clusterctl/client/tree/discovery.go
deleted file mode 100644
index da2f1ce3a..000000000
--- a/cmd/clusterctl/client/tree/discovery.go
+++ /dev/null
@@ -1,231 +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 tree
-
-import (
-	"context"
-
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/controllers/external"
-	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-// DiscoverOptions define options for the discovery process.
-type DiscoverOptions struct {
-	// ShowOtherConditions is a list of comma separated kind or kind/name for which we should add the ShowObjectConditionsAnnotation
-	// to signal to the presentation layer to show all the conditions for the objects.
-	ShowOtherConditions string
-
-	// DisableNoEcho disable hiding MachineInfrastructure or BootstrapConfig objects if the object's ready condition is true
-	// or it has the same Status, Severity and Reason of the parent's object ready condition (it is an echo)
-	DisableNoEcho bool
-
-	// DisableGrouping disable grouping machines objects in case the ready condition
-	// has the same Status, Severity and Reason
-	DisableGrouping bool
-}
-
-func (d DiscoverOptions) toObjectTreeOptions() ObjectTreeOptions {
-	return ObjectTreeOptions{
-		ShowOtherConditions: d.ShowOtherConditions,
-		DisableNoEcho:       d.DisableNoEcho,
-		DisableGrouping:     d.DisableGrouping,
-	}
-}
-
-// Discovery returns an object tree representing the status of a Cluster API cluster.
-func Discovery(ctx context.Context, c client.Client, namespace, name string, options DiscoverOptions) (*ObjectTree, error) {
-	// Fetch the Cluster instance.
-	cluster := &clusterv1.Cluster{}
-	clusterKey := client.ObjectKey{
-		Namespace: namespace,
-		Name:      name,
-	}
-	if err := c.Get(ctx, clusterKey, cluster); err != nil {
-		return nil, err
-	}
-
-	// Create an object tree with the cluster as root
-	tree := NewObjectTree(cluster, options.toObjectTreeOptions())
-
-	// Adds cluster infra
-	if clusterInfra, err := external.Get(ctx, c, cluster.Spec.InfrastructureRef, cluster.Namespace); err == nil {
-		tree.Add(cluster, clusterInfra, ObjectMetaName("ClusterInfrastructure"))
-	}
-
-	// Adds control plane
-	controlPLane, err := external.Get(ctx, c, cluster.Spec.ControlPlaneRef, cluster.Namespace)
-	if err == nil {
-		tree.Add(cluster, controlPLane, ObjectMetaName("ControlPlane"), GroupingObject(true))
-	}
-
-	// Adds control plane machines.
-	machinesList, err := getMachinesInCluster(ctx, c, cluster.Namespace, cluster.Name)
-	if err != nil {
-		return nil, err
-	}
-	machineMap := map[string]bool{}
-	addMachineFunc := func(parent controllerutil.Object, m *clusterv1.Machine) {
-		_, visible := tree.Add(parent, m)
-		machineMap[m.Name] = true
-
-		if visible {
-			if machineInfra, err := external.Get(ctx, c, &m.Spec.InfrastructureRef, cluster.Namespace); err == nil {
-				tree.Add(m, machineInfra, ObjectMetaName("MachineInfrastructure"), NoEcho(true))
-			}
-
-			if machineBootstrap, err := external.Get(ctx, c, m.Spec.Bootstrap.ConfigRef, cluster.Namespace); err == nil {
-				tree.Add(m, machineBootstrap, ObjectMetaName("BootstrapConfig"), NoEcho(true))
-			}
-		}
-	}
-
-	controlPlaneMachines := selectControlPlaneMachines(machinesList)
-	for i := range controlPlaneMachines {
-		cp := controlPlaneMachines[i]
-		addMachineFunc(controlPLane, cp)
-	}
-
-	if len(machinesList.Items) == len(controlPlaneMachines) {
-		return tree, nil
-	}
-
-	workers := VirtualObject(cluster.Namespace, "WorkerGroup", "Workers")
-	tree.Add(cluster, workers)
-
-	// Adds worker machines.
-	machinesDeploymentList, err := getMachineDeploymentsInCluster(ctx, c, cluster.Namespace, cluster.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	machineSetList, err := getMachineSetsInCluster(ctx, c, cluster.Namespace, cluster.Name)
-	if err != nil {
-		return nil, err
-	}
-
-	for i := range machinesDeploymentList.Items {
-		md := &machinesDeploymentList.Items[i]
-		tree.Add(workers, md, GroupingObject(true))
-
-		machineSets := selectMachinesSetsControlledBy(machineSetList, md)
-		for i := range machineSets {
-			ms := machineSets[i]
-
-			machines := selectMachinesControlledBy(machinesList, ms)
-			for _, w := range machines {
-				addMachineFunc(md, w)
-			}
-		}
-	}
-
-	// Handles orphan machines.
-	if len(machineMap) < len(machinesList.Items) {
-		other := VirtualObject(cluster.Namespace, "OtherGroup", "Other")
-		tree.Add(workers, other)
-
-		for i := range machinesList.Items {
-			m := &machinesList.Items[i]
-			if _, ok := machineMap[m.Name]; ok {
-				continue
-			}
-			addMachineFunc(other, m)
-		}
-	}
-
-	return tree, nil
-}
-
-func getMachinesInCluster(ctx context.Context, c client.Client, namespace, name string) (*clusterv1.MachineList, error) {
-	if name == "" {
-		return nil, nil
-	}
-
-	machineList := &clusterv1.MachineList{}
-	labels := map[string]string{clusterv1.ClusterLabelName: name}
-
-	if err := c.List(ctx, machineList, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
-		return nil, err
-	}
-
-	return machineList, nil
-}
-
-func getMachineDeploymentsInCluster(ctx context.Context, c client.Client, namespace, name string) (*clusterv1.MachineDeploymentList, error) {
-	if name == "" {
-		return nil, nil
-	}
-
-	machineDeploymentList := &clusterv1.MachineDeploymentList{}
-	labels := map[string]string{clusterv1.ClusterLabelName: name}
-
-	if err := c.List(ctx, machineDeploymentList, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
-		return nil, err
-	}
-
-	return machineDeploymentList, nil
-}
-
-func getMachineSetsInCluster(ctx context.Context, c client.Client, namespace, name string) (*clusterv1.MachineSetList, error) {
-	if name == "" {
-		return nil, nil
-	}
-
-	machineSetList := &clusterv1.MachineSetList{}
-	labels := map[string]string{clusterv1.ClusterLabelName: name}
-
-	if err := c.List(ctx, machineSetList, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil {
-		return nil, err
-	}
-
-	return machineSetList, nil
-}
-
-func selectControlPlaneMachines(machineList *clusterv1.MachineList) []*clusterv1.Machine {
-	machines := []*clusterv1.Machine{}
-	for i := range machineList.Items {
-		m := &machineList.Items[i]
-		if util.IsControlPlaneMachine(m) {
-			machines = append(machines, m)
-		}
-	}
-	return machines
-}
-
-func selectMachinesSetsControlledBy(machineSetList *clusterv1.MachineSetList, controller controllerutil.Object) []*clusterv1.MachineSet {
-	machineSets := []*clusterv1.MachineSet{}
-	for i := range machineSetList.Items {
-		m := &machineSetList.Items[i]
-		if util.IsControlledBy(m, controller) {
-			machineSets = append(machineSets, m)
-		}
-	}
-	return machineSets
-}
-
-func selectMachinesControlledBy(machineList *clusterv1.MachineList, controller controllerutil.Object) []*clusterv1.Machine {
-	machines := []*clusterv1.Machine{}
-	for i := range machineList.Items {
-		m := &machineList.Items[i]
-		if util.IsControlledBy(m, controller) {
-			machines = append(machines, m)
-		}
-	}
-	return machines
-}
diff --git a/cmd/clusterctl/client/tree/doc.go b/cmd/clusterctl/client/tree/doc.go
deleted file mode 100644
index 9ba3ff635..000000000
--- a/cmd/clusterctl/client/tree/doc.go
+++ /dev/null
@@ -1,55 +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 tree
-
-/*
-This package support the generation of an "at glance" view of a Cluster API cluster designed to help the user in quickly
-understanding if there are problems and where.
-
-The "at glance" view is based on the idea that we should avoid to overload the user with information, but instead
-surface problems, if any; in practice:
-
-- The view assumes we are processing objects conforming with https://github.com/kubernetes-sigs/cluster-api/blob/master/docs/proposals/20200506-conditions.md.
-  As a consequence each object should have a Ready condition summarizing the object state.
-
-- The view organizes objects in a hierarchical tree, however it is not required that the
-  tree reflects the ownerReference tree so it is possible to skip objects not relevant for triaging the cluster status
-  e.g. secrets or templates.
-
-- It is possible to add "meta names" to object, thus making hierarchical tree more consistent for the users,
-  e.g. use MachineInfrastructure instead of using all the different infrastructure machine kinds (AWSMachine, VSphereMachine etc.).
-
-- It is possible to add "virtual nodes", thus allowing to make the hierarchical tree more meaningful for the users,
-  e.g. adding a Workers object to group all the MachineDeployments.
-
-- It is possible to "group" siblings objects by ready condition e.g. group all the machines with Ready=true
-  in a single node instead of listing each one of them.
-
-- Given that the ready condition of the child object bubbles up to the parents, it is possible to avoid the "echo"
-  (reporting the same condition at the parent/child) e.g. if a machine's Ready condition is already
-  surface an error from the infrastructure machine, let's avoid to show the InfrastructureMachine
-  given that representing its state is redundant in this case.
-
-- In order to avoid long list of objects (think e.g. a cluster with 50 worker machines), sibling objects with the
-  same value for the ready condition can be grouped together into a virtual node, e.g. 10 Machines ready
-
-The ObjectTree object defined implements all the above behaviors of the "at glance" visualization, by generating
-a tree of Kubernetes objects; each object gets a set of annotation, reflecting its own visualization specific attributes,
-e.g is virtual node, is group node, meta name etc.
-
-The Discovery object uses the ObjectTree to build the "at glance" view of a Cluster API.
-*/
diff --git a/cmd/clusterctl/client/tree/options.go b/cmd/clusterctl/client/tree/options.go
deleted file mode 100644
index 607c4ca61..000000000
--- a/cmd/clusterctl/client/tree/options.go
+++ /dev/null
@@ -1,59 +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 tree
-
-// AddObjectOption define an option for the ObjectTree Add operation.
-type AddObjectOption interface {
-	ApplyToAdd(*addObjectOptions)
-}
-
-type addObjectOptions struct {
-	MetaName       string
-	GroupingObject bool
-	NoEcho         bool
-}
-
-func (o *addObjectOptions) ApplyOptions(opts []AddObjectOption) *addObjectOptions {
-	for _, opt := range opts {
-		opt.ApplyToAdd(o)
-	}
-	return o
-}
-
-// The ObjectMetaName option defines the meta name that should be used for the object in the presentation layer,
-// e.g. control plane for KCP.
-type ObjectMetaName string
-
-func (n ObjectMetaName) ApplyToAdd(options *addObjectOptions) {
-	options.MetaName = string(n)
-}
-
-// The GroupingObject option makes this node responsible of triggering the grouping action
-// when adding the node's children.
-type GroupingObject bool
-
-func (n GroupingObject) ApplyToAdd(options *addObjectOptions) {
-	options.GroupingObject = bool(n)
-}
-
-// The NoEcho options defines if the object should be hidden if the object's ready condition has the
-// same Status, Severity and Reason of the parent's object ready condition (it is an echo).
-type NoEcho bool
-
-func (n NoEcho) ApplyToAdd(options *addObjectOptions) {
-	options.NoEcho = bool(n)
-}
diff --git a/cmd/clusterctl/client/tree/tree.go b/cmd/clusterctl/client/tree/tree.go
deleted file mode 100644
index 701ef0d16..000000000
--- a/cmd/clusterctl/client/tree/tree.go
+++ /dev/null
@@ -1,273 +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 tree
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-
-	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"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-type ObjectTreeOptions struct {
-	// ShowOtherConditions is a list of comma separated kind or kind/name for which we should add   the ShowObjectConditionsAnnotation
-	// to signal to the presentation layer to show all the conditions for the objects.
-	ShowOtherConditions string
-
-	// DisableNoEcho disables hiding objects if the object's ready condition has the
-	// same Status, Severity and Reason of the parent's object ready condition (it is an echo)
-	DisableNoEcho bool
-
-	// DisableGrouping disables grouping sibling objects in case the ready condition
-	// has the same Status, Severity and Reason
-	DisableGrouping bool
-}
-
-// ObjectTree defines an object tree representing the status of a Cluster API cluster.
-type ObjectTree struct {
-	root      controllerutil.Object
-	options   ObjectTreeOptions
-	items     map[types.UID]controllerutil.Object
-	ownership map[types.UID]map[types.UID]bool
-}
-
-func NewObjectTree(root controllerutil.Object, options ObjectTreeOptions) *ObjectTree {
-	return &ObjectTree{
-		root:      root,
-		options:   options,
-		items:     make(map[types.UID]controllerutil.Object),
-		ownership: make(map[types.UID]map[types.UID]bool),
-	}
-}
-
-// Add a object to the object tree.
-func (od ObjectTree) Add(parent, obj controllerutil.Object, opts ...AddObjectOption) (added bool, visible bool) {
-	if parent == nil || obj == nil {
-		return false, false
-	}
-	addOpts := &addObjectOptions{}
-	addOpts.ApplyOptions(opts)
-
-	objReady := GetReadyCondition(obj)
-	parentReady := GetReadyCondition(parent)
-
-	// If it is requested to show all the conditions for the object, add
-	// the ShowObjectConditionsAnnotation to signal this to the presentation layer.
-	if isObjDebug(obj, od.options.ShowOtherConditions) {
-		addAnnotation(obj, ShowObjectConditionsAnnotation, "True")
-	}
-
-	// If the object should be hidden if the object's ready condition is true ot it has the
-	// same Status, Severity and Reason of the parent's object ready condition (it is an echo),
-	// return early.
-	if addOpts.NoEcho && !od.options.DisableNoEcho {
-		if (objReady != nil && objReady.Status == corev1.ConditionTrue) || hasSameReadyStatusSeverityAndReason(parentReady, objReady) {
-			return false, false
-		}
-	}
-
-	// If it is requested to use a meta name for the object in the presentation layer, add
-	// the ObjectMetaNameAnnotation to signal this to the presentation layer.
-	if addOpts.MetaName != "" {
-		addAnnotation(obj, ObjectMetaNameAnnotation, addOpts.MetaName)
-	}
-
-	// If it is requested that this object and its sibling should be grouped in case the ready condition
-	// has the same Status, Severity and Reason, process all the sibling nodes.
-	if IsGroupingObject(parent) {
-		siblings := od.GetObjectsByParent(parent.GetUID())
-
-		for i := range siblings {
-			s := siblings[i]
-			sReady := GetReadyCondition(s)
-
-			// If the object's ready condition has a different Status, Severity and Reason than the sibling object,
-			// move on (they should not be grouped).
-			if !hasSameReadyStatusSeverityAndReason(objReady, sReady) {
-				continue
-			}
-
-			// If the sibling node is already a group object, upgrade it with the current object.
-			if IsGroupObject(s) {
-				updateGroupNode(s, sReady, obj, objReady)
-				return true, false
-			}
-
-			// Otherwise the object and the current sibling should be merged in a group.
-
-			// Create virtual object for the group and add it to the object tree.
-			groupNode := createGroupNode(s, sReady, obj, objReady)
-			od.addInner(parent, groupNode)
-
-			// Remove the current sibling (now merged in the group).
-			od.remove(parent, s)
-			return true, false
-		}
-	}
-
-	// If it is requested that the child of this node should be grouped in case the ready condition
-	// has the same Status, Severity and Reason, add the GroupingObjectAnnotation to signal
-	// this to the presentation layer.
-	if addOpts.GroupingObject && !od.options.DisableGrouping {
-		addAnnotation(obj, GroupingObjectAnnotation, "True")
-	}
-
-	// Add the object to the object tree.
-	od.addInner(parent, obj)
-
-	return true, true
-}
-
-func (od ObjectTree) remove(parent controllerutil.Object, s controllerutil.Object) {
-	for _, child := range od.GetObjectsByParent(s.GetUID()) {
-		od.remove(s, child)
-	}
-	delete(od.items, s.GetUID())
-	delete(od.ownership[parent.GetUID()], s.GetUID())
-}
-
-func (od ObjectTree) addInner(parent controllerutil.Object, obj controllerutil.Object) {
-	od.items[obj.GetUID()] = obj
-	if od.ownership[parent.GetUID()] == nil {
-		od.ownership[parent.GetUID()] = make(map[types.UID]bool)
-	}
-	od.ownership[parent.GetUID()][obj.GetUID()] = true
-}
-
-func (od ObjectTree) GetRoot() controllerutil.Object { return od.root }
-
-func (od ObjectTree) GetObject(id types.UID) controllerutil.Object { return od.items[id] }
-
-func (od ObjectTree) IsObjectWithChild(id types.UID) bool {
-	return len(od.ownership[id]) > 0
-}
-
-func (od ObjectTree) GetObjectsByParent(id types.UID) []controllerutil.Object {
-	out := make([]controllerutil.Object, 0, len(od.ownership[id]))
-	for k := range od.ownership[id] {
-		out = append(out, od.GetObject(k))
-	}
-	return out
-}
-
-func hasSameReadyStatusSeverityAndReason(a, b *clusterv1.Condition) bool {
-	if a == nil && b == nil {
-		return true
-	}
-	if (a == nil) != (b == nil) {
-		return false
-	}
-
-	return a.Status == b.Status &&
-		a.Severity == b.Severity &&
-		a.Reason == b.Reason
-}
-
-func createGroupNode(sibling controllerutil.Object, siblingReady *clusterv1.Condition, obj controllerutil.Object, objReady *clusterv1.Condition) *unstructured.Unstructured {
-	kind := fmt.Sprintf("%sGroup", obj.GetObjectKind().GroupVersionKind().Kind)
-
-	// Create a new group node and add the GroupObjectAnnotation to signal
-	// this to the presentation layer.
-	// NB. The group nodes gets a unique ID to avoid conflicts.
-	groupNode := VirtualObject(obj.GetNamespace(), kind, readyStatusSeverityAndReasonUID(obj))
-	addAnnotation(groupNode, GroupObjectAnnotation, "True")
-
-	// Update the list of items included in the group and store it in the GroupItemsAnnotation.
-	items := []string{obj.GetName(), sibling.GetName()}
-	sort.Strings(items)
-	addAnnotation(groupNode, GroupItemsAnnotation, strings.Join(items, GroupItemsSeparator))
-
-	// Update the group's ready condition.
-	if objReady != nil {
-		objReady.LastTransitionTime = minLastTransitionTime(objReady, siblingReady)
-		objReady.Message = ""
-		setReadyCondition(groupNode, objReady)
-	}
-	return groupNode
-}
-
-func readyStatusSeverityAndReasonUID(obj controllerutil.Object) string {
-	ready := GetReadyCondition(obj)
-	if ready == nil {
-		return fmt.Sprintf("zzz_%s", util.RandomString(6))
-	}
-	return fmt.Sprintf("zz_%s_%s_%s_%s", ready.Status, ready.Severity, ready.Reason, util.RandomString(6))
-}
-
-func minLastTransitionTime(a, b *clusterv1.Condition) metav1.Time {
-	if a == nil && b == nil {
-		return metav1.Time{}
-	}
-	if (a != nil) && (b == nil) {
-		return a.LastTransitionTime
-	}
-	if (a == nil) && (b != nil) {
-		return b.LastTransitionTime
-	}
-	if a.LastTransitionTime.Time.After(b.LastTransitionTime.Time) {
-		return b.LastTransitionTime
-	}
-	return a.LastTransitionTime
-}
-
-func updateGroupNode(groupObj controllerutil.Object, groupReady *clusterv1.Condition, obj controllerutil.Object, objReady *clusterv1.Condition) {
-	// Update the list of items included in the group and store it in the GroupItemsAnnotation.
-	items := strings.Split(GetGroupItems(groupObj), GroupItemsSeparator)
-	items = append(items, obj.GetName())
-	sort.Strings(items)
-	addAnnotation(groupObj, GroupItemsAnnotation, strings.Join(items, GroupItemsSeparator))
-
-	// Update the group's ready condition.
-	if groupReady != nil {
-		groupReady.LastTransitionTime = minLastTransitionTime(objReady, groupReady)
-		groupReady.Message = ""
-		setReadyCondition(groupObj, groupReady)
-	}
-}
-
-func isObjDebug(obj controllerutil.Object, debugFilter string) bool {
-	if debugFilter == "" {
-		return false
-	}
-	for _, filter := range strings.Split(debugFilter, ",") {
-		filter = strings.TrimSpace(filter)
-		if filter == "" {
-			continue
-		}
-		if strings.ToLower(filter) == "all" {
-			return true
-		}
-		kn := strings.Split(filter, "/")
-		if len(kn) == 2 {
-			if obj.GetObjectKind().GroupVersionKind().Kind == kn[0] && obj.GetName() == kn[1] {
-				return true
-			}
-			continue
-		}
-		if obj.GetObjectKind().GroupVersionKind().Kind == kn[0] {
-			return true
-		}
-	}
-	return false
-}
diff --git a/cmd/clusterctl/client/tree/util.go b/cmd/clusterctl/client/tree/util.go
deleted file mode 100644
index c9ee54e1b..000000000
--- a/cmd/clusterctl/client/tree/util.go
+++ /dev/null
@@ -1,108 +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 tree
-
-import (
-	"fmt"
-	"sort"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util/conditions"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-// GetReadyCondition returns the ReadyCondition for an object, if defined.
-func GetReadyCondition(obj controllerutil.Object) *clusterv1.Condition {
-	getter := objToGetter(obj)
-	if getter == nil {
-		return nil
-	}
-	return conditions.Get(getter, clusterv1.ReadyCondition)
-}
-
-// GetOtherConditions returns the other conditions (all the conditions except ready) for an object, if defined.
-func GetOtherConditions(obj controllerutil.Object) []*clusterv1.Condition {
-	getter := objToGetter(obj)
-	if getter == nil {
-		return nil
-	}
-	var conditions []*clusterv1.Condition
-	for _, c := range getter.GetConditions() {
-		c := c
-		if c.Type != clusterv1.ReadyCondition {
-			conditions = append(conditions, &c)
-		}
-	}
-	sort.Slice(conditions, func(i, j int) bool {
-		return conditions[i].Type < conditions[j].Type
-	})
-	return conditions
-}
-
-func setReadyCondition(obj controllerutil.Object, ready *clusterv1.Condition) {
-	setter := objToSetter(obj)
-	if setter == nil {
-		return
-	}
-	conditions.Set(setter, ready)
-}
-
-func objToGetter(obj controllerutil.Object) conditions.Getter {
-	if getter, ok := obj.(conditions.Getter); ok {
-		return getter
-	}
-
-	objUnstructured, ok := obj.(*unstructured.Unstructured)
-	if !ok {
-		return nil
-	}
-	getter := conditions.UnstructuredGetter(objUnstructured)
-	return getter
-}
-
-func objToSetter(obj controllerutil.Object) conditions.Setter {
-	if setter, ok := obj.(conditions.Setter); ok {
-		return setter
-	}
-
-	objUnstructured, ok := obj.(*unstructured.Unstructured)
-	if !ok {
-		return nil
-	}
-	setter := conditions.UnstructuredSetter(objUnstructured)
-	return setter
-}
-
-// VirtualObject return a new virtual object.
-func VirtualObject(namespace, kind, name string) *unstructured.Unstructured {
-	gk := "virtual.cluster.x-k8s.io/v1alpha3"
-	return &unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": gk,
-			"kind":       kind,
-			"metadata": map[string]interface{}{
-				"namespace": namespace,
-				"name":      name,
-				"annotations": map[string]interface{}{
-					VirtualObjectAnnotation: "True",
-				},
-				"uid": fmt.Sprintf("%s, %s/%s", gk, namespace, name),
-			},
-		},
-	}
-}
diff --git a/cmd/clusterctl/cmd/describe.go b/cmd/clusterctl/cmd/describe.go
deleted file mode 100644
index 65e1760f4..000000000
--- a/cmd/clusterctl/cmd/describe.go
+++ /dev/null
@@ -1,31 +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 cmd
-
-import (
-	"github.com/spf13/cobra"
-)
-
-var describeCmd = &cobra.Command{
-	Use:   "describe",
-	Short: "Describe workload clusters.",
-	Long:  `Describe the status of workload clusters.`,
-}
-
-func init() {
-	RootCmd.AddCommand(describeCmd)
-}
diff --git a/cmd/clusterctl/cmd/describe_cluster.go b/cmd/clusterctl/cmd/describe_cluster.go
deleted file mode 100644
index 0a30ea176..000000000
--- a/cmd/clusterctl/cmd/describe_cluster.go
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
-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 cmd
-
-import (
-	"fmt"
-	"sort"
-	"strings"
-	"time"
-
-	"github.com/fatih/color"
-	"github.com/gobuffalo/flect"
-	"github.com/gosuri/uitable"
-	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/util/duration"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/tree"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-const (
-	firstElemPrefix = `├─`
-	lastElemPrefix  = `└─`
-	indent          = "  "
-	pipe            = `│ `
-)
-
-var (
-	gray   = color.New(color.FgHiBlack)
-	red    = color.New(color.FgRed)
-	green  = color.New(color.FgGreen)
-	yellow = color.New(color.FgYellow)
-	white  = color.New(color.FgWhite)
-	cyan   = color.New(color.FgCyan)
-)
-
-type describeClusterOptions struct {
-	kubeconfig        string
-	kubeconfigContext string
-
-	namespace           string
-	showOtherConditions string
-	disableNoEcho       bool
-	disableGrouping     bool
-}
-
-var dc = &describeClusterOptions{}
-
-var describeClusterClusterCmd = &cobra.Command{
-	Use:   "cluster",
-	Short: "Describe workload clusters.",
-	Long: LongDesc(`
-		Provide an "at glance" view of a Cluster API cluster designed to help the user in quickly
-		understanding if there are problems and where.
-		.`),
-
-	Example: Examples(`
-		# Describe the cluster named test-1.
-		clusterctl describe cluster test-1
-
-		# Describe the cluster named test-1 showing all the conditions for the KubeadmControlPlane object kind.
-		clusterctl describe cluster test-1 --show-conditions KubeadmControlPlane
-
-		# Describe the cluster named test-1 showing all the conditions for a specific machine.
-		clusterctl describe cluster test-1 --show-conditions Machine/m1
-
-		# Describe the cluster named test-1 disabling automatic grouping of objects with the same ready condition 
-		# e.g. un-group all the machines with Ready=true instead of showing a single group node.
-		clusterctl describe cluster test-1 --disable-grouping
-
-		# Describe the cluster named test-1 disabling automatic echo suppression 
-        # e.g. show the infrastructure machine objects, no matter if the current state is already reported by the machine's Ready condition.
-		clusterctl describe cluster test-1`),
-
-	Args: cobra.ExactArgs(1),
-	RunE: func(cmd *cobra.Command, args []string) error {
-		return runDescribeCluster(args[0])
-	},
-}
-
-func init() {
-	describeClusterClusterCmd.Flags().StringVar(&dc.kubeconfig, "kubeconfig", "",
-		"Path to a kubeconfig file to use for the management cluster. If empty, default discovery rules apply.")
-	describeClusterClusterCmd.Flags().StringVar(&dc.kubeconfigContext, "kubeconfig-context", "",
-		"Context to be used within the kubeconfig file. If empty, current context will be used.")
-	describeClusterClusterCmd.Flags().StringVarP(&dc.namespace, "namespace", "n", "",
-		"The namespace where the workload cluster is located. If unspecified, the current namespace will be used.")
-
-	describeClusterClusterCmd.Flags().StringVar(&dc.showOtherConditions, "show-conditions", "",
-		" list of comma separated kind or kind/name for which the command should show all the object's conditions (use 'all' to show conditions for everything).")
-	describeClusterClusterCmd.Flags().BoolVar(&dc.disableNoEcho, "disable-no-echo", false, ""+
-		"Disable hiding of a MachineInfrastructure and BootstrapConfig when ready condition is true or it has the Status, Severity and Reason of the machine's object.")
-	describeClusterClusterCmd.Flags().BoolVar(&dc.disableGrouping, "disable-grouping", false,
-		"Disable grouping machines when ready condition has the same Status, Severity and Reason.")
-
-	describeCmd.AddCommand(describeClusterClusterCmd)
-}
-
-func runDescribeCluster(name string) error {
-	c, err := client.New(cfgFile)
-	if err != nil {
-		return err
-	}
-
-	tree, err := c.DescribeCluster(client.DescribeClusterOptions{
-		Kubeconfig:          client.Kubeconfig{Path: dc.kubeconfig, Context: dc.kubeconfigContext},
-		Namespace:           dc.namespace,
-		ClusterName:         name,
-		ShowOtherConditions: dc.showOtherConditions,
-		DisableNoEcho:       dc.disableNoEcho,
-		DisableGrouping:     dc.disableGrouping,
-	})
-	if err != nil {
-		return err
-	}
-
-	printObjectTree(tree)
-	return nil
-}
-
-// printObjectTree prints the cluster status to stdout
-func printObjectTree(tree *tree.ObjectTree) {
-	// Creates the output table
-	tbl := uitable.New()
-	tbl.Separator = "  "
-	tbl.AddRow("NAME", "READY", "SEVERITY", "REASON", "SINCE", "MESSAGE")
-
-	// Add row for the root object, the cluster, and recursively for all the nodes representing the cluster status.
-	addObjectRow("", tbl, tree, tree.GetRoot())
-
-	// Prints the output table
-	fmt.Fprintln(color.Error, tbl)
-}
-
-// addObjectRow add a row for a given object, and recursively for all the object's children.
-// NOTE: each row name gets a prefix, that generates a tree view like representation.
-func addObjectRow(prefix string, tbl *uitable.Table, objectTree *tree.ObjectTree, obj controllerutil.Object) {
-	// Gets the descriptor for the object's ready condition, if any.
-	readyDescriptor := conditionDescriptor{readyColor: gray}
-	if ready := tree.GetReadyCondition(obj); ready != nil {
-		readyDescriptor = newConditionDescriptor(ready)
-	}
-
-	// If the object is a group object, override the condition message with the list of objects in the group. e.g machine-1, machine-2, ...
-	if tree.IsGroupObject(obj) {
-		items := strings.Split(tree.GetGroupItems(obj), tree.GroupItemsSeparator)
-		if len(items) <= 2 {
-			readyDescriptor.message = gray.Sprintf("See %s", strings.Join(items, tree.GroupItemsSeparator))
-		} else {
-			readyDescriptor.message = gray.Sprintf("See %s, ...", strings.Join(items[:2], tree.GroupItemsSeparator))
-		}
-	}
-
-	// Gets the row name for the object.
-	// NOTE: The object name gets manipulated in order to improve readability.
-	name := getRowName(obj)
-
-	// Add the row representing the object that includes
-	// - The row name with the tree view prefix.
-	// - The object's ready condition.
-	tbl.AddRow(
-		fmt.Sprintf("%s%s", gray.Sprint(prefix), name),
-		readyDescriptor.readyColor.Sprint(readyDescriptor.status),
-		readyDescriptor.readyColor.Sprint(readyDescriptor.severity),
-		readyDescriptor.readyColor.Sprint(readyDescriptor.reason),
-		readyDescriptor.age,
-		readyDescriptor.message)
-
-	// If it is required to show all the conditions for the object, add a row for each object's conditions.
-	if tree.IsShowConditionsObject(obj) {
-		addOtherConditions(prefix, tbl, objectTree, obj)
-	}
-
-	// Add a row for each object's children, taking care of updating the tree view prefix.
-	// NOTE: Children objects are sorted by row name for better readability.
-	childrenObj := objectTree.GetObjectsByParent(obj.GetUID())
-	sort.Slice(childrenObj, func(i, j int) bool {
-		return getRowName(childrenObj[i]) < getRowName(childrenObj[j])
-	})
-
-	for i, child := range childrenObj {
-		addObjectRow(getChildPrefix(prefix, i, len(childrenObj)), tbl, objectTree, child)
-	}
-}
-
-// addOtherConditions adds a row for each object condition except the ready condition,
-// which is already represented on the object's main row.
-func addOtherConditions(prefix string, tbl *uitable.Table, objectTree *tree.ObjectTree, obj controllerutil.Object) {
-	// Add a row for each other condition, taking care of updating the tree view prefix.
-	// In this case the tree prefix get a filler, to indent conditions from objects, and eventually a
-	// and additional pipe if the object has children that should be presented after the conditions.
-	filler := strings.Repeat(" ", 10)
-	childrenPipe := indent
-	if objectTree.IsObjectWithChild(obj.GetUID()) {
-		childrenPipe = pipe
-	}
-
-	otherConditions := tree.GetOtherConditions(obj)
-	for i := range otherConditions {
-		otherCondition := otherConditions[i]
-		otherDescriptor := newConditionDescriptor(otherCondition)
-		otherConditionPrefix := getChildPrefix(prefix+childrenPipe+filler, i, len(otherConditions))
-		tbl.AddRow(
-			fmt.Sprintf("%s%s", gray.Sprint(otherConditionPrefix), cyan.Sprint(otherCondition.Type)),
-			otherDescriptor.readyColor.Sprint(otherDescriptor.status),
-			otherDescriptor.readyColor.Sprint(otherDescriptor.severity),
-			otherDescriptor.readyColor.Sprint(otherDescriptor.reason),
-			otherDescriptor.age,
-			otherDescriptor.message)
-	}
-}
-
-// getChildPrefix return the tree view prefix for a row representing a child object.
-func getChildPrefix(currentPrefix string, childIndex, childCount int) string {
-	nextPrefix := currentPrefix
-
-	// Alter the current prefix for hosting the next child object:
-
-	// All ├─ should be replaced by |, so all the existing hierarchic dependencies are carried on
-	nextPrefix = strings.ReplaceAll(nextPrefix, firstElemPrefix, pipe)
-	// All └─ should be replaced by " " because we are under the last element of the tree (nothing to carry on)
-	nextPrefix = strings.ReplaceAll(nextPrefix, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))))
-
-	// Add the prefix for the new child object (├─ for the firsts children, └─ for the last children).
-	if childIndex < childCount-1 {
-		return nextPrefix + firstElemPrefix
-	}
-	return nextPrefix + lastElemPrefix
-}
-
-// getRowName returns the object name in the tree view according to following rules:
-// - group objects are represented as #of objects kind, e.g. 3 Machines...
-// - other virtual objects are represented using the object name, e.g. Workers
-// - objects with a meta name are represented as meta name - (kind/name), e.g. ClusterInfrastructure - DockerCluster/test1
-// - other objects are represented as kind/name, e.g.Machine/test1-md-0-779b87ff56-642vs
-// - if the object is being deleted, a prefix will be added.
-func getRowName(obj controllerutil.Object) string {
-	if tree.IsGroupObject(obj) {
-		items := strings.Split(tree.GetGroupItems(obj), tree.GroupItemsSeparator)
-		kind := flect.Pluralize(strings.TrimSuffix(obj.GetObjectKind().GroupVersionKind().Kind, "Group"))
-		return white.Add(color.Bold).Sprintf("%d %s...", len(items), kind)
-	}
-
-	if tree.IsVirtualObject(obj) {
-		return obj.GetName()
-	}
-
-	objName := fmt.Sprintf("%s/%s",
-		obj.GetObjectKind().GroupVersionKind().Kind,
-		color.New(color.Bold).Sprint(obj.GetName()))
-
-	name := objName
-	if objectPrefix := tree.GetMetaName(obj); objectPrefix != "" {
-		name = fmt.Sprintf("%s - %s", objectPrefix, gray.Sprintf(name))
-	}
-
-	if !obj.GetDeletionTimestamp().IsZero() {
-		name = fmt.Sprintf("%s %s", red.Sprintf("!! DELETED !!"), name)
-	}
-
-	return name
-}
-
-// conditionDescriptor contains all the info for representing a condition.
-type conditionDescriptor struct {
-	readyColor *color.Color
-	age        string
-	status     string
-	severity   string
-	reason     string
-	message    string
-}
-
-// newConditionDescriptor returns a conditionDescriptor for the given condition.
-func newConditionDescriptor(c *clusterv1.Condition) conditionDescriptor {
-	v := conditionDescriptor{}
-
-	v.status = string(c.Status)
-	v.severity = string(c.Severity)
-	v.reason = c.Reason
-	v.message = c.Message
-
-	// Eventually cut the message to keep the table dimension under control.
-	if len(v.message) > 100 {
-		v.message = fmt.Sprintf("%s ...", v.message[:100])
-	}
-
-	// Compute the condition age.
-	v.age = duration.HumanDuration(time.Since(c.LastTransitionTime.Time))
-
-	// Determine the color to be used for showing the conditions according to Status and Severity in case Status is false.
-	switch c.Status {
-	case corev1.ConditionTrue:
-		v.readyColor = green
-	case corev1.ConditionFalse, corev1.ConditionUnknown:
-		switch c.Severity {
-		case clusterv1.ConditionSeverityError:
-			v.readyColor = red
-		case clusterv1.ConditionSeverityWarning:
-			v.readyColor = yellow
-		default:
-			v.readyColor = white
-		}
-	default:
-		v.readyColor = gray
-	}
-
-	return v
-}
diff --git a/go.mod b/go.mod
index 68a2784b4..23c35a504 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,6 @@ require (
 	github.com/docker/distribution v2.7.1+incompatible
 	github.com/drone/envsubst v1.0.3-0.20200709223903-efdb65b94e5a
 	github.com/evanphx/json-patch v4.9.0+incompatible
-	github.com/fatih/color v1.7.0
 	github.com/go-logr/logr v0.1.0
 	github.com/gobuffalo/flect v0.2.2
 	github.com/gogo/protobuf v1.3.1 // indirect
@@ -19,7 +18,6 @@ require (
 	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/gosuri/uitable v0.0.4
 	github.com/onsi/ginkgo v1.12.1
 	github.com/onsi/gomega v1.10.1
 	github.com/opencontainers/go-digest v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 8b8a2b4a7..16d2e4f3d 100644
--- a/go.sum
+++ b/go.sum
@@ -111,7 +111,6 @@ github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5I
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@@ -233,8 +232,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
-github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
@@ -301,12 +298,10 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
 github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
-github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=


Test patch:
diff --git a/cmd/clusterctl/client/client_test.go b/cmd/clusterctl/client/client_test.go
index 993acf01f..3f323f7db 100644
--- a/cmd/clusterctl/client/client_test.go
+++ b/cmd/clusterctl/client/client_test.go
@@ -29,7 +29,6 @@ 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/client/repository"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/tree"
 	yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
 	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
@@ -118,10 +117,6 @@ func (f fakeClient) ProcessYAML(options ProcessYAMLOptions) (YamlPrinter, error)
 	return f.internalClient.ProcessYAML(options)
 }
 
-func (f fakeClient) DescribeCluster(options DescribeClusterOptions) (*tree.ObjectTree, error) {
-	return f.internalClient.DescribeCluster(options)
-}
-
 // newFakeClient returns a clusterctl client that allows to execute tests on a set of fake config, fake repositories and fake clusters.
 // you can use WithCluster and WithRepository to prepare for the test case.
 func newFakeClient(configClient config.Client) *fakeClient {
diff --git a/cmd/clusterctl/client/tree/discovery_test.go b/cmd/clusterctl/client/tree/discovery_test.go
deleted file mode 100644
index 94b64b15c..000000000
--- a/cmd/clusterctl/client/tree/discovery_test.go
+++ /dev/null
@@ -1,306 +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 tree
-
-import (
-	"context"
-	"strings"
-	"testing"
-
-	. "github.com/onsi/gomega"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-func Test_Discovery(t *testing.T) {
-	type nodeCheck func(*WithT, controllerutil.Object)
-	type args struct {
-		objs            []runtime.Object
-		discoverOptions DiscoverOptions
-	}
-	tests := []struct {
-		name          string
-		args          args
-		wantTree      map[string][]string
-		wantNodeCheck map[string]nodeCheck
-	}{
-		{
-			name: "Discovery with default discovery settings",
-			args: args{
-				discoverOptions: DiscoverOptions{},
-				objs: test.NewFakeCluster("ns1", "cluster1").
-					WithControlPlane(
-						test.NewFakeControlPlane("cp").
-							WithMachines(
-								test.NewFakeMachine("cp1"),
-							),
-					).
-					WithMachineDeployments(
-						test.NewFakeMachineDeployment("md1").
-							WithMachineSets(
-								test.NewFakeMachineSet("ms1").
-									WithMachines(
-										test.NewFakeMachine("m1"),
-										test.NewFakeMachine("m2"),
-									),
-							),
-					).
-					Objs(),
-			},
-			wantTree: map[string][]string{
-				// Cluster should be parent of InfrastructureCluster, ControlPlane, and WorkerNodes
-				"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/cluster1": {
-					"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1",
-					"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp",
-					"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers",
-				},
-				// InfrastructureCluster should be leaf
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": {},
-				// ControlPlane should have a machine
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": {
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1",
-				},
-				// Machine should be leaf (no echo)
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1": {},
-				// Workers should have a machine deployment
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": {
-					"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1",
-				},
-				// Machine deployment should have a group of machines (grouping)
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": {
-					"virtual.cluster.x-k8s.io/v1alpha3, ns1/zzz_",
-				},
-			},
-			wantNodeCheck: map[string]nodeCheck{
-				// InfrastructureCluster should have a meta name
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ClusterInfrastructure"))
-				},
-				// ControlPlane should have a meta name, be a grouping object
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ControlPlane"))
-					g.Expect(IsGroupingObject(obj)).To(BeTrue())
-				},
-				// Workers should be a virtual node
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsVirtualObject(obj)).To(BeTrue())
-				},
-				// Machine deployment should be a grouping object
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsGroupingObject(obj)).To(BeTrue())
-				},
-			},
-		},
-		{
-			name: "Discovery with grouping disabled",
-			args: args{
-				discoverOptions: DiscoverOptions{
-					DisableGrouping: true,
-				},
-				objs: test.NewFakeCluster("ns1", "cluster1").
-					WithControlPlane(
-						test.NewFakeControlPlane("cp").
-							WithMachines(
-								test.NewFakeMachine("cp1"),
-							),
-					).
-					WithMachineDeployments(
-						test.NewFakeMachineDeployment("md1").
-							WithMachineSets(
-								test.NewFakeMachineSet("ms1").
-									WithMachines(
-										test.NewFakeMachine("m1"),
-										test.NewFakeMachine("m2"),
-									),
-							),
-					).
-					Objs(),
-			},
-			wantTree: map[string][]string{
-				// Cluster should be parent of InfrastructureCluster, ControlPlane, and WorkerNodes
-				"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/cluster1": {
-					"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1",
-					"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp",
-					"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers",
-				},
-				// InfrastructureCluster should be leaf
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": {},
-				// ControlPlane should have a machine
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": {
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1",
-				},
-				// Workers should have a machine deployment
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": {
-					"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1",
-				},
-				// Machine deployment should have a group of machines
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": {
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m1",
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m2",
-				},
-				// Machine should be leaf (no echo)
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1": {},
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m1":  {},
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m2":  {},
-			},
-			wantNodeCheck: map[string]nodeCheck{
-				// InfrastructureCluster should have a meta name
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ClusterInfrastructure"))
-				},
-				// ControlPlane should have a meta name, should NOT be a grouping object
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ControlPlane"))
-					g.Expect(IsGroupingObject(obj)).To(BeFalse())
-				},
-				// Workers should be a virtual node
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsVirtualObject(obj)).To(BeTrue())
-				},
-				// Machine deployment should NOT be a grouping object
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsGroupingObject(obj)).To(BeFalse())
-				},
-			},
-		},
-		{
-			name: "Discovery with grouping and no-echo disabled",
-			args: args{
-				discoverOptions: DiscoverOptions{
-					DisableGrouping: true,
-					DisableNoEcho:   true,
-				},
-				objs: test.NewFakeCluster("ns1", "cluster1").
-					WithControlPlane(
-						test.NewFakeControlPlane("cp").
-							WithMachines(
-								test.NewFakeMachine("cp1"),
-							),
-					).
-					WithMachineDeployments(
-						test.NewFakeMachineDeployment("md1").
-							WithMachineSets(
-								test.NewFakeMachineSet("ms1").
-									WithMachines(
-										test.NewFakeMachine("m1"),
-									),
-							),
-					).
-					Objs(),
-			},
-			wantTree: map[string][]string{
-				// Cluster should be parent of InfrastructureCluster, ControlPlane, and WorkerNodes
-				"cluster.x-k8s.io/v1alpha3, Kind=Cluster, ns1/cluster1": {
-					"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1",
-					"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp",
-					"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers",
-				},
-				// InfrastructureCluster should be leaf
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": {},
-				// ControlPlane should have a machine
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": {
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1",
-				},
-				// Machine should have infra machine and bootstrap (echo)
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/cp1": {
-					"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureMachine, ns1/cp1",
-					"bootstrap.cluster.x-k8s.io/v1alpha3, Kind=GenericBootstrapConfig, ns1/cp1",
-				},
-				// Workers should have a machine deployment
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": {
-					"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1",
-				},
-				// Machine deployment should have a group of machines
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": {
-					"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m1",
-				},
-				// Machine should have infra machine and bootstrap (echo)
-				"cluster.x-k8s.io/v1alpha3, Kind=Machine, ns1/m1": {
-					"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureMachine, ns1/m1",
-					"bootstrap.cluster.x-k8s.io/v1alpha3, Kind=GenericBootstrapConfig, ns1/m1",
-				},
-			},
-			wantNodeCheck: map[string]nodeCheck{
-				// InfrastructureCluster should have a meta name
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureCluster, ns1/cluster1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ClusterInfrastructure"))
-				},
-				// ControlPlane should have a meta name, should NOT be a grouping object
-				"controlplane.cluster.x-k8s.io/v1alpha3, Kind=GenericControlPlane, ns1/cp": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("ControlPlane"))
-					g.Expect(IsGroupingObject(obj)).To(BeFalse())
-				},
-				// Workers should be a virtual node
-				"virtual.cluster.x-k8s.io/v1alpha3, ns1/Workers": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsVirtualObject(obj)).To(BeTrue())
-				},
-				// Machine deployment should NOT be a grouping object
-				"cluster.x-k8s.io/v1alpha3, Kind=MachineDeployment, ns1/md1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(IsGroupingObject(obj)).To(BeFalse())
-				},
-				// infra machines and boostrap should have meta names
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureMachine, ns1/cp1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("MachineInfrastructure"))
-				},
-				"bootstrap.cluster.x-k8s.io/v1alpha3, Kind=GenericBootstrapConfig, ns1/cp1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("BootstrapConfig"))
-				},
-				"infrastructure.cluster.x-k8s.io/v1alpha3, Kind=GenericInfrastructureMachine, ns1/m1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("MachineInfrastructure"))
-				},
-				"bootstrap.cluster.x-k8s.io/v1alpha3, Kind=GenericBootstrapConfig, ns1/m1": func(g *WithT, obj controllerutil.Object) {
-					g.Expect(GetMetaName(obj)).To(Equal("BootstrapConfig"))
-				},
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			client, err := test.NewFakeProxy().WithObjs(tt.args.objs...).NewClient()
-			g.Expect(client).ToNot(BeNil())
-			g.Expect(err).ToNot(HaveOccurred())
-
-			tree, err := Discovery(context.TODO(), client, "ns1", "cluster1", tt.args.discoverOptions)
-			g.Expect(tree).ToNot(BeNil())
-			g.Expect(err).ToNot(HaveOccurred())
-
-			for parent, wantChildren := range tt.wantTree {
-				gotChildren := tree.GetObjectsByParent(types.UID(parent))
-				g.Expect(wantChildren).To(HaveLen(len(gotChildren)), "%q doesn't have the expected number of children nodes", parent)
-
-				for _, gotChild := range gotChildren {
-					found := false
-					for _, wantChild := range wantChildren {
-						if strings.HasPrefix(string(gotChild.GetUID()), wantChild) {
-							found = true
-							break
-						}
-					}
-					g.Expect(found).To(BeTrue(), "got child %q for parent %q, expecting [%s]", gotChild.GetUID(), parent, strings.Join(wantChildren, "] ["))
-
-					if test, ok := tt.wantNodeCheck[string(gotChild.GetUID())]; ok {
-						test(g, gotChild)
-					}
-				}
-			}
-		})
-	}
-}
diff --git a/cmd/clusterctl/client/tree/tree_test.go b/cmd/clusterctl/client/tree/tree_test.go
deleted file mode 100644
index 3928fa4df..000000000
--- a/cmd/clusterctl/client/tree/tree_test.go
+++ /dev/null
@@ -1,765 +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 tree
-
-import (
-	"strings"
-	"testing"
-	"time"
-
-	. "github.com/onsi/gomega"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/types"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/util/conditions"
-)
-
-func Test_hasSameReadyStatusSeverityAndReason(t *testing.T) {
-	readyTrue := conditions.TrueCondition(clusterv1.ReadyCondition)
-	readyFalseReasonInfo := conditions.FalseCondition(clusterv1.ReadyCondition, "Reason", clusterv1.ConditionSeverityInfo, "message falseInfo1")
-	readyFalseAnotherReasonInfo := conditions.FalseCondition(clusterv1.ReadyCondition, "AnotherReason", clusterv1.ConditionSeverityInfo, "message falseInfo1")
-	readyFalseReasonWarning := conditions.FalseCondition(clusterv1.ReadyCondition, "Reason", clusterv1.ConditionSeverityWarning, "message falseInfo1")
-
-	type args struct {
-		a *clusterv1.Condition
-		b *clusterv1.Condition
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "Objects without conditions are the same",
-			args: args{
-				a: nil,
-				b: nil,
-			},
-			want: true,
-		},
-		{
-			name: "Objects with same Ready condition are the same",
-			args: args{
-				a: readyTrue,
-				b: readyTrue,
-			},
-			want: true,
-		},
-		{
-			name: "Objects with different Ready.Status are not the same",
-			args: args{
-				a: readyTrue,
-				b: readyFalseReasonInfo,
-			},
-			want: false,
-		},
-		{
-			name: "Objects with different Ready.Reason are not the same",
-			args: args{
-				a: readyFalseReasonInfo,
-				b: readyFalseAnotherReasonInfo,
-			},
-			want: false,
-		},
-		{
-			name: "Objects with different Ready.Severity are not the same",
-			args: args{
-				a: readyFalseReasonInfo,
-				b: readyFalseReasonWarning,
-			},
-			want: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			got := hasSameReadyStatusSeverityAndReason(tt.args.a, tt.args.b)
-			g.Expect(got).To(Equal(tt.want))
-		})
-	}
-}
-
-func Test_minLastTransitionTime(t *testing.T) {
-	now := &clusterv1.Condition{Type: "now", LastTransitionTime: metav1.Now()}
-	beforeNow := &clusterv1.Condition{Type: "beforeNow", LastTransitionTime: metav1.Time{Time: now.LastTransitionTime.Time.Add(-1 * time.Hour)}}
-	type args struct {
-		a *clusterv1.Condition
-		b *clusterv1.Condition
-	}
-	tests := []struct {
-		name string
-		args args
-		want metav1.Time
-	}{
-		{
-			name: "nil, nil should return empty time",
-			args: args{
-				a: nil,
-				b: nil,
-			},
-			want: metav1.Time{},
-		},
-		{
-			name: "nil, now should return now",
-			args: args{
-				a: nil,
-				b: now,
-			},
-			want: now.LastTransitionTime,
-		},
-		{
-			name: "now, nil should return now",
-			args: args{
-				a: now,
-				b: nil,
-			},
-			want: now.LastTransitionTime,
-		},
-		{
-			name: "now, beforeNow should return beforeNow",
-			args: args{
-				a: now,
-				b: beforeNow,
-			},
-			want: beforeNow.LastTransitionTime,
-		},
-		{
-			name: "beforeNow, now should return beforeNow",
-			args: args{
-				a: now,
-				b: beforeNow,
-			},
-			want: beforeNow.LastTransitionTime,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			got := minLastTransitionTime(tt.args.a, tt.args.b)
-			g.Expect(got.Time).To(BeTemporally("~", tt.want.Time))
-		})
-	}
-}
-
-func Test_isObjDebug(t *testing.T) {
-	obj := fakeMachine("my-machine")
-	type args struct {
-		filter string
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "empty filter should return false",
-			args: args{
-				filter: "",
-			},
-			want: false,
-		},
-		{
-			name: "all filter should return true",
-			args: args{
-				filter: "all",
-			},
-			want: true,
-		},
-		{
-			name: "kind filter should return true",
-			args: args{
-				filter: "Machine",
-			},
-			want: true,
-		},
-		{
-			name: "another kind filter should return false",
-			args: args{
-				filter: "AnotherKind",
-			},
-			want: false,
-		},
-		{
-			name: "kind/name filter should return true",
-			args: args{
-				filter: "Machine/my-machine",
-			},
-			want: true,
-		},
-		{
-			name: "kind/wrong name filter should return false",
-			args: args{
-				filter: "Cluster/another-cluster",
-			},
-			want: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			got := isObjDebug(obj, tt.args.filter)
-			g.Expect(got).To(Equal(tt.want))
-		})
-	}
-}
-
-func Test_createGroupNode(t *testing.T) {
-	now := metav1.Now()
-	beforeNow := metav1.Time{Time: now.Time.Add(-1 * time.Hour)}
-
-	obj := &clusterv1.Machine{
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Machine",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      "my-machine",
-		},
-		Status: clusterv1.MachineStatus{
-			Conditions: clusterv1.Conditions{
-				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: now},
-			},
-		},
-	}
-
-	sibling := &clusterv1.Machine{
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Machine",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      "sibling-machine",
-		},
-		Status: clusterv1.MachineStatus{
-			Conditions: clusterv1.Conditions{
-				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: beforeNow},
-			},
-		},
-	}
-
-	want := &unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": "virtual.cluster.x-k8s.io/v1alpha3",
-			"kind":       "MachineGroup",
-			"metadata": map[string]interface{}{
-				"namespace": "ns",
-				"name":      "", // random string
-				"annotations": map[string]interface{}{
-					VirtualObjectAnnotation: "True",
-					GroupObjectAnnotation:   "True",
-					GroupItemsAnnotation:    "my-machine, sibling-machine",
-				},
-				"uid": "", // random string
-			},
-			"status": map[string]interface{}{
-				"conditions": []interface{}{
-					map[string]interface{}{
-						"status":             "",
-						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
-						"type":               "Ready",
-					},
-				},
-			},
-		},
-	}
-
-	g := NewWithT(t)
-	got := createGroupNode(sibling, GetReadyCondition(sibling), obj, GetReadyCondition(obj))
-
-	// Some values are generated randomly, so pick up them.
-	want.SetName(got.GetName())
-	want.SetUID(got.GetUID())
-
-	g.Expect(got).To(Equal(want))
-}
-
-func Test_updateGroupNode(t *testing.T) {
-	now := metav1.Now()
-	beforeNow := metav1.Time{Time: now.Time.Add(-1 * time.Hour)}
-
-	group := &unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": "virtual.cluster.x-k8s.io/v1alpha4",
-			"kind":       "MachineGroup",
-			"metadata": map[string]interface{}{
-				"namespace": "ns",
-				"name":      "random-name",
-				"annotations": map[string]interface{}{
-					VirtualObjectAnnotation: "True",
-					GroupObjectAnnotation:   "True",
-					GroupItemsAnnotation:    "my-machine, sibling-machine",
-				},
-				"uid": "random-uid",
-			},
-			"status": map[string]interface{}{
-				"conditions": []interface{}{
-					map[string]interface{}{
-						"status":             "",
-						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
-						"type":               "Ready",
-					},
-				},
-			},
-		},
-	}
-
-	obj := &clusterv1.Machine{
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Machine",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      "another-machine",
-		},
-		Status: clusterv1.MachineStatus{
-			Conditions: clusterv1.Conditions{
-				clusterv1.Condition{Type: clusterv1.ReadyCondition, LastTransitionTime: now},
-			},
-		},
-	}
-
-	want := &unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": "virtual.cluster.x-k8s.io/v1alpha4",
-			"kind":       "MachineGroup",
-			"metadata": map[string]interface{}{
-				"namespace": "ns",
-				"name":      "random-name",
-				"annotations": map[string]interface{}{
-					VirtualObjectAnnotation: "True",
-					GroupObjectAnnotation:   "True",
-					GroupItemsAnnotation:    "another-machine, my-machine, sibling-machine",
-				},
-				"uid": "random-uid",
-			},
-			"status": map[string]interface{}{
-				"conditions": []interface{}{
-					map[string]interface{}{
-						"status":             "",
-						"lastTransitionTime": beforeNow.Time.UTC().Format(time.RFC3339),
-						"type":               "Ready",
-					},
-				},
-			},
-		},
-	}
-
-	g := NewWithT(t)
-	updateGroupNode(group, GetReadyCondition(group), obj, GetReadyCondition(obj))
-
-	g.Expect(group).To(Equal(want))
-}
-
-func Test_Add_setsShowObjectConditionsAnnotation(t *testing.T) {
-	parent := fakeCluster("parent")
-	obj := fakeMachine("my-machine")
-
-	type args struct {
-		treeOptions ObjectTreeOptions
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "filter selecting my machine should not add the annotation",
-			args: args{
-				treeOptions: ObjectTreeOptions{ShowOtherConditions: "all"},
-			},
-			want: true,
-		},
-		{
-			name: "filter not selecting my machine should not add the annotation",
-			args: args{
-				treeOptions: ObjectTreeOptions{ShowOtherConditions: ""},
-			},
-			want: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			root := parent.DeepCopy()
-			tree := NewObjectTree(root, tt.args.treeOptions)
-
-			g := NewWithT(t)
-			getAdded, gotVisible := tree.Add(root, obj.DeepCopy())
-			g.Expect(getAdded).To(BeTrue())
-			g.Expect(gotVisible).To(BeTrue())
-
-			gotObj := tree.GetObject("my-machine")
-			g.Expect(gotObj).ToNot(BeNil())
-			switch tt.want {
-			case true:
-				g.Expect(gotObj.GetAnnotations()).To(HaveKey(ShowObjectConditionsAnnotation))
-				g.Expect(gotObj.GetAnnotations()[ShowObjectConditionsAnnotation]).To(Equal("True"))
-			case false:
-				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(ShowObjectConditionsAnnotation))
-			}
-		})
-	}
-}
-
-func Test_Add_setsGroupingObjectAnnotation(t *testing.T) {
-	parent := fakeCluster("parent")
-	obj := fakeMachine("my-machine")
-
-	type args struct {
-		treeOptions ObjectTreeOptions
-		addOptions  []AddObjectOption
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "should not add the annotation if not requested to",
-			args: args{
-				treeOptions: ObjectTreeOptions{},
-				addOptions:  nil, // without GroupingObject option
-			},
-			want: false,
-		},
-		{
-			name: "should add the annotation if requested to",
-			args: args{
-				treeOptions: ObjectTreeOptions{},
-				addOptions:  []AddObjectOption{GroupingObject(true)},
-			},
-			want: true,
-		},
-		{
-			name: "should not add the annotation if requested to, but grouping is disabled",
-			args: args{
-				treeOptions: ObjectTreeOptions{DisableGrouping: true},
-				addOptions:  []AddObjectOption{GroupingObject(true)},
-			},
-			want: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			root := parent.DeepCopy()
-			tree := NewObjectTree(root, tt.args.treeOptions)
-
-			g := NewWithT(t)
-			getAdded, gotVisible := tree.Add(root, obj.DeepCopy(), tt.args.addOptions...)
-			g.Expect(getAdded).To(BeTrue())
-			g.Expect(gotVisible).To(BeTrue())
-
-			gotObj := tree.GetObject("my-machine")
-			g.Expect(gotObj).ToNot(BeNil())
-			switch tt.want {
-			case true:
-				g.Expect(gotObj.GetAnnotations()).To(HaveKey(GroupingObjectAnnotation))
-				g.Expect(gotObj.GetAnnotations()[GroupingObjectAnnotation]).To(Equal("True"))
-			case false:
-				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(GroupingObjectAnnotation))
-			}
-		})
-	}
-}
-
-func Test_Add_setsObjectMetaNameAnnotation(t *testing.T) {
-	parent := fakeCluster("parent")
-	obj := fakeMachine("my-machine")
-
-	type args struct {
-		addOptions []AddObjectOption
-	}
-	tests := []struct {
-		name string
-		args args
-		want bool
-	}{
-		{
-			name: "should not add the annotation if not requested to",
-			args: args{
-				addOptions: nil, // without ObjectMetaName option
-			},
-			want: false,
-		},
-		{
-			name: "should add the annotation if requested to",
-			args: args{
-				addOptions: []AddObjectOption{ObjectMetaName("MetaName")},
-			},
-			want: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			root := parent.DeepCopy()
-			tree := NewObjectTree(root, ObjectTreeOptions{})
-
-			g := NewWithT(t)
-			getAdded, gotVisible := tree.Add(root, obj.DeepCopy(), tt.args.addOptions...)
-			g.Expect(getAdded).To(BeTrue())
-			g.Expect(gotVisible).To(BeTrue())
-
-			gotObj := tree.GetObject("my-machine")
-			g.Expect(gotObj).ToNot(BeNil())
-			switch tt.want {
-			case true:
-				g.Expect(gotObj.GetAnnotations()).To(HaveKey(ObjectMetaNameAnnotation))
-				g.Expect(gotObj.GetAnnotations()[ObjectMetaNameAnnotation]).To(Equal("MetaName"))
-			case false:
-				g.Expect(gotObj.GetAnnotations()).ToNot(HaveKey(ObjectMetaNameAnnotation))
-			}
-		})
-	}
-}
-
-func Test_Add_NoEcho(t *testing.T) {
-	parent := fakeCluster("parent",
-		withClusterCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-	)
-
-	type args struct {
-		treeOptions ObjectTreeOptions
-		addOptions  []AddObjectOption
-		obj         *clusterv1.Machine
-	}
-	tests := []struct {
-		name     string
-		args     args
-		wantNode bool
-	}{
-		{
-			name: "should always add if NoEcho option is not present",
-			args: args{
-				treeOptions: ObjectTreeOptions{},
-				addOptions:  nil,
-				obj: fakeMachine("my-machine",
-					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-				),
-			},
-			wantNode: true,
-		},
-		{
-			name: "should not add if NoEcho option is present and objects have same ReadyCondition",
-			args: args{
-				treeOptions: ObjectTreeOptions{},
-				addOptions:  []AddObjectOption{NoEcho(true)},
-				obj: fakeMachine("my-machine",
-					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-				),
-			},
-			wantNode: false,
-		},
-		{
-			name: "should add if NoEcho option is present but objects have not same ReadyCondition",
-			args: args{
-				treeOptions: ObjectTreeOptions{},
-				addOptions:  []AddObjectOption{NoEcho(true)},
-				obj: fakeMachine("my-machine",
-					withMachineCondition(conditions.FalseCondition(clusterv1.ReadyCondition, "", clusterv1.ConditionSeverityInfo, "")),
-				),
-			},
-			wantNode: true,
-		},
-		{
-			name: "should add if NoEcho option is present, objects have same ReadyCondition, but NoEcho is disabled",
-			args: args{
-				treeOptions: ObjectTreeOptions{DisableNoEcho: true},
-				addOptions:  []AddObjectOption{NoEcho(true)},
-				obj: fakeMachine("my-machine",
-					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-				),
-			},
-			wantNode: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			root := parent.DeepCopy()
-			tree := NewObjectTree(root, tt.args.treeOptions)
-
-			g := NewWithT(t)
-			getAdded, gotVisible := tree.Add(root, tt.args.obj, tt.args.addOptions...)
-			g.Expect(getAdded).To(Equal(tt.wantNode))
-			g.Expect(gotVisible).To(Equal(tt.wantNode))
-
-			gotObj := tree.GetObject("my-machine")
-			switch tt.wantNode {
-			case true:
-				g.Expect(gotObj).ToNot(BeNil())
-			case false:
-				g.Expect(gotObj).To(BeNil())
-			}
-		})
-	}
-}
-
-func Test_Add_Grouping(t *testing.T) {
-	parent := fakeCluster("parent",
-		withClusterAnnotation(GroupingObjectAnnotation, "True"),
-	)
-
-	type args struct {
-		addOptions []AddObjectOption
-		siblings   []*clusterv1.Machine
-		obj        *clusterv1.Machine
-	}
-	tests := []struct {
-		name            string
-		args            args
-		wantNodesPrefix []string
-		wantVisible     bool
-		wantItems       string
-	}{
-		{
-			name: "should never group the first child object",
-			args: args{
-				obj: fakeMachine("my-machine"),
-			},
-			wantNodesPrefix: []string{"my-machine"},
-			wantVisible:     true,
-		},
-		{
-			name: "should group child node if it has same conditions of an existing one",
-			args: args{
-				siblings: []*clusterv1.Machine{
-					fakeMachine("first-machine",
-						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-					),
-				},
-				obj: fakeMachine("second-machine",
-					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-				),
-			},
-			wantNodesPrefix: []string{"zz_True"},
-			wantVisible:     false,
-			wantItems:       "first-machine, second-machine",
-		},
-		{
-			name: "should group child node if it has same conditions of an existing group",
-			args: args{
-				siblings: []*clusterv1.Machine{
-					fakeMachine("first-machine",
-						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-					),
-					fakeMachine("second-machine",
-						withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-					),
-				},
-				obj: fakeMachine("third-machine",
-					withMachineCondition(conditions.TrueCondition(clusterv1.ReadyCondition)),
-				),
-			},
-			wantNodesPrefix: []string{"zz_True"},
-			wantVisible:     false,
-			wantItems:       "first-machine, second-machine, third-machine",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			root := parent.DeepCopy()
-			tree := NewObjectTree(root, ObjectTreeOptions{})
-
-			for i := range tt.args.siblings {
-				tree.Add(parent, tt.args.siblings[i], tt.args.addOptions...)
-			}
-
-			g := NewWithT(t)
-			getAdded, gotVisible := tree.Add(root, tt.args.obj, tt.args.addOptions...)
-			g.Expect(getAdded).To(BeTrue())
-			g.Expect(gotVisible).To(Equal(tt.wantVisible))
-
-			gotObjs := tree.GetObjectsByParent("parent")
-			g.Expect(gotObjs).To(HaveLen(len(tt.wantNodesPrefix)))
-			for _, obj := range gotObjs {
-				found := false
-				for _, prefix := range tt.wantNodesPrefix {
-					if strings.HasPrefix(obj.GetName(), prefix) {
-						found = true
-						break
-					}
-				}
-				g.Expect(found).To(BeTrue(), "Found object with name %q, waiting for one of %s", obj.GetName(), tt.wantNodesPrefix)
-
-				if strings.HasPrefix(obj.GetName(), "zz_") {
-					g.Expect(GetGroupItems(obj)).To(Equal(tt.wantItems))
-				}
-			}
-		})
-	}
-}
-
-type clusterOption func(*clusterv1.Cluster)
-
-func fakeCluster(name string, options ...clusterOption) *clusterv1.Cluster { // nolint:unparam
-	c := &clusterv1.Cluster{
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Cluster",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      name,
-			UID:       types.UID(name),
-		},
-	}
-	for _, opt := range options {
-		opt(c)
-	}
-	return c
-}
-
-func withClusterAnnotation(name, value string) func(*clusterv1.Cluster) {
-	return func(c *clusterv1.Cluster) {
-		if c.Annotations == nil {
-			c.Annotations = map[string]string{}
-		}
-		c.Annotations[name] = value
-	}
-}
-
-func withClusterCondition(c *clusterv1.Condition) func(*clusterv1.Cluster) {
-	return func(m *clusterv1.Cluster) {
-		conditions.Set(m, c)
-	}
-}
-
-type machineOption func(*clusterv1.Machine)
-
-func fakeMachine(name string, options ...machineOption) *clusterv1.Machine {
-	m := &clusterv1.Machine{
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Machine",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      name,
-			UID:       types.UID(name),
-		},
-	}
-	for _, opt := range options {
-		opt(m)
-	}
-	return m
-}
-
-func withMachineCondition(c *clusterv1.Condition) func(*clusterv1.Machine) {
-	return func(m *clusterv1.Machine) {
-		conditions.Set(m, c)
-	}
-}
diff --git a/cmd/clusterctl/cmd/describe_cluster_test.go b/cmd/clusterctl/cmd/describe_cluster_test.go
deleted file mode 100644
index 39ea06e46..000000000
--- a/cmd/clusterctl/cmd/describe_cluster_test.go
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
-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 cmd
-
-import (
-	"fmt"
-	"strings"
-	"testing"
-
-	"github.com/gosuri/uitable"
-	. "github.com/onsi/gomega"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/cluster-api/util/conditions"
-
-	"github.com/fatih/color"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3"
-	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/tree"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-)
-
-func Test_getRowName(t *testing.T) {
-	tests := []struct {
-		name   string
-		object controllerutil.Object
-		expect string
-	}{
-		{
-			name:   "Row name for objects should be kind/name",
-			object: fakeObject("c1"),
-			expect: "Object/c1",
-		},
-		{
-			name:   "Row name for a deleting object should have deleted prefix",
-			object: fakeObject("c1", withDeletionTimestamp),
-			expect: "!! DELETED !! Object/c1",
-		},
-		{
-			name:   "Row name for objects with meta name should be meta-name - kind/name",
-			object: fakeObject("c1", withAnnotation(tree.ObjectMetaNameAnnotation, "MetaName")),
-			expect: "MetaName - Object/c1",
-		},
-		{
-			name:   "Row name for virtual objects should be name",
-			object: fakeObject("c1", withAnnotation(tree.VirtualObjectAnnotation, "True")),
-			expect: "c1",
-		},
-		{
-			name: "Row name for group objects should be #-of-items kind",
-			object: fakeObject("c1",
-				withAnnotation(tree.VirtualObjectAnnotation, "True"),
-				withAnnotation(tree.GroupObjectAnnotation, "True"),
-				withAnnotation(tree.GroupItemsAnnotation, "c1, c2, c3"),
-			),
-			expect: "3 Objects...",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-			got := getRowName(tt.object)
-			g.Expect(got).To(Equal(tt.expect))
-		})
-	}
-}
-
-func Test_newConditionDescriptor_readyColor(t *testing.T) {
-	tests := []struct {
-		name             string
-		condition        *clusterv1.Condition
-		expectReadyColor *color.Color
-	}{
-		{
-			name:             "True condition should be green",
-			condition:        conditions.TrueCondition("C"),
-			expectReadyColor: green,
-		},
-		{
-			name:             "Unknown condition should be white",
-			condition:        conditions.UnknownCondition("C", "", ""),
-			expectReadyColor: white,
-		},
-		{
-			name:             "False condition, severity error should be red",
-			condition:        conditions.FalseCondition("C", "", clusterv1.ConditionSeverityError, ""),
-			expectReadyColor: red,
-		},
-		{
-			name:             "False condition, severity warning should be yellow",
-			condition:        conditions.FalseCondition("C", "", clusterv1.ConditionSeverityWarning, ""),
-			expectReadyColor: yellow,
-		},
-		{
-			name:             "False condition, severity info should be white",
-			condition:        conditions.FalseCondition("C", "", clusterv1.ConditionSeverityInfo, ""),
-			expectReadyColor: white,
-		},
-		{
-			name:             "Condition without status should be gray",
-			condition:        &clusterv1.Condition{},
-			expectReadyColor: gray,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-			got := newConditionDescriptor(tt.condition)
-			g.Expect(got.readyColor).To(Equal(tt.expectReadyColor))
-		})
-	}
-}
-
-func Test_newConditionDescriptor_truncateMessages(t *testing.T) {
-	tests := []struct {
-		name          string
-		condition     *clusterv1.Condition
-		expectMessage string
-	}{
-		{
-			name:          "Short messages are not changed",
-			condition:     conditions.UnknownCondition("C", "", "short message"),
-			expectMessage: "short message",
-		},
-		{
-			name:          "Long message are truncated",
-			condition:     conditions.UnknownCondition("C", "", strings.Repeat("s", 150)),
-			expectMessage: fmt.Sprintf("%s ...", strings.Repeat("s", 100)),
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-			got := newConditionDescriptor(tt.condition)
-			g.Expect(got.message).To(Equal(tt.expectMessage))
-		})
-	}
-}
-
-func Test_TreePrefix(t *testing.T) {
-	tests := []struct {
-		name         string
-		objectTree   *tree.ObjectTree
-		expectPrefix []string
-	}{
-		{
-			name: "First level child should get the right prefix",
-			objectTree: func() *tree.ObjectTree {
-				root := fakeObject("root")
-				obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{})
-
-				o1 := fakeObject("child1")
-				o2 := fakeObject("child2")
-				obectjTree.Add(root, o1)
-				obectjTree.Add(root, o2)
-				return obectjTree
-			}(),
-			expectPrefix: []string{
-				"Object/root",
-				"├─Object/child1", // first objects gets ├─
-				"└─Object/child2", // last objects gets └─
-			},
-		},
-		{
-			name: "Second level child should get the right prefix",
-			objectTree: func() *tree.ObjectTree {
-				root := fakeObject("root")
-				obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{})
-
-				o1 := fakeObject("child1")
-				o1_1 := fakeObject("child1.1")
-				o1_2 := fakeObject("child1.2")
-				o2 := fakeObject("child2")
-				o2_1 := fakeObject("child2.1")
-				o2_2 := fakeObject("child2.2")
-
-				obectjTree.Add(root, o1)
-				obectjTree.Add(o1, o1_1)
-				obectjTree.Add(o1, o1_2)
-				obectjTree.Add(root, o2)
-				obectjTree.Add(o2, o2_1)
-				obectjTree.Add(o2, o2_2)
-				return obectjTree
-			}(),
-			expectPrefix: []string{
-				"Object/root",
-				"├─Object/child1",
-				"│ ├─Object/child1.1", // first second level child gets pipes and ├─
-				"│ └─Object/child1.2", // last second level child gets pipes and └─
-				"└─Object/child2",
-				"  ├─Object/child2.1", // first second level child spaces and ├─
-				"  └─Object/child2.2", // last second level child gets spaces and └─
-			},
-		},
-		{
-			name: "Conditions should get the right prefix",
-			objectTree: func() *tree.ObjectTree {
-				root := fakeObject("root")
-				obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{})
-
-				o1 := fakeObject("child1",
-					withAnnotation(tree.ShowObjectConditionsAnnotation, "True"),
-					withCondition(conditions.TrueCondition("C1.1")),
-					withCondition(conditions.TrueCondition("C1.2")),
-				)
-				o2 := fakeObject("child2",
-					withAnnotation(tree.ShowObjectConditionsAnnotation, "True"),
-					withCondition(conditions.TrueCondition("C2.1")),
-					withCondition(conditions.TrueCondition("C2.2")),
-				)
-				obectjTree.Add(root, o1)
-				obectjTree.Add(root, o2)
-				return obectjTree
-			}(),
-			expectPrefix: []string{
-				"Object/root",
-				"├─Object/child1",
-				"│             ├─C1.1", // first condition child gets pipes and ├─
-				"│             └─C1.2", // last condition child gets └─ and pipes and └─
-				"└─Object/child2",
-				"              ├─C2.1", // first condition child gets spaces and ├─
-				"              └─C2.2", // last condition child gets spaces and └─
-			},
-		},
-		{
-			name: "Conditions should get the right prefix if the object has a child",
-			objectTree: func() *tree.ObjectTree {
-				root := fakeObject("root")
-				obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{})
-
-				o1 := fakeObject("child1",
-					withAnnotation(tree.ShowObjectConditionsAnnotation, "True"),
-					withCondition(conditions.TrueCondition("C1.1")),
-					withCondition(conditions.TrueCondition("C1.2")),
-				)
-				o1_1 := fakeObject("child1.1")
-
-				o2 := fakeObject("child2",
-					withAnnotation(tree.ShowObjectConditionsAnnotation, "True"),
-					withCondition(conditions.TrueCondition("C2.1")),
-					withCondition(conditions.TrueCondition("C2.2")),
-				)
-				o2_1 := fakeObject("child2.1")
-				obectjTree.Add(root, o1)
-				obectjTree.Add(o1, o1_1)
-				obectjTree.Add(root, o2)
-				obectjTree.Add(o2, o2_1)
-				return obectjTree
-			}(),
-			expectPrefix: []string{
-				"Object/root",
-				"├─Object/child1",
-				"│ │           ├─C1.1", // first condition child gets pipes, children pipe and ├─
-				"│ │           └─C1.2", // last condition child gets pipes, children pipe and └─
-				"│ └─Object/child1.1",
-				"└─Object/child2",
-				"  │           ├─C2.1", // first condition child gets spaces, children pipe and ├─
-				"  │           └─C2.2", // last condition child gets spaces, children pipe and └─
-				"  └─Object/child2.1",
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			g := NewWithT(t)
-
-			// Creates the output table
-			tbl := uitable.New()
-
-			// Add row for the root object, the cluster, and recursively for all the nodes representing the cluster status.
-			addObjectRow("", tbl, tt.objectTree, tt.objectTree.GetRoot())
-
-			for i := range tt.expectPrefix {
-				g.Expect(tbl.Rows[i].Cells[0].String()).To(Equal(tt.expectPrefix[i]))
-			}
-
-		})
-	}
-}
-
-type objectOption func(object controllerutil.Object)
-
-func fakeObject(name string, options ...objectOption) controllerutil.Object {
-	c := &clusterv1.Cluster{ // suing type cluster for simplicity, but this could be any object
-		TypeMeta: metav1.TypeMeta{
-			Kind: "Object",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: "ns",
-			Name:      name,
-			UID:       types.UID(name),
-		},
-	}
-	for _, opt := range options {
-		opt(c)
-	}
-	return c
-}
-
-func withAnnotation(name, value string) func(controllerutil.Object) {
-	return func(c controllerutil.Object) {
-		if c.GetAnnotations() == nil {
-			c.SetAnnotations(map[string]string{})
-		}
-		a := c.GetAnnotations()
-		a[name] = value
-		c.SetAnnotations(a)
-	}
-}
-
-func withCondition(c *clusterv1.Condition) func(controllerutil.Object) {
-	return func(m controllerutil.Object) {
-		setter := m.(conditions.Setter)
-		conditions.Set(setter, c)
-	}
-}
-
-func withDeletionTimestamp(object controllerutil.Object) {
-	now := metav1.Now()
-	object.SetDeletionTimestamp(&now)
-}
diff --git a/test/framework/kubetest/run.go b/test/framework/kubetest/run.go
index 880a349bc..f1bd5284e 100644
--- a/test/framework/kubetest/run.go
+++ b/test/framework/kubetest/run.go
@@ -62,6 +62,9 @@ type RunInput struct {
 	KubernetesVersion string
 	// ConformanceImage is an optional field to specify an exact conformance image
 	ConformanceImage string
+	// KubeTestRepoListPath is optional file for specifying custom image repositories
+	// https://github.com/kubernetes/kubernetes/blob/master/test/images/README.md#testing-the-new-image
+	KubeTestRepoListPath string
 }
 
 // Run executes kube-test given an artifact directory, and sets settings
@@ -117,6 +120,14 @@ func Run(input RunInput) error {
 		return err
 	}
 
+	var testRepoListVolumeArgs []string
+	if input.KubeTestRepoListPath != "" {
+		testRepoListVolumeArgs, err = buildKubeTestRepoListArgs(kubetestConfigDir, input.KubeTestRepoListPath)
+		if err != nil {
+			return err
+		}
+	}
+
 	e2eVars := map[string]string{
 		"kubeconfig":           "/tmp/kubeconfig",
 		"provider":             "skeleton",
@@ -140,7 +151,11 @@ func Run(input RunInput) error {
 		return errors.Wrap(err, "unable to determine current user")
 	}
 	userArg := user.Uid + ":" + user.Gid
-	e2eCmd := exec.Command("docker", "run", "--user", userArg, kubeConfigVolumeMount, outputVolumeMount, viperVolumeMount, "-t", input.ConformanceImage)
+	e2eCmd := exec.Command("docker", "run", "--user", userArg, kubeConfigVolumeMount, outputVolumeMount, viperVolumeMount, "-t")
+	if len(testRepoListVolumeArgs) > 0 {
+		e2eCmd.Args = append(e2eCmd.Args, testRepoListVolumeArgs...)
+	}
+	e2eCmd.Args = append(e2eCmd.Args, input.ConformanceImage)
 	e2eCmd.Args = append(e2eCmd.Args, "/usr/local/bin/ginkgo")
 	e2eCmd.Args = append(e2eCmd.Args, ginkgoArgs...)
 	e2eCmd.Args = append(e2eCmd.Args, "/usr/local/bin/e2e.test")
@@ -218,6 +233,11 @@ func volumeArg(src, dest string) string {
 	return volumeArg
 }
 
+func envArg(key, value string) string {
+	envArg := "-e" + key + "=" + value
+	return envArg
+}
+
 func versionToConformanceImage(kubernetesVersion string) string {
 	k8sVersion := strings.ReplaceAll(kubernetesVersion, "+", "_")
 	if isUsingCIArtifactsVersion(kubernetesVersion) {
@@ -236,3 +256,16 @@ func buildArgs(kv map[string]string, flagMarker string) []string {
 	}
 	return args
 }
+
+func buildKubeTestRepoListArgs(kubetestConfigDir, kubeTestRepoListPath string) ([]string, error) {
+	args := make([]string, 2)
+
+	tmpKubeTestRepoListPath := path.Join(kubetestConfigDir, "repo_list.yaml")
+	if err := copyFile(kubeTestRepoListPath, tmpKubeTestRepoListPath); err != nil {
+		return nil, err
+	}
+	dest := "/tmp/repo_list.yaml"
+	args[0] = envArg("KUBE_TEST_REPO_LIST", dest)
+	args[1] = volumeArg(tmpKubeTestRepoListPath, dest)
+	return args, nil
+}
diff --git a/test/infrastructure/docker/go.sum b/test/infrastructure/docker/go.sum
index e01117ae5..213bfd6ad 100644
--- a/test/infrastructure/docker/go.sum
+++ b/test/infrastructure/docker/go.sum
@@ -210,7 +210,6 @@ github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEo
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=