Repository: rancher/fleet
Pull request #1676 (2023-07-26 09:36:09)

Return early from clusterregistration handler
Refers to #1651 

The clusterregistration.OnChange handler would create objects before the secret token was generated. It's not necessary to create the objects for requests that are not granted.

This also removes the deep nesting from the little state machine of if statements and makes the outcome more explicit.



Patch:
diff --git a/.golangci.json b/.golangci.json
index 6455fbc9..c0523554 100644
--- a/.golangci.json
+++ b/.golangci.json
@@ -46,10 +46,6 @@
   },
   "issues": {
     "exclude-rules": [
-      {
-        "path": "pkg/controllers/clusterregistration/controller.go",
-        "text": "G101: Potential hardcoded credentials"
-      },
       {
         "path": "cmd/fleetcontroller/main.go",
         "text": "G108: Profiling endpoint is automatically exposed on /debug/pprof"
@@ -59,11 +55,11 @@
         "text": "G108: Profiling endpoint is automatically exposed on /debug/pprof"
       },
       {
-        "path": "modules/agent/pkg/register/register.go",
+        "path": "internal/cmd/agent/register/register.go",
         "text": "G107: Potential HTTP request made with variable url"
       },
       {
-        "path": "modules/agent/pkg/deployer/internal/",
+        "path": "internal/cmd/agent/deployer/internal/",
         "linters": [
           "revive"
         ]
diff --git a/internal/cmd/controller/controllers/clusterregistration/controller.go b/internal/cmd/controller/controllers/clusterregistration/controller.go
index c3308add..63e25fcc 100644
--- a/internal/cmd/controller/controllers/clusterregistration/controller.go
+++ b/internal/cmd/controller/controllers/clusterregistration/controller.go
@@ -35,7 +35,7 @@ import (
 )
 
 const (
-	AgentCredentialSecretType     = "fleet.cattle.io/agent-credential"
+	AgentCredentialSecretType     = "fleet.cattle.io/agent-credential" // nolint:gosec // not a credential
 	clusterByClientID             = "clusterByClientID"
 	clusterRegistrationByClientID = "clusterRegistrationByClientID"
 	deleteSecretAfter             = durations.ClusterRegistrationDeleteDelay
@@ -159,10 +159,6 @@ func (h *handler) OnSecretChange(key string, secret *v1.Secret) (*v1.Secret, err
 // It can also get content resources, but not list them. The name of content
 // resources is random.
 func (h *handler) OnChange(request *fleet.ClusterRegistration, status fleet.ClusterRegistrationStatus) ([]runtime.Object, fleet.ClusterRegistrationStatus, error) {
-	var (
-		objects []runtime.Object
-	)
-
 	if status.Granted {
 		// only create the cluster for the request once
 		return nil, status, generic.ErrSkip
@@ -180,51 +176,48 @@ func (h *handler) OnChange(request *fleet.ClusterRegistration, status fleet.Clus
 
 	saName := name.SafeConcatName(request.Name, string(request.UID))
 	sa, err := h.serviceAccountCache.Get(cluster.Status.Namespace, saName)
-	if err == nil {
-		if secret, err := h.authorizeCluster(sa, cluster, request); err != nil {
-			return nil, status, fmt.Errorf("failed to authorize cluster: %w", err)
-		} else if secret != nil {
-			status.Granted = true
-			objects = append(objects, secret)
-		}
+	if err != nil && apierrors.IsNotFound(err) {
+		// create request service account if missing
+		status.ClusterName = cluster.Name
+		return []runtime.Object{requestSA(saName, cluster, request)}, status, nil
+	} else if err != nil {
+		return nil, status, fmt.Errorf("failed to retrieve service account from cache: %w", err)
 	}
 
-	logrus.Infof("Cluster registration request '%s/%s', cluster '%s/%s' granted [%v], creating cluster and request service account",
-		request.Namespace, request.Name, cluster.Namespace, cluster.Name, status.Granted)
+	// try to get request service account's token
+	var secret *v1.Secret
+	if secret, err = h.authorizeCluster(sa, cluster, request); err != nil {
+		return nil, status, fmt.Errorf("failed to authorize cluster, cannot get service account token: %w", err)
+	} else if secret == nil {
+		status.ClusterName = cluster.Name
+		logrus.Infof("Cluster registration request '%s/%s', cluster '%s/%s' not granted, waiting for service account token",
+			request.Namespace, request.Name, cluster.Namespace, cluster.Name)
+		return nil, status, nil
+	}
 
-	if status.Granted {
-		logrus.Debugf("Cluster registration request '%s/%s' granted, creating registration secret", request.Namespace, request.Name)
-
-		crlist, _ := h.clusterRegistration.List(request.Namespace, metav1.ListOptions{})
-		for _, creg := range crlist.Items {
-			if shouldDelete(creg, *request) {
-				logrus.Infof("Deleting old clusterregistration '%s/%s', now at '%s'", creg.Namespace, creg.Name, request.Name)
-				if err := h.clusterRegistration.Delete(creg.Namespace, creg.Name, nil); err != nil && !apierrors.IsNotFound(err) {
-					return nil, status, err
-				}
+	// delete old cluster registrations
+	crlist, _ := h.clusterRegistration.List(request.Namespace, metav1.ListOptions{})
+	for _, creg := range crlist.Items {
+		if shouldDelete(creg, *request) {
+			logrus.Debugf("Deleting old clusterregistration '%s/%s', now at '%s'", creg.Namespace, creg.Name, request.Name)
+			if err := h.clusterRegistration.Delete(creg.Namespace, creg.Name, nil); err != nil && !apierrors.IsNotFound(err) {
+				return nil, status, err
 			}
 		}
 	}
 
+	// request is granted, create the registration secret and roles
 	status.ClusterName = cluster.Name
+	status.Granted = true
+
+	logrus.Infof("Cluster registration request '%s/%s' granted, creating cluster, request service account, registration secret", request.Namespace, request.Name)
 
-	return append(objects,
+	return []runtime.Object{
+		// the registration secret c-clientID-clientRandom
+		secret,
 		// Update the existing service account 'request-UID' in the
 		// cluster namespace, e.g. 'cluster-fleet-default-NAME-ID'
-		&v1.ServiceAccount{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      saName,
-				Namespace: cluster.Status.Namespace,
-				Labels: map[string]string{
-					fleet.ManagedLabel: "true",
-				},
-				Annotations: map[string]string{
-					fleet.ClusterAnnotation:                      cluster.Name,
-					fleet.ClusterRegistrationAnnotation:          request.Name,
-					fleet.ClusterRegistrationNamespaceAnnotation: request.Namespace,
-				},
-			},
-		},
+		requestSA(saName, cluster, request),
 		// Add role bindings to manage bundledeployments and contents,
 		// the agent could previously only access secrets in
 		// 'cattle-fleet-clusters-system' and clusterregistrations in
@@ -313,7 +306,7 @@ func (h *handler) OnChange(request *fleet.ClusterRegistration, status fleet.Clus
 				Name:     "fleet-content",
 			},
 		},
-	), status, nil
+	}, status, nil
 }
 
 // shouldDelete returns true for any other cluster registration with the same clientID, but different random and older creation timestamp
@@ -405,3 +398,20 @@ func (h *handler) authorizeCluster(sa *v1.ServiceAccount, cluster *fleet.Cluster
 		},
 	}, nil
 }
+
+func requestSA(saName string, cluster *fleet.Cluster, request *fleet.ClusterRegistration) *v1.ServiceAccount {
+	return &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      saName,
+			Namespace: cluster.Status.Namespace,
+			Labels: map[string]string{
+				fleet.ManagedLabel: "true",
+			},
+			Annotations: map[string]string{
+				fleet.ClusterAnnotation:                      cluster.Name,
+				fleet.ClusterRegistrationAnnotation:          request.Name,
+				fleet.ClusterRegistrationNamespaceAnnotation: request.Namespace,
+			},
+		},
+	}
+}
diff --git a/internal/cmd/controller/mocks/cluster_cache_mock.go b/internal/cmd/controller/mocks/cluster_cache_mock.go
new file mode 100644
index 00000000..b0b94dc2
--- /dev/null
+++ b/internal/cmd/controller/mocks/cluster_cache_mock.go
@@ -0,0 +1,94 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 (interfaces: ClusterCache)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	v1alpha10 "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
+	labels "k8s.io/apimachinery/pkg/labels"
+)
+
+// MockClusterCache is a mock of ClusterCache interface.
+type MockClusterCache struct {
+	ctrl     *gomock.Controller
+	recorder *MockClusterCacheMockRecorder
+}
+
+// MockClusterCacheMockRecorder is the mock recorder for MockClusterCache.
+type MockClusterCacheMockRecorder struct {
+	mock *MockClusterCache
+}
+
+// NewMockClusterCache creates a new mock instance.
+func NewMockClusterCache(ctrl *gomock.Controller) *MockClusterCache {
+	mock := &MockClusterCache{ctrl: ctrl}
+	mock.recorder = &MockClusterCacheMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClusterCache) EXPECT() *MockClusterCacheMockRecorder {
+	return m.recorder
+}
+
+// AddIndexer mocks base method.
+func (m *MockClusterCache) AddIndexer(arg0 string, arg1 v1alpha10.ClusterIndexer) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddIndexer", arg0, arg1)
+}
+
+// AddIndexer indicates an expected call of AddIndexer.
+func (mr *MockClusterCacheMockRecorder) AddIndexer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockClusterCache)(nil).AddIndexer), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockClusterCache) Get(arg0, arg1 string) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockClusterCacheMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClusterCache)(nil).Get), arg0, arg1)
+}
+
+// GetByIndex mocks base method.
+func (m *MockClusterCache) GetByIndex(arg0, arg1 string) ([]*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetByIndex", arg0, arg1)
+	ret0, _ := ret[0].([]*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetByIndex indicates an expected call of GetByIndex.
+func (mr *MockClusterCacheMockRecorder) GetByIndex(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockClusterCache)(nil).GetByIndex), arg0, arg1)
+}
+
+// List mocks base method.
+func (m *MockClusterCache) List(arg0 string, arg1 labels.Selector) ([]*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].([]*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockClusterCacheMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClusterCache)(nil).List), arg0, arg1)
+}
diff --git a/internal/cmd/controller/mocks/cluster_client_mock.go b/internal/cmd/controller/mocks/cluster_client_mock.go
new file mode 100644
index 00000000..06c6ceed
--- /dev/null
+++ b/internal/cmd/controller/mocks/cluster_client_mock.go
@@ -0,0 +1,162 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 (interfaces: ClusterClient)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+)
+
+// MockClusterClient is a mock of ClusterClient interface.
+type MockClusterClient struct {
+	ctrl     *gomock.Controller
+	recorder *MockClusterClientMockRecorder
+}
+
+// MockClusterClientMockRecorder is the mock recorder for MockClusterClient.
+type MockClusterClientMockRecorder struct {
+	mock *MockClusterClient
+}
+
+// NewMockClusterClient creates a new mock instance.
+func NewMockClusterClient(ctrl *gomock.Controller) *MockClusterClient {
+	mock := &MockClusterClient{ctrl: ctrl}
+	mock.recorder = &MockClusterClientMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClusterClient) EXPECT() *MockClusterClientMockRecorder {
+	return m.recorder
+}
+
+// Create mocks base method.
+func (m *MockClusterClient) Create(arg0 *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockClusterClientMockRecorder) Create(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClusterClient)(nil).Create), arg0)
+}
+
+// Delete mocks base method.
+func (m *MockClusterClient) Delete(arg0, arg1 string, arg2 *v1.DeleteOptions) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockClusterClientMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClusterClient)(nil).Delete), arg0, arg1, arg2)
+}
+
+// Get mocks base method.
+func (m *MockClusterClient) Get(arg0, arg1 string, arg2 v1.GetOptions) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockClusterClientMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClusterClient)(nil).Get), arg0, arg1, arg2)
+}
+
+// List mocks base method.
+func (m *MockClusterClient) List(arg0 string, arg1 v1.ListOptions) (*v1alpha1.ClusterList, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].(*v1alpha1.ClusterList)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockClusterClientMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClusterClient)(nil).List), arg0, arg1)
+}
+
+// Patch mocks base method.
+func (m *MockClusterClient) Patch(arg0, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 ...string) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2, arg3}
+	for _, a := range arg4 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Patch", varargs...)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Patch indicates an expected call of Patch.
+func (mr *MockClusterClientMockRecorder) Patch(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClusterClient)(nil).Patch), varargs...)
+}
+
+// Update mocks base method.
+func (m *MockClusterClient) Update(arg0 *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Update", arg0)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockClusterClientMockRecorder) Update(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClusterClient)(nil).Update), arg0)
+}
+
+// UpdateStatus mocks base method.
+func (m *MockClusterClient) UpdateStatus(arg0 *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateStatus", arg0)
+	ret0, _ := ret[0].(*v1alpha1.Cluster)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateStatus indicates an expected call of UpdateStatus.
+func (mr *MockClusterClientMockRecorder) UpdateStatus(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockClusterClient)(nil).UpdateStatus), arg0)
+}
+
+// Watch mocks base method.
+func (m *MockClusterClient) Watch(arg0 string, arg1 v1.ListOptions) (watch.Interface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Watch", arg0, arg1)
+	ret0, _ := ret[0].(watch.Interface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Watch indicates an expected call of Watch.
+func (mr *MockClusterClientMockRecorder) Watch(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockClusterClient)(nil).Watch), arg0, arg1)
+}
diff --git a/internal/cmd/controller/mocks/cluster_registration_controller_mock.go b/internal/cmd/controller/mocks/cluster_registration_controller_mock.go
new file mode 100644
index 00000000..261453f5
--- /dev/null
+++ b/internal/cmd/controller/mocks/cluster_registration_controller_mock.go
@@ -0,0 +1,296 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 (interfaces: ClusterRegistrationController)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+	time "time"
+
+	gomock "github.com/golang/mock/gomock"
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	v1alpha10 "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
+	generic "github.com/rancher/wrangler/pkg/generic"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// MockClusterRegistrationController is a mock of ClusterRegistrationController interface.
+type MockClusterRegistrationController struct {
+	ctrl     *gomock.Controller
+	recorder *MockClusterRegistrationControllerMockRecorder
+}
+
+// MockClusterRegistrationControllerMockRecorder is the mock recorder for MockClusterRegistrationController.
+type MockClusterRegistrationControllerMockRecorder struct {
+	mock *MockClusterRegistrationController
+}
+
+// NewMockClusterRegistrationController creates a new mock instance.
+func NewMockClusterRegistrationController(ctrl *gomock.Controller) *MockClusterRegistrationController {
+	mock := &MockClusterRegistrationController{ctrl: ctrl}
+	mock.recorder = &MockClusterRegistrationControllerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockClusterRegistrationController) EXPECT() *MockClusterRegistrationControllerMockRecorder {
+	return m.recorder
+}
+
+// AddGenericHandler mocks base method.
+func (m *MockClusterRegistrationController) AddGenericHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericHandler", arg0, arg1, arg2)
+}
+
+// AddGenericHandler indicates an expected call of AddGenericHandler.
+func (mr *MockClusterRegistrationControllerMockRecorder) AddGenericHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockClusterRegistrationController)(nil).AddGenericHandler), arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler mocks base method.
+func (m *MockClusterRegistrationController) AddGenericRemoveHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericRemoveHandler", arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler.
+func (mr *MockClusterRegistrationControllerMockRecorder) AddGenericRemoveHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockClusterRegistrationController)(nil).AddGenericRemoveHandler), arg0, arg1, arg2)
+}
+
+// Cache mocks base method.
+func (m *MockClusterRegistrationController) Cache() v1alpha10.ClusterRegistrationCache {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Cache")
+	ret0, _ := ret[0].(v1alpha10.ClusterRegistrationCache)
+	return ret0
+}
+
+// Cache indicates an expected call of Cache.
+func (mr *MockClusterRegistrationControllerMockRecorder) Cache() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockClusterRegistrationController)(nil).Cache))
+}
+
+// Create mocks base method.
+func (m *MockClusterRegistrationController) Create(arg0 *v1alpha1.ClusterRegistration) (*v1alpha1.ClusterRegistration, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistration)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockClusterRegistrationControllerMockRecorder) Create(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClusterRegistrationController)(nil).Create), arg0)
+}
+
+// Delete mocks base method.
+func (m *MockClusterRegistrationController) Delete(arg0, arg1 string, arg2 *v1.DeleteOptions) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockClusterRegistrationControllerMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClusterRegistrationController)(nil).Delete), arg0, arg1, arg2)
+}
+
+// Enqueue mocks base method.
+func (m *MockClusterRegistrationController) Enqueue(arg0, arg1 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Enqueue", arg0, arg1)
+}
+
+// Enqueue indicates an expected call of Enqueue.
+func (mr *MockClusterRegistrationControllerMockRecorder) Enqueue(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockClusterRegistrationController)(nil).Enqueue), arg0, arg1)
+}
+
+// EnqueueAfter mocks base method.
+func (m *MockClusterRegistrationController) EnqueueAfter(arg0, arg1 string, arg2 time.Duration) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "EnqueueAfter", arg0, arg1, arg2)
+}
+
+// EnqueueAfter indicates an expected call of EnqueueAfter.
+func (mr *MockClusterRegistrationControllerMockRecorder) EnqueueAfter(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockClusterRegistrationController)(nil).EnqueueAfter), arg0, arg1, arg2)
+}
+
+// Get mocks base method.
+func (m *MockClusterRegistrationController) Get(arg0, arg1 string, arg2 v1.GetOptions) (*v1alpha1.ClusterRegistration, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistration)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockClusterRegistrationControllerMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClusterRegistrationController)(nil).Get), arg0, arg1, arg2)
+}
+
+// GroupVersionKind mocks base method.
+func (m *MockClusterRegistrationController) GroupVersionKind() schema.GroupVersionKind {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GroupVersionKind")
+	ret0, _ := ret[0].(schema.GroupVersionKind)
+	return ret0
+}
+
+// GroupVersionKind indicates an expected call of GroupVersionKind.
+func (mr *MockClusterRegistrationControllerMockRecorder) GroupVersionKind() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockClusterRegistrationController)(nil).GroupVersionKind))
+}
+
+// Informer mocks base method.
+func (m *MockClusterRegistrationController) Informer() cache.SharedIndexInformer {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Informer")
+	ret0, _ := ret[0].(cache.SharedIndexInformer)
+	return ret0
+}
+
+// Informer indicates an expected call of Informer.
+func (mr *MockClusterRegistrationControllerMockRecorder) Informer() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockClusterRegistrationController)(nil).Informer))
+}
+
+// List mocks base method.
+func (m *MockClusterRegistrationController) List(arg0 string, arg1 v1.ListOptions) (*v1alpha1.ClusterRegistrationList, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistrationList)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockClusterRegistrationControllerMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockClusterRegistrationController)(nil).List), arg0, arg1)
+}
+
+// OnChange mocks base method.
+func (m *MockClusterRegistrationController) OnChange(arg0 context.Context, arg1 string, arg2 v1alpha10.ClusterRegistrationHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnChange", arg0, arg1, arg2)
+}
+
+// OnChange indicates an expected call of OnChange.
+func (mr *MockClusterRegistrationControllerMockRecorder) OnChange(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnChange", reflect.TypeOf((*MockClusterRegistrationController)(nil).OnChange), arg0, arg1, arg2)
+}
+
+// OnRemove mocks base method.
+func (m *MockClusterRegistrationController) OnRemove(arg0 context.Context, arg1 string, arg2 v1alpha10.ClusterRegistrationHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnRemove", arg0, arg1, arg2)
+}
+
+// OnRemove indicates an expected call of OnRemove.
+func (mr *MockClusterRegistrationControllerMockRecorder) OnRemove(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRemove", reflect.TypeOf((*MockClusterRegistrationController)(nil).OnRemove), arg0, arg1, arg2)
+}
+
+// Patch mocks base method.
+func (m *MockClusterRegistrationController) Patch(arg0, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 ...string) (*v1alpha1.ClusterRegistration, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2, arg3}
+	for _, a := range arg4 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Patch", varargs...)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistration)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Patch indicates an expected call of Patch.
+func (mr *MockClusterRegistrationControllerMockRecorder) Patch(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClusterRegistrationController)(nil).Patch), varargs...)
+}
+
+// Update mocks base method.
+func (m *MockClusterRegistrationController) Update(arg0 *v1alpha1.ClusterRegistration) (*v1alpha1.ClusterRegistration, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Update", arg0)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistration)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockClusterRegistrationControllerMockRecorder) Update(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockClusterRegistrationController)(nil).Update), arg0)
+}
+
+// UpdateStatus mocks base method.
+func (m *MockClusterRegistrationController) UpdateStatus(arg0 *v1alpha1.ClusterRegistration) (*v1alpha1.ClusterRegistration, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "UpdateStatus", arg0)
+	ret0, _ := ret[0].(*v1alpha1.ClusterRegistration)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// UpdateStatus indicates an expected call of UpdateStatus.
+func (mr *MockClusterRegistrationControllerMockRecorder) UpdateStatus(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockClusterRegistrationController)(nil).UpdateStatus), arg0)
+}
+
+// Updater mocks base method.
+func (m *MockClusterRegistrationController) Updater() generic.Updater {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Updater")
+	ret0, _ := ret[0].(generic.Updater)
+	return ret0
+}
+
+// Updater indicates an expected call of Updater.
+func (mr *MockClusterRegistrationControllerMockRecorder) Updater() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockClusterRegistrationController)(nil).Updater))
+}
+
+// Watch mocks base method.
+func (m *MockClusterRegistrationController) Watch(arg0 string, arg1 v1.ListOptions) (watch.Interface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Watch", arg0, arg1)
+	ret0, _ := ret[0].(watch.Interface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Watch indicates an expected call of Watch.
+func (mr *MockClusterRegistrationControllerMockRecorder) Watch(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockClusterRegistrationController)(nil).Watch), arg0, arg1)
+}
diff --git a/internal/cmd/controller/mocks/secret_cache_mock.go b/internal/cmd/controller/mocks/secret_cache_mock.go
new file mode 100644
index 00000000..4e03f0dc
--- /dev/null
+++ b/internal/cmd/controller/mocks/secret_cache_mock.go
@@ -0,0 +1,94 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/wrangler/pkg/generated/controllers/core/v1 (interfaces: SecretCache)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1 "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
+	v10 "k8s.io/api/core/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+)
+
+// MockSecretCache is a mock of SecretCache interface.
+type MockSecretCache struct {
+	ctrl     *gomock.Controller
+	recorder *MockSecretCacheMockRecorder
+}
+
+// MockSecretCacheMockRecorder is the mock recorder for MockSecretCache.
+type MockSecretCacheMockRecorder struct {
+	mock *MockSecretCache
+}
+
+// NewMockSecretCache creates a new mock instance.
+func NewMockSecretCache(ctrl *gomock.Controller) *MockSecretCache {
+	mock := &MockSecretCache{ctrl: ctrl}
+	mock.recorder = &MockSecretCacheMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSecretCache) EXPECT() *MockSecretCacheMockRecorder {
+	return m.recorder
+}
+
+// AddIndexer mocks base method.
+func (m *MockSecretCache) AddIndexer(arg0 string, arg1 v1.SecretIndexer) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddIndexer", arg0, arg1)
+}
+
+// AddIndexer indicates an expected call of AddIndexer.
+func (mr *MockSecretCacheMockRecorder) AddIndexer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockSecretCache)(nil).AddIndexer), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockSecretCache) Get(arg0, arg1 string) (*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockSecretCacheMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSecretCache)(nil).Get), arg0, arg1)
+}
+
+// GetByIndex mocks base method.
+func (m *MockSecretCache) GetByIndex(arg0, arg1 string) ([]*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetByIndex", arg0, arg1)
+	ret0, _ := ret[0].([]*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetByIndex indicates an expected call of GetByIndex.
+func (mr *MockSecretCacheMockRecorder) GetByIndex(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockSecretCache)(nil).GetByIndex), arg0, arg1)
+}
+
+// List mocks base method.
+func (m *MockSecretCache) List(arg0 string, arg1 labels.Selector) ([]*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].([]*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockSecretCacheMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSecretCache)(nil).List), arg0, arg1)
+}
diff --git a/internal/cmd/controller/mocks/secret_controller_mock.go b/internal/cmd/controller/mocks/secret_controller_mock.go
new file mode 100644
index 00000000..285471bc
--- /dev/null
+++ b/internal/cmd/controller/mocks/secret_controller_mock.go
@@ -0,0 +1,281 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/wrangler/pkg/generated/controllers/core/v1 (interfaces: SecretController)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	context "context"
+	reflect "reflect"
+	time "time"
+
+	gomock "github.com/golang/mock/gomock"
+	v1 "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
+	generic "github.com/rancher/wrangler/pkg/generic"
+	v10 "k8s.io/api/core/v1"
+	v11 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// MockSecretController is a mock of SecretController interface.
+type MockSecretController struct {
+	ctrl     *gomock.Controller
+	recorder *MockSecretControllerMockRecorder
+}
+
+// MockSecretControllerMockRecorder is the mock recorder for MockSecretController.
+type MockSecretControllerMockRecorder struct {
+	mock *MockSecretController
+}
+
+// NewMockSecretController creates a new mock instance.
+func NewMockSecretController(ctrl *gomock.Controller) *MockSecretController {
+	mock := &MockSecretController{ctrl: ctrl}
+	mock.recorder = &MockSecretControllerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSecretController) EXPECT() *MockSecretControllerMockRecorder {
+	return m.recorder
+}
+
+// AddGenericHandler mocks base method.
+func (m *MockSecretController) AddGenericHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericHandler", arg0, arg1, arg2)
+}
+
+// AddGenericHandler indicates an expected call of AddGenericHandler.
+func (mr *MockSecretControllerMockRecorder) AddGenericHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericHandler", reflect.TypeOf((*MockSecretController)(nil).AddGenericHandler), arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler mocks base method.
+func (m *MockSecretController) AddGenericRemoveHandler(arg0 context.Context, arg1 string, arg2 generic.Handler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddGenericRemoveHandler", arg0, arg1, arg2)
+}
+
+// AddGenericRemoveHandler indicates an expected call of AddGenericRemoveHandler.
+func (mr *MockSecretControllerMockRecorder) AddGenericRemoveHandler(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddGenericRemoveHandler", reflect.TypeOf((*MockSecretController)(nil).AddGenericRemoveHandler), arg0, arg1, arg2)
+}
+
+// Cache mocks base method.
+func (m *MockSecretController) Cache() v1.SecretCache {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Cache")
+	ret0, _ := ret[0].(v1.SecretCache)
+	return ret0
+}
+
+// Cache indicates an expected call of Cache.
+func (mr *MockSecretControllerMockRecorder) Cache() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cache", reflect.TypeOf((*MockSecretController)(nil).Cache))
+}
+
+// Create mocks base method.
+func (m *MockSecretController) Create(arg0 *v10.Secret) (*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Create", arg0)
+	ret0, _ := ret[0].(*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Create indicates an expected call of Create.
+func (mr *MockSecretControllerMockRecorder) Create(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSecretController)(nil).Create), arg0)
+}
+
+// Delete mocks base method.
+func (m *MockSecretController) Delete(arg0, arg1 string, arg2 *v11.DeleteOptions) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Delete", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockSecretControllerMockRecorder) Delete(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSecretController)(nil).Delete), arg0, arg1, arg2)
+}
+
+// Enqueue mocks base method.
+func (m *MockSecretController) Enqueue(arg0, arg1 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Enqueue", arg0, arg1)
+}
+
+// Enqueue indicates an expected call of Enqueue.
+func (mr *MockSecretControllerMockRecorder) Enqueue(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enqueue", reflect.TypeOf((*MockSecretController)(nil).Enqueue), arg0, arg1)
+}
+
+// EnqueueAfter mocks base method.
+func (m *MockSecretController) EnqueueAfter(arg0, arg1 string, arg2 time.Duration) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "EnqueueAfter", arg0, arg1, arg2)
+}
+
+// EnqueueAfter indicates an expected call of EnqueueAfter.
+func (mr *MockSecretControllerMockRecorder) EnqueueAfter(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnqueueAfter", reflect.TypeOf((*MockSecretController)(nil).EnqueueAfter), arg0, arg1, arg2)
+}
+
+// Get mocks base method.
+func (m *MockSecretController) Get(arg0, arg1 string, arg2 v11.GetOptions) (*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockSecretControllerMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSecretController)(nil).Get), arg0, arg1, arg2)
+}
+
+// GroupVersionKind mocks base method.
+func (m *MockSecretController) GroupVersionKind() schema.GroupVersionKind {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GroupVersionKind")
+	ret0, _ := ret[0].(schema.GroupVersionKind)
+	return ret0
+}
+
+// GroupVersionKind indicates an expected call of GroupVersionKind.
+func (mr *MockSecretControllerMockRecorder) GroupVersionKind() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupVersionKind", reflect.TypeOf((*MockSecretController)(nil).GroupVersionKind))
+}
+
+// Informer mocks base method.
+func (m *MockSecretController) Informer() cache.SharedIndexInformer {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Informer")
+	ret0, _ := ret[0].(cache.SharedIndexInformer)
+	return ret0
+}
+
+// Informer indicates an expected call of Informer.
+func (mr *MockSecretControllerMockRecorder) Informer() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockSecretController)(nil).Informer))
+}
+
+// List mocks base method.
+func (m *MockSecretController) List(arg0 string, arg1 v11.ListOptions) (*v10.SecretList, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].(*v10.SecretList)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockSecretControllerMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockSecretController)(nil).List), arg0, arg1)
+}
+
+// OnChange mocks base method.
+func (m *MockSecretController) OnChange(arg0 context.Context, arg1 string, arg2 v1.SecretHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnChange", arg0, arg1, arg2)
+}
+
+// OnChange indicates an expected call of OnChange.
+func (mr *MockSecretControllerMockRecorder) OnChange(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnChange", reflect.TypeOf((*MockSecretController)(nil).OnChange), arg0, arg1, arg2)
+}
+
+// OnRemove mocks base method.
+func (m *MockSecretController) OnRemove(arg0 context.Context, arg1 string, arg2 v1.SecretHandler) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "OnRemove", arg0, arg1, arg2)
+}
+
+// OnRemove indicates an expected call of OnRemove.
+func (mr *MockSecretControllerMockRecorder) OnRemove(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnRemove", reflect.TypeOf((*MockSecretController)(nil).OnRemove), arg0, arg1, arg2)
+}
+
+// Patch mocks base method.
+func (m *MockSecretController) Patch(arg0, arg1 string, arg2 types.PatchType, arg3 []byte, arg4 ...string) (*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0, arg1, arg2, arg3}
+	for _, a := range arg4 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Patch", varargs...)
+	ret0, _ := ret[0].(*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Patch indicates an expected call of Patch.
+func (mr *MockSecretControllerMockRecorder) Patch(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockSecretController)(nil).Patch), varargs...)
+}
+
+// Update mocks base method.
+func (m *MockSecretController) Update(arg0 *v10.Secret) (*v10.Secret, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Update", arg0)
+	ret0, _ := ret[0].(*v10.Secret)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Update indicates an expected call of Update.
+func (mr *MockSecretControllerMockRecorder) Update(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSecretController)(nil).Update), arg0)
+}
+
+// Updater mocks base method.
+func (m *MockSecretController) Updater() generic.Updater {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Updater")
+	ret0, _ := ret[0].(generic.Updater)
+	return ret0
+}
+
+// Updater indicates an expected call of Updater.
+func (mr *MockSecretControllerMockRecorder) Updater() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Updater", reflect.TypeOf((*MockSecretController)(nil).Updater))
+}
+
+// Watch mocks base method.
+func (m *MockSecretController) Watch(arg0 string, arg1 v11.ListOptions) (watch.Interface, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Watch", arg0, arg1)
+	ret0, _ := ret[0].(watch.Interface)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Watch indicates an expected call of Watch.
+func (mr *MockSecretControllerMockRecorder) Watch(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockSecretController)(nil).Watch), arg0, arg1)
+}
diff --git a/internal/cmd/controller/mocks/service_account_cache_mock.go b/internal/cmd/controller/mocks/service_account_cache_mock.go
new file mode 100644
index 00000000..bc9357a2
--- /dev/null
+++ b/internal/cmd/controller/mocks/service_account_cache_mock.go
@@ -0,0 +1,94 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/wrangler/pkg/generated/controllers/core/v1 (interfaces: ServiceAccountCache)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1 "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
+	v10 "k8s.io/api/core/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+)
+
+// MockServiceAccountCache is a mock of ServiceAccountCache interface.
+type MockServiceAccountCache struct {
+	ctrl     *gomock.Controller
+	recorder *MockServiceAccountCacheMockRecorder
+}
+
+// MockServiceAccountCacheMockRecorder is the mock recorder for MockServiceAccountCache.
+type MockServiceAccountCacheMockRecorder struct {
+	mock *MockServiceAccountCache
+}
+
+// NewMockServiceAccountCache creates a new mock instance.
+func NewMockServiceAccountCache(ctrl *gomock.Controller) *MockServiceAccountCache {
+	mock := &MockServiceAccountCache{ctrl: ctrl}
+	mock.recorder = &MockServiceAccountCacheMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockServiceAccountCache) EXPECT() *MockServiceAccountCacheMockRecorder {
+	return m.recorder
+}
+
+// AddIndexer mocks base method.
+func (m *MockServiceAccountCache) AddIndexer(arg0 string, arg1 v1.ServiceAccountIndexer) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "AddIndexer", arg0, arg1)
+}
+
+// AddIndexer indicates an expected call of AddIndexer.
+func (mr *MockServiceAccountCacheMockRecorder) AddIndexer(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIndexer", reflect.TypeOf((*MockServiceAccountCache)(nil).AddIndexer), arg0, arg1)
+}
+
+// Get mocks base method.
+func (m *MockServiceAccountCache) Get(arg0, arg1 string) (*v10.ServiceAccount, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0, arg1)
+	ret0, _ := ret[0].(*v10.ServiceAccount)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockServiceAccountCacheMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockServiceAccountCache)(nil).Get), arg0, arg1)
+}
+
+// GetByIndex mocks base method.
+func (m *MockServiceAccountCache) GetByIndex(arg0, arg1 string) ([]*v10.ServiceAccount, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetByIndex", arg0, arg1)
+	ret0, _ := ret[0].([]*v10.ServiceAccount)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetByIndex indicates an expected call of GetByIndex.
+func (mr *MockServiceAccountCacheMockRecorder) GetByIndex(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIndex", reflect.TypeOf((*MockServiceAccountCache)(nil).GetByIndex), arg0, arg1)
+}
+
+// List mocks base method.
+func (m *MockServiceAccountCache) List(arg0 string, arg1 labels.Selector) ([]*v10.ServiceAccount, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "List", arg0, arg1)
+	ret0, _ := ret[0].([]*v10.ServiceAccount)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// List indicates an expected call of List.
+func (mr *MockServiceAccountCacheMockRecorder) List(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockServiceAccountCache)(nil).List), arg0, arg1)
+}


Test patch:
diff --git a/internal/cmd/controller/controllers/clusterregistration/controller_test.go b/internal/cmd/controller/controllers/clusterregistration/controller_test.go
new file mode 100644
index 00000000..0c808011
--- /dev/null
+++ b/internal/cmd/controller/controllers/clusterregistration/controller_test.go
@@ -0,0 +1,252 @@
+package clusterregistration
+
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/service_account_cache_mock.go -package=mocks github.com/rancher/wrangler/pkg/generated/controllers/core/v1 ServiceAccountCache
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/secret_cache_mock.go -package=mocks github.com/rancher/wrangler/pkg/generated/controllers/core/v1 SecretCache
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/secret_controller_mock.go -package=mocks github.com/rancher/wrangler/pkg/generated/controllers/core/v1 SecretController
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/cluster_client_mock.go -package=mocks github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 ClusterClient
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/cluster_cache_mock.go -package=mocks github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 ClusterCache
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/cluster_registration_controller_mock.go -package=mocks github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 ClusterRegistrationController
+
+import (
+	"fmt"
+
+	"github.com/golang/mock/gomock"
+
+	"github.com/rancher/fleet/internal/cmd/controller/mocks"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/wrangler/pkg/generic"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("ClusterRegistration OnChange", func() {
+	var (
+		request *fleet.ClusterRegistration
+		status  fleet.ClusterRegistrationStatus
+		cluster *fleet.Cluster
+		sa      *corev1.ServiceAccount
+
+		saCache                       *mocks.MockServiceAccountCache
+		secretCache                   *mocks.MockSecretCache
+		secretController              *mocks.MockSecretController
+		clusterClient                 *mocks.MockClusterClient
+		clusterRegistrationController *mocks.MockClusterRegistrationController
+		clusterCache                  *mocks.MockClusterCache
+		h                             *handler
+		notFound                      = errors.NewNotFound(schema.GroupResource{}, "")
+		anError                       = fmt.Errorf("an error occurred")
+	)
+
+	BeforeEach(func() {
+		ctrl := gomock.NewController(GinkgoT())
+		saCache = mocks.NewMockServiceAccountCache(ctrl)
+		secretCache = mocks.NewMockSecretCache(ctrl)
+		secretController = mocks.NewMockSecretController(ctrl)
+		clusterClient = mocks.NewMockClusterClient(ctrl)
+		clusterRegistrationController = mocks.NewMockClusterRegistrationController(ctrl)
+		clusterCache = mocks.NewMockClusterCache(ctrl)
+
+		h = &handler{
+			systemNamespace:             "fleet-system",
+			systemRegistrationNamespace: "fleet-clusters-system",
+			clusterRegistration:         clusterRegistrationController,
+			clusterCache:                clusterCache,
+			clusters:                    clusterClient,
+			secretsCache:                secretCache,
+			secrets:                     secretController,
+			serviceAccountCache:         saCache,
+		}
+
+	})
+
+	Context("ClusterRegistration already granted", func() {
+		BeforeEach(func() {
+			status = fleet.ClusterRegistrationStatus{
+				Granted: true,
+			}
+		})
+
+		It("does nothing", func() {
+			objs, newStatus, err := h.OnChange(request, status)
+			Expect(err).To(Equal(generic.ErrSkip))
+			Expect(objs).To(BeEmpty())
+			Expect(newStatus.Granted).To(BeTrue())
+		})
+	})
+
+	Context("Cluster is missing", func() {
+		BeforeEach(func() {
+			request = &fleet.ClusterRegistration{
+				Spec: fleet.ClusterRegistrationSpec{
+					ClientID: "client-id",
+				},
+			}
+			status = fleet.ClusterRegistrationStatus{}
+
+			clusterCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return(nil, nil)
+			// code panics if cache.Get returns an error or nil
+			clusterCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, nil).Return(nil, notFound)
+		})
+
+		When("cluster creation works", func() {
+			BeforeEach(func() {
+				clusterClient.EXPECT().Create(gomock.Any()).Return(nil, nil).Do(func(obj interface{}) {
+					switch cluster := obj.(type) {
+					case *fleet.Cluster:
+						Expect(cluster.Spec.ClientID).To(Equal("client-id"))
+					default:
+						Fail("unexpected type")
+					}
+				})
+			})
+
+			It("creates the missing cluster", func() {
+				objs, newStatus, err := h.OnChange(request, status)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(objs).To(BeEmpty())
+				Expect(newStatus.Granted).To(BeFalse())
+			})
+		})
+
+		When("cluster creation fails", func() {
+			BeforeEach(func() {
+				clusterClient.EXPECT().Create(gomock.Any()).Return(nil, anError)
+			})
+
+			It("returns an error", func() {
+				objs, newStatus, err := h.OnChange(request, status)
+				Expect(err).To(HaveOccurred())
+				Expect(objs).To(BeEmpty())
+				Expect(newStatus.Granted).To(BeFalse())
+			})
+		})
+	})
+
+	Context("Cluster exists", func() {
+		BeforeEach(func() {
+			request = &fleet.ClusterRegistration{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "request-1",
+					Namespace: "fleet-default",
+				},
+				Spec: fleet.ClusterRegistrationSpec{
+					ClientID: "client-id",
+				},
+			}
+
+			cluster = &fleet.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "cluster",
+					Namespace: "fleet-default",
+				},
+				Spec: fleet.ClusterSpec{
+					ClientID: "client-id",
+				},
+			}
+			status = fleet.ClusterRegistrationStatus{}
+
+			clusterCache.EXPECT().GetByIndex(gomock.Any(), gomock.Any()).Return(nil, nil)
+			clusterCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, nil).Return(cluster, nil)
+		})
+
+		When("cluster status has no namespace", func() {
+			It("sets the cluster name into the registrations status", func() {
+				objs, newStatus, err := h.OnChange(request, status)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(objs).To(BeEmpty())
+				Expect(newStatus.Granted).To(BeFalse())
+				Expect(newStatus.ClusterName).To(Equal("cluster"))
+			})
+		})
+
+		When("service account does not exist", func() {
+			BeforeEach(func() {
+				cluster.Status = fleet.ClusterStatus{Namespace: "fleet-default"}
+				saCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, notFound)
+			})
+
+			It("creates a new service account", func() {
+				objs, newStatus, err := h.OnChange(request, status)
+				Expect(err).ToNot(HaveOccurred())
+				Expect(objs).To(HaveLen(1))
+				Expect(newStatus.Granted).To(BeFalse())
+				Expect(newStatus.ClusterName).To(Equal("cluster"))
+			})
+		})
+
+		When("service account secret is missing", func() {
+			BeforeEach(func() {
+				cluster.Status = fleet.ClusterStatus{Namespace: "fleet-default"}
+				// post k8s 1.22 service account without sa.Secrets list
+				sa = &corev1.ServiceAccount{}
+				saCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(sa, nil)
+			})
+
+			Context("cannot create secret", func() {
+				BeforeEach(func() {
+					secretController.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, notFound)
+					secretController.EXPECT().Create(gomock.Any()).Return(nil, anError)
+				})
+
+				It("creates a new service account and errors", func() {
+					objs, _, err := h.OnChange(request, status)
+					Expect(err).To(HaveOccurred())
+					Expect(err.Error()).To(ContainSubstring("failed to authorize cluster"))
+					Expect(objs).To(BeEmpty())
+				})
+			})
+
+			Context("authorizeCluster returns nil,nil", func() {
+				BeforeEach(func() {
+					// pre k8s 1.22 service account has sa.Secrets list
+					sa.Secrets = []corev1.ObjectReference{{Name: "tokensecret"}}
+					secretCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, notFound)
+					secretController.EXPECT().Get(gomock.Any(), "tokensecret", gomock.Any()).Return(nil, nil)
+				})
+
+				It("returns early", func() {
+					objs, newStatus, err := h.OnChange(request, status)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(objs).To(BeEmpty())
+					Expect(newStatus.ClusterName).To(Equal("cluster"))
+					Expect(newStatus.Granted).To(BeFalse())
+				})
+			})
+		})
+
+		When("service account secret exists", func() {
+			BeforeEach(func() {
+				cluster.Status = fleet.ClusterStatus{Namespace: "fleet-default"}
+
+				sa = &corev1.ServiceAccount{}
+				saCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(sa, nil)
+
+				// needs token here, otherwise controller will sleep to wait for it
+				secret := &corev1.Secret{
+					Data: map[string][]byte{"token": []byte("secrettoken")},
+				}
+				secretController.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(secret, nil)
+
+				clusterRegistrationController.EXPECT().List(gomock.Any(), gomock.Any()).Return(&fleet.ClusterRegistrationList{}, nil)
+			})
+
+			Context("grants registration, cleans up and creates objects", func() {
+				BeforeEach(func() {
+				})
+
+				It("creates a new secret", func() {
+					objs, newStatus, err := h.OnChange(request, status)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(objs).To(HaveLen(6))
+					Expect(newStatus.Granted).To(BeTrue())
+				})
+			})
+		})
+	})
+})
diff --git a/internal/cmd/controller/controllers/clusterregistration/suite_test.go b/internal/cmd/controller/controllers/clusterregistration/suite_test.go
new file mode 100644
index 00000000..01a47fcf
--- /dev/null
+++ b/internal/cmd/controller/controllers/clusterregistration/suite_test.go
@@ -0,0 +1,18 @@
+package clusterregistration_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/rancher/fleet/internal/config"
+)
+
+func TestFleet(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "ClusterRegistration Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+	_ = config.Set(&config.Config{IgnoreClusterRegistrationLabels: false})
+})