Repository: submariner-io/submariner-operator
Pull request #1843 (2022-02-14 01:13:59)

Invoke LH agent deployment cleanup in the Operator controller on ServiceDiscovery deletion
Create a Deployment to uninstall the **submariner-lighthouse-agent** component. See individual commits for details.


submariner-bot: 🤖 Created branch: z_pr1843/tpantelis/lh_agent_uninstall 
🚀 Full E2E won't run until the "ready-to-test" label is applied. I will add it automatically once the PR has 2 approvals, or you can add it manually.

Patch:
diff --git a/controllers/servicediscovery/cleanup.go b/controllers/servicediscovery/cleanup.go
index b567daac..339510aa 100644
--- a/controllers/servicediscovery/cleanup.go
+++ b/controllers/servicediscovery/cleanup.go
@@ -20,6 +20,7 @@ package servicediscovery
 
 import (
 	"context"
+	"time"
 
 	"github.com/pkg/errors"
 	"github.com/submariner-io/admiral/pkg/finalizer"
@@ -28,13 +29,22 @@ import (
 	operatorv1alpha1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
 	"github.com/submariner-io/submariner-operator/controllers/constants"
 	ctrlresource "github.com/submariner-io/submariner-operator/controllers/resource"
+	"github.com/submariner-io/submariner-operator/controllers/uninstall"
+	"github.com/submariner-io/submariner-operator/pkg/names"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
 func (r *Reconciler) doCleanup(ctx context.Context, instance *operatorv1alpha1.ServiceDiscovery) (reconcile.Result, error) {
+	if uninstall.IsSupportedForVersion(instance.Spec.Version) {
+		log.Info("Deleting ServiceDiscovery version does not support uninstall", "version", instance.Spec.Version)
+		return reconcile.Result{}, r.removeFinalizer(ctx, instance)
+	}
+
 	var err error
 
 	if instance.Spec.CoreDNSCustomConfig != nil && instance.Spec.CoreDNSCustomConfig.ConfigMapName != "" {
@@ -52,10 +62,41 @@ func (r *Reconciler) doCleanup(ctx context.Context, instance *operatorv1alpha1.S
 		return reconcile.Result{}, err
 	}
 
-	err = finalizer.Remove(ctx, ctrlresource.ForControllerClient(r.config.Client, instance.Namespace, &operatorv1alpha1.ServiceDiscovery{}),
-		instance, constants.CleanupFinalizer)
+	components := []*uninstall.Component{
+		{
+			Resource: &appsv1.Deployment{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      names.ServiceDiscoveryComponent,
+					Namespace: instance.Namespace,
+				},
+			},
+			UninstallResource: newLighthouseAgent(instance, names.AppendUninstall(names.ServiceDiscoveryComponent)),
+		},
+	}
 
-	return reconcile.Result{}, err // nolint:wrapcheck // No need to wrap
+	uninstallInfo := &uninstall.Info{
+		Client:     r.config.Client,
+		Components: components,
+		StartTime:  instance.DeletionTimestamp.Time,
+		Log:        log,
+	}
+
+	requeue, err := uninstallInfo.Run(ctx)
+	if err != nil {
+		return reconcile.Result{}, err // nolint:wrapcheck // No need to wrap
+	}
+
+	if requeue {
+		return reconcile.Result{RequeueAfter: time.Millisecond * 100}, nil
+	}
+
+	return reconcile.Result{}, r.removeFinalizer(ctx, instance)
+}
+
+// nolint:wrapcheck // No need to wrap
+func (r *Reconciler) removeFinalizer(ctx context.Context, instance *operatorv1alpha1.ServiceDiscovery) error {
+	return finalizer.Remove(ctx, ctrlresource.ForControllerClient(r.config.Client, instance.Namespace, instance),
+		instance, constants.CleanupFinalizer)
 }
 
 func (r *Reconciler) removeLighthouseConfigFromCustomDNSConfigMap(ctx context.Context,
diff --git a/controllers/servicediscovery/servicediscovery_controller.go b/controllers/servicediscovery/servicediscovery_controller.go
index 6274fc02..bd244f33 100644
--- a/controllers/servicediscovery/servicediscovery_controller.go
+++ b/controllers/servicediscovery/servicediscovery_controller.go
@@ -61,7 +61,6 @@ var log = logf.Log.WithName("controller_servicediscovery")
 
 const (
 	componentName                 = "submariner-lighthouse"
-	deploymentName                = "submariner-lighthouse-agent"
 	lighthouseCoreDNSName         = "submariner-lighthouse-coredns"
 	defaultOpenShiftDNSController = "default"
 	lighthouseForwardPluginName   = "lighthouse"
@@ -118,7 +117,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (
 		deployment := &appsv1.Deployment{}
 		opts := []controllerClient.DeleteAllOfOption{
 			controllerClient.InNamespace(request.NamespacedName.Namespace),
-			controllerClient.MatchingLabels{"app": deploymentName},
+			controllerClient.MatchingLabels{"app": names.ServiceDiscoveryComponent},
 		}
 		err := r.config.Client.DeleteAllOf(ctx, deployment, opts...)
 
@@ -205,14 +204,14 @@ func (r *Reconciler) addFinalizer(ctx context.Context,
 	return r.getServiceDiscovery(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: instance.Name})
 }
 
-func newLighthouseAgent(cr *submarinerv1alpha1.ServiceDiscovery) *appsv1.Deployment {
+func newLighthouseAgent(cr *submarinerv1alpha1.ServiceDiscovery, name string) *appsv1.Deployment {
 	replicas := int32(1)
 	labels := map[string]string{
-		"app":       deploymentName,
+		"app":       name,
 		"component": componentName,
 	}
 	matchLabels := map[string]string{
-		"app": deploymentName,
+		"app": name,
 	}
 
 	terminationGracePeriodSeconds := int64(0)
@@ -237,7 +236,7 @@ func newLighthouseAgent(cr *submarinerv1alpha1.ServiceDiscovery) *appsv1.Deploym
 	return &appsv1.Deployment{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: cr.Namespace,
-			Name:      deploymentName,
+			Name:      name,
 			Labels:    labels,
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -253,8 +252,8 @@ func newLighthouseAgent(cr *submarinerv1alpha1.ServiceDiscovery) *appsv1.Deploym
 					Containers: []corev1.Container{
 						{
 							Name:            "submariner-lighthouse-agent",
-							Image:           getImagePath(cr, names.ServiceDiscoveryImage, names.ServiceDiscoveryComponent),
-							ImagePullPolicy: helpers.GetPullPolicy(cr.Spec.Version, cr.Spec.ImageOverrides[names.ServiceDiscoveryComponent]),
+							Image:           getImagePath(cr, names.ServiceDiscoveryImage, names.ServiceDiscoveryImage),
+							ImagePullPolicy: helpers.GetPullPolicy(cr.Spec.Version, cr.Spec.ImageOverrides[names.ServiceDiscoveryImage]),
 							Env: []corev1.EnvVar{
 								{Name: "SUBMARINER_NAMESPACE", Value: cr.Spec.Namespace},
 								{Name: "SUBMARINER_CLUSTERID", Value: cr.Spec.ClusterID},
@@ -711,7 +710,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
 }
 
 func (r *Reconciler) ensureLightHouseAgent(instance *submarinerv1alpha1.ServiceDiscovery, reqLogger logr.Logger) error {
-	lightHouseAgent := newLighthouseAgent(instance)
+	lightHouseAgent := newLighthouseAgent(instance, names.ServiceDiscoveryComponent)
 	if _, err := helpers.ReconcileDeployment(instance, lightHouseAgent, reqLogger,
 		r.config.Client, r.config.Scheme); err != nil {
 		return errors.Wrap(err, "error reconciling agent deployment")
diff --git a/controllers/submariner/cleanup.go b/controllers/submariner/cleanup.go
index a36ea889..3fc94ff7 100644
--- a/controllers/submariner/cleanup.go
+++ b/controllers/submariner/cleanup.go
@@ -20,44 +20,22 @@ package submariner
 
 import (
 	"context"
-	"fmt"
 	"time"
 
-	"github.com/coreos/go-semver/semver"
-	"github.com/pkg/errors"
 	"github.com/submariner-io/admiral/pkg/finalizer"
 	operatorv1alpha1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
 	"github.com/submariner-io/submariner-operator/controllers/constants"
 	"github.com/submariner-io/submariner-operator/controllers/resource"
+	"github.com/submariner-io/submariner-operator/controllers/uninstall"
 	"github.com/submariner-io/submariner-operator/pkg/names"
 	appsv1 "k8s.io/api/apps/v1"
-	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
-const UninstallComponentReadyTimeout = time.Minute * 2
-
-var requeueAfter = reconcile.Result{RequeueAfter: time.Millisecond * 100}
-
-var minComponentUninstallVersion = semver.New("0.12.0")
-
-type component struct {
-	resource          client.Object
-	uninstallResource client.Object
-	checkInstalled    func() bool
-}
-
-func (c *component) isInstalled() bool {
-	return c.checkInstalled == nil || c.checkInstalled()
-}
-
 func (r *Reconciler) runComponentCleanup(ctx context.Context, instance *operatorv1alpha1.Submariner) (reconcile.Result, error) {
-	submVersion, _ := semver.NewVersion(instance.Spec.Version)
-	if submVersion != nil && submVersion.LessThan(*minComponentUninstallVersion) {
-		log.Info("Deleting Submariner version does not support uninstall", "version", submVersion)
+	if uninstall.IsSupportedForVersion(instance.Spec.Version) {
+		log.Info("Deleting Submariner version does not support uninstall", "version", instance.Spec.Version)
 		return reconcile.Result{}, r.removeFinalizer(ctx, instance)
 	}
 
@@ -67,68 +45,46 @@ func (r *Reconciler) runComponentCleanup(ctx context.Context, instance *operator
 		return reconcile.Result{}, err
 	}
 
-	components := []*component{
+	components := []*uninstall.Component{
 		{
-			resource:          newDaemonSet(names.GatewayComponent, instance.Namespace),
-			uninstallResource: newGatewayDaemonSet(instance, names.AppendUninstall(names.GatewayComponent)),
+			Resource:          newDaemonSet(names.GatewayComponent, instance.Namespace),
+			UninstallResource: newGatewayDaemonSet(instance, names.AppendUninstall(names.GatewayComponent)),
 		},
 		{
-			resource:          newDaemonSet(names.RouteAgentComponent, instance.Namespace),
-			uninstallResource: newRouteAgentDaemonSet(instance, names.AppendUninstall(names.RouteAgentComponent)),
+			Resource:          newDaemonSet(names.RouteAgentComponent, instance.Namespace),
+			UninstallResource: newRouteAgentDaemonSet(instance, names.AppendUninstall(names.RouteAgentComponent)),
 		},
 		{
-			resource:          newDaemonSet(names.GlobalnetComponent, instance.Namespace),
-			uninstallResource: newGlobalnetDaemonSet(instance, names.AppendUninstall(names.GlobalnetComponent)),
-			checkInstalled: func() bool {
+			Resource:          newDaemonSet(names.GlobalnetComponent, instance.Namespace),
+			UninstallResource: newGlobalnetDaemonSet(instance, names.AppendUninstall(names.GlobalnetComponent)),
+			CheckInstalled: func() bool {
 				return instance.Spec.GlobalCIDR != ""
 			},
 		},
 		{
-			resource: newDeployment(names.NetworkPluginSyncerComponent, instance.Namespace),
-			uninstallResource: newNetworkPluginSyncerDeployment(instance, clusterNetwork,
+			Resource: newDeployment(names.NetworkPluginSyncerComponent, instance.Namespace),
+			UninstallResource: newNetworkPluginSyncerDeployment(instance, clusterNetwork,
 				names.AppendUninstall(names.NetworkPluginSyncerComponent)),
-			checkInstalled: func() bool {
+			CheckInstalled: func() bool {
 				return needsNetworkPluginSyncer(instance)
 			},
 		},
 	}
 
-	// First, delete the regular DaemonSets/Deployments.
-	for _, c := range components {
-		if !c.isInstalled() {
-			continue
-		}
-
-		err := r.ensureDeleted(ctx, c.resource)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
+	uninstallInfo := &uninstall.Info{
+		Client:     r.config.Client,
+		Components: components,
+		StartTime:  instance.DeletionTimestamp.Time,
+		Log:        log,
 	}
 
-	// Next, ensure all their pods are cleaned up.
-	requeue, err := r.ensurePodsDeleted(ctx, components)
+	requeue, err := uninstallInfo.Run(ctx)
 	if err != nil {
-		return reconcile.Result{}, err
+		return reconcile.Result{}, err // nolint:wrapcheck // No need to wrap
 	}
 
 	if requeue {
-		return requeueAfter, nil
-	}
-
-	err = r.createUninstallResources(ctx, components)
-	if err != nil {
-		return reconcile.Result{}, err
-	}
-
-	timedOut := time.Since(instance.DeletionTimestamp.Time) >= UninstallComponentReadyTimeout
-
-	requeue, err = r.ensureUninstallResourcesComplete(ctx, components, timedOut)
-	if err != nil {
-		return reconcile.Result{}, err
-	}
-
-	if requeue {
-		return requeueAfter, nil
+		return reconcile.Result{RequeueAfter: time.Millisecond * 100}, nil
 	}
 
 	return reconcile.Result{}, r.removeFinalizer(ctx, instance)
@@ -140,201 +96,6 @@ func (r *Reconciler) removeFinalizer(ctx context.Context, instance *operatorv1al
 		instance, constants.CleanupFinalizer)
 }
 
-func (r *Reconciler) ensurePodsDeleted(ctx context.Context, components []*component) (bool, error) {
-	for _, c := range components {
-		if !c.isInstalled() {
-			continue
-		}
-
-		pods, err := findPodsBySelector(ctx, r.config.Client, c.resource.GetNamespace(), &metav1.LabelSelector{
-			MatchLabels: map[string]string{appLabel: c.resource.GetName()},
-		})
-		if err != nil {
-			return false, err
-		}
-
-		numPods := len(pods)
-		if numPods > 0 {
-			log.Info(fmt.Sprintf("%T still has pods - requeueing: ", c.resource), "name", c.resource.GetName(),
-				"namespace", c.resource.GetNamespace(), "numPods", numPods)
-			return true, nil
-		}
-	}
-
-	return false, nil
-}
-
-func (r *Reconciler) ensureUninstallResourcesComplete(ctx context.Context, components []*component, timedOut bool) (bool, error) {
-	for _, c := range components {
-		if !c.isInstalled() {
-			continue
-		}
-
-		var requeue bool
-		var err error
-
-		switch d := c.uninstallResource.(type) {
-		case *appsv1.DaemonSet:
-			requeue, err = r.ensureDaemonSetReady(ctx, client.ObjectKeyFromObject(d), timedOut)
-		case *appsv1.Deployment:
-			requeue, err = r.ensureDeploymentReady(ctx, client.ObjectKeyFromObject(d), timedOut)
-		}
-
-		if err != nil {
-			return false, err
-		}
-
-		if requeue {
-			return true, nil
-		}
-	}
-
-	return false, nil
-}
-
-func (r *Reconciler) createUninstallResources(ctx context.Context, components []*component) error {
-	for _, c := range components {
-		if !c.isInstalled() {
-			continue
-		}
-
-		var err error
-
-		switch d := c.uninstallResource.(type) {
-		case *appsv1.DaemonSet:
-			err = r.createUninstallDaemonSetFrom(ctx, d)
-		case *appsv1.Deployment:
-			err = r.createUninstallDeploymentFrom(ctx, d)
-		}
-
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (r *Reconciler) ensureDeleted(ctx context.Context, obj client.Object) error {
-	err := r.config.Client.Delete(ctx, obj)
-	if apierrors.IsNotFound(err) {
-		return nil
-	}
-
-	if err == nil {
-		log.Info(fmt.Sprintf("Deleted %T:", obj), "name", obj.GetName(), "namespace", obj.GetNamespace())
-	}
-
-	return errors.Wrapf(err, "error deleting %#v", obj)
-}
-
-func (r *Reconciler) createUninstallDaemonSetFrom(ctx context.Context, daemonSet *appsv1.DaemonSet) error {
-	convertPodSpecContainersToUninstall(&daemonSet.Spec.Template.Spec)
-
-	err := r.config.Client.Create(ctx, daemonSet)
-	if err != nil {
-		if !apierrors.IsAlreadyExists(err) {
-			return errors.Wrapf(err, "error creating %#v", daemonSet)
-		}
-	} else {
-		log.Info("Created DaemonSet:", "name", daemonSet.Name, "namespace", daemonSet.Namespace,
-			"Image", daemonSet.Spec.Template.Spec.InitContainers[0].Image)
-	}
-
-	return nil
-}
-
-func (r *Reconciler) ensureDaemonSetReady(ctx context.Context, key client.ObjectKey, timedOut bool) (bool, error) {
-	daemonSet := &appsv1.DaemonSet{}
-
-	err := r.config.Client.Get(ctx, key, daemonSet)
-	if err != nil {
-		return false, errors.Wrapf(err, "error getting %#v", daemonSet)
-	}
-
-	if daemonSet.Status.ObservedGeneration == 0 || daemonSet.Status.ObservedGeneration < daemonSet.Generation {
-		log.Info("DaemonSet generation not yet observed - requeueing:", "name", daemonSet.Name,
-			"namespace", daemonSet.Namespace, "Generation", daemonSet.Generation, "ObservedGeneration", daemonSet.Status.ObservedGeneration)
-		return true, nil
-	}
-
-	if daemonSet.Status.DesiredNumberScheduled == 0 {
-		log.Info("DaemonSet has no available nodes:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
-	} else if daemonSet.Status.DesiredNumberScheduled != daemonSet.Status.NumberReady {
-		log.Info("DaemonSet not ready yet:", "name", daemonSet.Name, "namespace", daemonSet.Namespace,
-			"DesiredNumberScheduled", daemonSet.Status.DesiredNumberScheduled, "NumberReady", daemonSet.Status.NumberReady)
-
-		if !timedOut {
-			return true, nil
-		}
-
-		log.Info("DaemonSet timed out - deleting:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
-	} else {
-		log.Info("DaemonSet is ready:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
-	}
-
-	return false, r.ensureDeleted(ctx, daemonSet)
-}
-
-func (r *Reconciler) createUninstallDeploymentFrom(ctx context.Context, deployment *appsv1.Deployment) error {
-	convertPodSpecContainersToUninstall(&deployment.Spec.Template.Spec)
-
-	err := r.config.Client.Create(ctx, deployment)
-	if err != nil {
-		if !apierrors.IsAlreadyExists(err) {
-			return errors.Wrapf(err, "error creating %#v", deployment)
-		}
-	} else {
-		log.Info("Created Deployment:", "name", deployment.Name, "namespace", deployment.Namespace)
-	}
-
-	return nil
-}
-
-func (r *Reconciler) ensureDeploymentReady(ctx context.Context, key client.ObjectKey, timedOut bool) (bool, error) {
-	deployment := &appsv1.Deployment{}
-
-	err := r.config.Client.Get(ctx, key, deployment)
-	if err != nil {
-		return false, errors.Wrapf(err, "error getting %#v", deployment)
-	}
-
-	var replicas int32 = 1
-	if deployment.Spec.Replicas != nil {
-		replicas = *deployment.Spec.Replicas
-	}
-
-	if deployment.Status.AvailableReplicas != replicas {
-		log.Info("Deployment not ready yet:", "name", deployment.Name, "namespace", deployment.Namespace,
-			"AvailableReplicas", deployment.Status.AvailableReplicas, "DesiredReplicas", replicas)
-
-		if !timedOut {
-			return true, nil
-		}
-
-		log.Info("Deployment timed out - deleting:", "name", deployment.Name, "namespace", deployment.Namespace)
-	}
-
-	log.Info("Deployment is ready:", "name", deployment.Name, "namespace", deployment.Namespace)
-
-	return false, r.ensureDeleted(ctx, deployment)
-}
-
-func convertPodSpecContainersToUninstall(podSpec *corev1.PodSpec) {
-	// We're going to use the PodSpec to run a one-time task by using an init container to run the task.
-	// See http://blog.itaysk.com/2017/12/26/the-single-use-daemonset-pattern-and-prepulling-images-in-kubernetes
-	// for more details.
-	podSpec.InitContainers = podSpec.Containers
-	podSpec.InitContainers[0].Env = append(podSpec.InitContainers[0].Env, corev1.EnvVar{Name: "UNINSTALL", Value: "true"})
-
-	podSpec.Containers = []corev1.Container{
-		{
-			Name:  "pause",
-			Image: "gcr.io/google_containers/pause",
-		},
-	}
-}
-
 func newDaemonSet(name, namespace string) *appsv1.DaemonSet {
 	return &appsv1.DaemonSet{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/controllers/uninstall/uninstall.go b/controllers/uninstall/uninstall.go
new file mode 100644
index 00000000..788dc335
--- /dev/null
+++ b/controllers/uninstall/uninstall.go
@@ -0,0 +1,305 @@
+/*
+SPDX-License-Identifier: Apache-2.0
+
+Copyright Contributors to the Submariner project.
+
+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 uninstall
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/coreos/go-semver/semver"
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const ComponentReadyTimeout = time.Minute * 2
+
+var minComponentUninstallVersion = semver.New("0.12.0")
+
+type Component struct {
+	Resource          client.Object
+	UninstallResource client.Object
+	CheckInstalled    func() bool
+}
+
+type Info struct {
+	Client     client.Client
+	Components []*Component
+	StartTime  time.Time
+	Log        logr.Logger
+}
+
+func (c *Component) isInstalled() bool {
+	return c.CheckInstalled == nil || c.CheckInstalled()
+}
+
+func (i *Info) Run(ctx context.Context) (bool, error) {
+	// First, delete the regular DaemonSets/Deployments.
+	for _, c := range i.Components {
+		if !c.isInstalled() {
+			continue
+		}
+
+		err := i.ensureDeleted(ctx, c.Resource)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	// Next, ensure all their pods are cleaned up.
+	requeue, err := i.ensurePodsDeleted(ctx)
+	if requeue || err != nil {
+		return requeue, err
+	}
+
+	err = i.createUninstallResources(ctx)
+	if err != nil {
+		return false, err
+	}
+
+	return i.ensureUninstallResourcesComplete(ctx)
+}
+
+func (i *Info) ensureDeleted(ctx context.Context, obj client.Object) error {
+	err := i.Client.Delete(ctx, obj)
+	if apierrors.IsNotFound(err) {
+		return nil
+	}
+
+	if err == nil {
+		i.Log.Info(fmt.Sprintf("Deleted %T:", obj), "name", obj.GetName(), "namespace", obj.GetNamespace())
+	}
+
+	return errors.Wrapf(err, "error deleting %#v", obj)
+}
+
+func (i *Info) ensurePodsDeleted(ctx context.Context) (bool, error) {
+	for _, c := range i.Components {
+		if !c.isInstalled() {
+			continue
+		}
+
+		pods, err := findPodsBySelector(ctx, i.Client, c.Resource.GetNamespace(), &metav1.LabelSelector{
+			MatchLabels: map[string]string{"app": c.Resource.GetName()},
+		})
+		if err != nil {
+			return false, err
+		}
+
+		numPods := len(pods)
+		if numPods > 0 {
+			i.Log.Info(fmt.Sprintf("%T still has pods - requeueing: ", c.Resource), "name", c.Resource.GetName(),
+				"namespace", c.Resource.GetNamespace(), "numPods", numPods)
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+func (i *Info) createUninstallResources(ctx context.Context) error {
+	for _, c := range i.Components {
+		if !c.isInstalled() {
+			continue
+		}
+
+		var err error
+
+		switch d := c.UninstallResource.(type) {
+		case *appsv1.DaemonSet:
+			err = i.createUninstallDaemonSetFrom(ctx, d)
+		case *appsv1.Deployment:
+			err = i.createUninstallDeploymentFrom(ctx, d)
+		default:
+			panic(fmt.Sprintf("Unknown type: %T", d))
+		}
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (i *Info) createUninstallDeploymentFrom(ctx context.Context, deployment *appsv1.Deployment) error {
+	convertPodSpecContainersToUninstall(&deployment.Spec.Template.Spec)
+
+	err := i.Client.Create(ctx, deployment)
+	if err != nil {
+		if !apierrors.IsAlreadyExists(err) {
+			return errors.Wrapf(err, "error creating %#v", deployment)
+		}
+	} else {
+		i.Log.Info("Created Deployment:", "name", deployment.Name, "namespace", deployment.Namespace)
+	}
+
+	return nil
+}
+
+func (i *Info) createUninstallDaemonSetFrom(ctx context.Context, daemonSet *appsv1.DaemonSet) error {
+	convertPodSpecContainersToUninstall(&daemonSet.Spec.Template.Spec)
+
+	err := i.Client.Create(ctx, daemonSet)
+	if err != nil {
+		if !apierrors.IsAlreadyExists(err) {
+			return errors.Wrapf(err, "error creating %#v", daemonSet)
+		}
+	} else {
+		i.Log.Info("Created DaemonSet:", "name", daemonSet.Name, "namespace", daemonSet.Namespace,
+			"Image", daemonSet.Spec.Template.Spec.InitContainers[0].Image)
+	}
+
+	return nil
+}
+
+func (i *Info) ensureUninstallResourcesComplete(ctx context.Context) (bool, error) {
+	timedOut := time.Since(i.StartTime) >= ComponentReadyTimeout
+
+	for _, c := range i.Components {
+		if !c.isInstalled() {
+			continue
+		}
+
+		var requeue bool
+		var err error
+
+		switch d := c.UninstallResource.(type) {
+		case *appsv1.DaemonSet:
+			requeue, err = i.ensureDaemonSetReady(ctx, client.ObjectKeyFromObject(d), timedOut)
+		case *appsv1.Deployment:
+			requeue, err = i.ensureDeploymentReady(ctx, client.ObjectKeyFromObject(d), timedOut)
+		default:
+			panic(fmt.Sprintf("Unknown type: %T", d))
+		}
+
+		if err != nil {
+			return false, err
+		}
+
+		if requeue {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
+
+func (i *Info) ensureDaemonSetReady(ctx context.Context, key client.ObjectKey, timedOut bool) (bool, error) {
+	daemonSet := &appsv1.DaemonSet{}
+
+	err := i.Client.Get(ctx, key, daemonSet)
+	if err != nil {
+		return false, errors.Wrapf(err, "error getting %#v", daemonSet)
+	}
+
+	if daemonSet.Status.ObservedGeneration == 0 || daemonSet.Status.ObservedGeneration < daemonSet.Generation {
+		i.Log.Info("DaemonSet generation not yet observed - requeueing:", "name", daemonSet.Name,
+			"namespace", daemonSet.Namespace, "Generation", daemonSet.Generation, "ObservedGeneration", daemonSet.Status.ObservedGeneration)
+		return true, nil
+	}
+
+	if daemonSet.Status.DesiredNumberScheduled == 0 {
+		i.Log.Info("DaemonSet has no available nodes:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
+	} else if daemonSet.Status.DesiredNumberScheduled != daemonSet.Status.NumberReady {
+		i.Log.Info("DaemonSet not ready yet:", "name", daemonSet.Name, "namespace", daemonSet.Namespace,
+			"DesiredNumberScheduled", daemonSet.Status.DesiredNumberScheduled, "NumberReady", daemonSet.Status.NumberReady)
+
+		if !timedOut {
+			return true, nil
+		}
+
+		i.Log.Info("DaemonSet timed out - deleting:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
+	} else {
+		i.Log.Info("DaemonSet is ready:", "name", daemonSet.Name, "namespace", daemonSet.Namespace)
+	}
+
+	return false, i.ensureDeleted(ctx, daemonSet)
+}
+
+func (i *Info) ensureDeploymentReady(ctx context.Context, key client.ObjectKey, timedOut bool) (bool, error) {
+	deployment := &appsv1.Deployment{}
+
+	err := i.Client.Get(ctx, key, deployment)
+	if err != nil {
+		return false, errors.Wrapf(err, "error getting %#v", deployment)
+	}
+
+	var replicas int32 = 1
+	if deployment.Spec.Replicas != nil {
+		replicas = *deployment.Spec.Replicas
+	}
+
+	if deployment.Status.AvailableReplicas != replicas {
+		i.Log.Info("Deployment not ready yet:", "name", deployment.Name, "namespace", deployment.Namespace,
+			"AvailableReplicas", deployment.Status.AvailableReplicas, "DesiredReplicas", replicas)
+
+		if !timedOut {
+			return true, nil
+		}
+
+		i.Log.Info("Deployment timed out - deleting:", "name", deployment.Name, "namespace", deployment.Namespace)
+	}
+
+	i.Log.Info("Deployment is ready:", "name", deployment.Name, "namespace", deployment.Namespace)
+
+	return false, i.ensureDeleted(ctx, deployment)
+}
+
+func convertPodSpecContainersToUninstall(podSpec *corev1.PodSpec) {
+	// We're going to use the PodSpec to run a one-time task by using an init container to run the task.
+	// See http://blog.itaysk.com/2017/12/26/the-single-use-daemonset-pattern-and-prepulling-images-in-kubernetes
+	// for more details.
+	podSpec.InitContainers = podSpec.Containers
+	podSpec.InitContainers[0].Env = append(podSpec.InitContainers[0].Env, corev1.EnvVar{Name: "UNINSTALL", Value: "true"})
+
+	podSpec.Containers = []corev1.Container{
+		{
+			Name:  "pause",
+			Image: "gcr.io/google_containers/pause",
+		},
+	}
+}
+
+func findPodsBySelector(ctx context.Context, clnt client.Reader, namespace string,
+	labelSelector *metav1.LabelSelector) ([]corev1.Pod, error) {
+	selector, err := metav1.LabelSelectorAsSelector(labelSelector)
+	if err != nil {
+		return nil, errors.Wrap(err, "error creating label selector")
+	}
+
+	pods := &corev1.PodList{}
+
+	err = clnt.List(ctx, pods, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: selector})
+	if err != nil {
+		return nil, errors.Wrap(err, "error listing DaemonSet pods")
+	}
+
+	return pods.Items, nil
+}
+
+func IsSupportedForVersion(version string) bool {
+	semVersion, _ := semver.NewVersion(version)
+	return !(semVersion != nil && !semVersion.LessThan(*minComponentUninstallVersion))
+}
diff --git a/pkg/names/names.go b/pkg/names/names.go
index e5195e09..60c02e8d 100644
--- a/pkg/names/names.go
+++ b/pkg/names/names.go
@@ -23,7 +23,7 @@ const (
 	RouteAgentComponent          = "submariner-routeagent"
 	GatewayComponent             = "submariner-gateway"
 	GlobalnetComponent           = "submariner-globalnet"
-	ServiceDiscoveryComponent    = "lighthouse-agent"
+	ServiceDiscoveryComponent    = "submariner-lighthouse-agent"
 	LighthouseCoreDNSComponent   = "lighthouse-coredns"
 	OperatorComponent            = "submariner-operator"
 	ServiceDiscoveryCrName       = "service-discovery"


Test patch:
diff --git a/controllers/servicediscovery/servicediscovery_controller_test.go b/controllers/servicediscovery/servicediscovery_controller_test.go
index bcad7bcd..d8bce300 100644
--- a/controllers/servicediscovery/servicediscovery_controller_test.go
+++ b/controllers/servicediscovery/servicediscovery_controller_test.go
@@ -18,6 +18,7 @@ limitations under the License.
 package servicediscovery_test
 
 import (
+	"context"
 	"strings"
 
 	. "github.com/onsi/ginkgo"
@@ -26,26 +27,35 @@ import (
 	submariner_v1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
 	"github.com/submariner-io/submariner-operator/controllers/constants"
 	"github.com/submariner-io/submariner-operator/controllers/resource"
+	"github.com/submariner-io/submariner-operator/pkg/names"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var _ = Describe("Service discovery controller", func() {
 	Context("Reconciliation", testReconciliation)
-	Context("Deletion", testDeletion)
+	Context("Deletion", func() {
+		Context("Coredns cleanup", testCoreDNSCleanup)
+		Context("Deployment cleanup", testDeploymentUninstall)
+	})
 })
 
 func testReconciliation() {
 	t := newTestDriver()
 
+	It("should add a finalizer to the ServiceDiscovery resource", func() {
+		_, _ = t.DoReconcile()
+		t.awaitFinalizer()
+	})
+
 	When("the openshift DNS config exists", func() {
 		Context("and the lighthouse config isn't present", func() {
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSConfig(""), newDNSService(clusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSConfig(""), newDNSService(clusterIP))
 			})
 
 			It("should add it", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				assertDNSConfigServers(t.assertDNSConfig(), newDNSConfig(clusterIP))
 			})
@@ -55,11 +65,11 @@ func testReconciliation() {
 			updatedClusterIP := "10.10.10.11"
 
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSConfig(clusterIP), newDNSService(updatedClusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSConfig(clusterIP), newDNSService(updatedClusterIP))
 			})
 
 			It("should update the lighthouse config", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				assertDNSConfigServers(t.assertDNSConfig(), newDNSConfig(updatedClusterIP))
 			})
@@ -67,15 +77,15 @@ func testReconciliation() {
 
 		Context("and the lighthouse DNS service doesn't exist", func() {
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSConfig(""))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSConfig(""))
 			})
 
 			It("should create the service and add the lighthouse config", func() {
-				t.assertReconcileError()
+				t.AssertReconcileError()
 
 				t.setLighthouseCoreDNSServiceIP()
 
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				assertDNSConfigServers(t.assertDNSConfig(), newDNSConfig(clusterIP))
 			})
@@ -85,12 +95,12 @@ func testReconciliation() {
 	When("the coredns ConfigMap exists", func() {
 		Context("and the lighthouse config isn't present", func() {
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSService(clusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSService(clusterIP))
 				t.createConfigMap(newCoreDNSConfigMap(coreDNSCorefileData("")))
 			})
 
 			It("should add it", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertCoreDNSConfigMap().Data["Corefile"])).To(Equal(coreDNSCorefileData(clusterIP)))
 			})
@@ -100,12 +110,12 @@ func testReconciliation() {
 			updatedClusterIP := "10.10.10.11"
 
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSService(updatedClusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSService(updatedClusterIP))
 				t.createConfigMap(newCoreDNSConfigMap(coreDNSCorefileData(clusterIP)))
 			})
 
 			It("should update the lighthouse config", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertCoreDNSConfigMap().Data["Corefile"])).To(Equal(coreDNSCorefileData(updatedClusterIP)))
 			})
@@ -117,11 +127,11 @@ func testReconciliation() {
 			})
 
 			It("should create the service and add the lighthouse config", func() {
-				t.assertReconcileError()
+				t.AssertReconcileError()
 
 				t.setLighthouseCoreDNSServiceIP()
 
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertCoreDNSConfigMap().Data["Corefile"])).To(Equal(coreDNSCorefileData(clusterIP)))
 			})
@@ -148,11 +158,11 @@ func testReconciliation() {
 					},
 				})
 
-				t.initClientObjs = append(t.initClientObjs, newDNSService(clusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSService(clusterIP))
 			})
 
 			It("should update it", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertConfigMap(t.serviceDiscovery.Spec.CoreDNSCustomConfig.ConfigMapName,
 					t.serviceDiscovery.Spec.CoreDNSCustomConfig.Namespace).Data["lighthouse.server"])).To(Equal(
@@ -162,11 +172,11 @@ func testReconciliation() {
 
 		Context("and the custom coredns ConfigMap doesn't exist", func() {
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, newDNSService(clusterIP))
+				t.InitClientObjs = append(t.InitClientObjs, newDNSService(clusterIP))
 			})
 
 			It("should create it", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertConfigMap(t.serviceDiscovery.Spec.CoreDNSCustomConfig.ConfigMapName,
 					t.serviceDiscovery.Spec.CoreDNSCustomConfig.Namespace).Data["lighthouse.server"])).To(Equal(
@@ -176,11 +186,11 @@ func testReconciliation() {
 
 		Context("and the lighthouse DNS service doesn't exist", func() {
 			It("should create the service and the custom coredns ConfigMap", func() {
-				t.assertReconcileError()
+				t.AssertReconcileError()
 
 				t.setLighthouseCoreDNSServiceIP()
 
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 
 				Expect(strings.TrimSpace(t.assertConfigMap(t.serviceDiscovery.Spec.CoreDNSCustomConfig.ConfigMapName,
 					t.serviceDiscovery.Spec.CoreDNSCustomConfig.Namespace).Data["lighthouse.server"])).To(Equal(
@@ -190,7 +200,7 @@ func testReconciliation() {
 	})
 }
 
-func testDeletion() {
+func testCoreDNSCleanup() {
 	t := newTestDriver()
 
 	BeforeEach(func() {
@@ -201,7 +211,15 @@ func testDeletion() {
 	})
 
 	JustBeforeEach(func() {
-		t.assertReconcileSuccess()
+		deployment := t.NewDeployment(names.AppendUninstall(names.ServiceDiscoveryComponent))
+
+		var one int32 = 1
+		deployment.Spec.Replicas = &one
+
+		Expect(t.Client.Create(context.TODO(), deployment)).To(Succeed())
+		t.UpdateDeploymentToReady(deployment)
+
+		t.AssertReconcileSuccess()
 	})
 
 	When("the coredns ConfigMap exists", func() {
@@ -212,7 +230,7 @@ func testDeletion() {
 		It("should remove the lighthouse config section", func() {
 			Expect(strings.TrimSpace(t.assertCoreDNSConfigMap().Data["Corefile"])).To(Equal(coreDNSCorefileData("")))
 
-			test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &submariner_v1.ServiceDiscovery{}),
+			test.AwaitNoFinalizer(resource.ForControllerClient(t.Client, submarinerNamespace, &submariner_v1.ServiceDiscovery{}),
 				serviceDiscoveryName, constants.CleanupFinalizer)
 		})
 
@@ -221,7 +239,7 @@ func testDeletion() {
 
 	When("the openshift DNS config exists", func() {
 		BeforeEach(func() {
-			t.initClientObjs = append(t.initClientObjs, newDNSConfig(clusterIP))
+			t.InitClientObjs = append(t.InitClientObjs, newDNSConfig(clusterIP))
 		})
 
 		It("should remove the lighthouse config", func() {
@@ -265,3 +283,56 @@ func testDeletion() {
 		})
 	})
 }
+
+func testDeploymentUninstall() {
+	t := newTestDriver()
+
+	BeforeEach(func() {
+		t.serviceDiscovery.SetFinalizers([]string{constants.CleanupFinalizer})
+
+		now := metav1.Now()
+		t.serviceDiscovery.SetDeletionTimestamp(&now)
+	})
+
+	Context("", func() {
+		BeforeEach(func() {
+			t.InitClientObjs = append(t.InitClientObjs,
+				t.NewDeployment(names.ServiceDiscoveryComponent))
+		})
+
+		It("should run a Deployment to uninstall the service discovery component", func() {
+			t.AssertReconcileRequeue()
+
+			t.AssertNoDeployment(names.ServiceDiscoveryComponent)
+
+			t.UpdateDeploymentToReady(t.assertUninstallServiceDiscoveryDeployment())
+
+			t.awaitFinalizer()
+
+			t.AssertReconcileSuccess()
+
+			t.AssertNoDeployment(names.AppendUninstall(names.ServiceDiscoveryComponent))
+
+			t.awaitNoFinalizer()
+		})
+	})
+
+	When("the version of the deleting ServiceDiscovery instance does not support uninstall", func() {
+		BeforeEach(func() {
+			t.serviceDiscovery.Spec.Version = "0.11.1"
+
+			t.InitClientObjs = append(t.InitClientObjs, t.NewDeployment(names.ServiceDiscoveryComponent))
+		})
+
+		It("should not perform uninstall", func() {
+			t.AssertReconcileSuccess()
+
+			_, err := t.GetDeployment(names.ServiceDiscoveryComponent)
+			Expect(err).To(Succeed())
+
+			t.AssertNoDeployment(names.AppendUninstall(names.ServiceDiscoveryComponent))
+
+			t.awaitNoFinalizer()
+		})
+	})
+}
diff --git a/controllers/servicediscovery/servicediscovery_suite_test.go b/controllers/servicediscovery/servicediscovery_suite_test.go
index bc26183b..b137d9a0 100644
--- a/controllers/servicediscovery/servicediscovery_suite_test.go
+++ b/controllers/servicediscovery/servicediscovery_suite_test.go
@@ -28,19 +28,18 @@ import (
 	. "github.com/onsi/gomega"
 	operatorv1 "github.com/openshift/api/operator/v1"
 	"github.com/submariner-io/admiral/pkg/log/kzerolog"
-	"github.com/submariner-io/admiral/pkg/test"
 	submariner_v1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
 	"github.com/submariner-io/submariner-operator/controllers/constants"
-	"github.com/submariner-io/submariner-operator/controllers/resource"
 	"github.com/submariner-io/submariner-operator/controllers/servicediscovery"
+	"github.com/submariner-io/submariner-operator/controllers/test"
+	"github.com/submariner-io/submariner-operator/pkg/names"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	fakeKubeClient "k8s.io/client-go/kubernetes/fake"
 	"k8s.io/client-go/kubernetes/scheme"
 	controllerClient "sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
 const (
@@ -75,65 +74,61 @@ func TestSubmariner(t *testing.T) {
 }
 
 type testDriver struct {
-	initClientObjs   []controllerClient.Object
-	fakeClient       controllerClient.Client
+	test.Driver
 	kubeClient       *fakeKubeClient.Clientset
 	serviceDiscovery *submariner_v1.ServiceDiscovery
-	controller       *servicediscovery.Reconciler
 }
 
 func newTestDriver() *testDriver {
-	t := &testDriver{}
+	t := &testDriver{
+		Driver: test.Driver{
+			Namespace:    submarinerNamespace,
+			ResourceName: serviceDiscoveryName,
+		},
+	}
 
 	BeforeEach(func() {
-		t.fakeClient = nil
+		t.BeforeEach()
 		t.serviceDiscovery = newServiceDiscovery()
-		t.initClientObjs = []controllerClient.Object{t.serviceDiscovery}
 		t.kubeClient = fakeKubeClient.NewSimpleClientset()
+		t.InitClientObjs = []controllerClient.Object{t.serviceDiscovery}
 	})
 
 	JustBeforeEach(func() {
-		if t.fakeClient == nil {
-			t.fakeClient = t.newClient()
-		}
+		t.JustBeforeEach()
 
-		t.controller = servicediscovery.NewReconciler(&servicediscovery.Config{
-			Client:         t.fakeClient,
+		t.Controller = servicediscovery.NewReconciler(&servicediscovery.Config{
+			Client:         t.Client,
 			Scheme:         scheme.Scheme,
 			KubeClient:     t.kubeClient,
-			OperatorClient: t.fakeClient,
+			OperatorClient: t.Client,
 		})
 	})
 
 	return t
 }
 
-func (t *testDriver) newClient() controllerClient.Client {
-	return fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(t.initClientObjs...).Build()
+func (t *testDriver) awaitFinalizer() {
+	t.AwaitFinalizer(t.serviceDiscovery, constants.CleanupFinalizer)
 }
 
-func (t *testDriver) doReconcile() (reconcile.Result, error) {
-	return t.controller.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{
-		Namespace: submarinerNamespace,
-		Name:      serviceDiscoveryName,
-	}})
+func (t *testDriver) awaitNoFinalizer() {
+	t.AwaitNoFinalizer(t.serviceDiscovery, constants.CleanupFinalizer)
 }
 
-func (t *testDriver) assertReconcileSuccess() {
-	r, err := t.doReconcile()
-	Expect(err).To(Succeed())
-	Expect(r.Requeue).To(BeFalse())
-	Expect(r.RequeueAfter).To(BeNumerically("==", 0))
-}
+func (t *testDriver) assertUninstallServiceDiscoveryDeployment() *appsv1.Deployment {
+	deployment := t.AssertDeployment(names.AppendUninstall(names.ServiceDiscoveryComponent))
+
+	Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1))
+	Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(
+		Equal(fmt.Sprintf("%s/%s:%s", t.serviceDiscovery.Spec.Repository, names.ServiceDiscoveryImage, t.serviceDiscovery.Spec.Version)))
 
-func (t *testDriver) assertReconcileError() {
-	_, err := t.doReconcile()
-	Expect(err).ToNot(Succeed())
+	return deployment
 }
 
 func (t *testDriver) getDNSConfig() (*operatorv1.DNS, error) {
 	foundDNSConfig := &operatorv1.DNS{}
-	err := t.fakeClient.Get(context.TODO(), types.NamespacedName{Name: openShiftDNSConfigName}, foundDNSConfig)
+	err := t.Client.Get(context.TODO(), types.NamespacedName{Name: openShiftDNSConfigName}, foundDNSConfig)
 
 	return foundDNSConfig, err
 }
@@ -244,7 +239,7 @@ func newDNSService(clusterIP string) *corev1.Service {
 
 func (t *testDriver) assertLighthouseCoreDNSService() *corev1.Service {
 	service := &corev1.Service{}
-	Expect(t.fakeClient.Get(context.TODO(), types.NamespacedName{Name: lighthouseDNSServiceName, Namespace: submarinerNamespace},
+	Expect(t.Client.Get(context.TODO(), types.NamespacedName{Name: lighthouseDNSServiceName, Namespace: submarinerNamespace},
 		service)).To(Succeed())
 
 	Expect(service.Labels).To(HaveKeyWithValue("app", lighthouseDNSServiceName))
@@ -259,13 +254,12 @@ func (t *testDriver) assertLighthouseCoreDNSService() *corev1.Service {
 func (t *testDriver) setLighthouseCoreDNSServiceIP() {
 	service := t.assertLighthouseCoreDNSService()
 	service.Spec.ClusterIP = clusterIP
-	Expect(t.fakeClient.Update(context.TODO(), service)).To(Succeed())
+	Expect(t.Client.Update(context.TODO(), service)).To(Succeed())
 }
 
 func (t *testDriver) testFinalizerRemoved() {
 	It("remove the finalizer from the Submariner resource", func() {
-		test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &submariner_v1.ServiceDiscovery{}),
-			serviceDiscoveryName, constants.CleanupFinalizer)
+		t.awaitNoFinalizer()
 	})
 }
 
diff --git a/controllers/submariner/submariner_controller_test.go b/controllers/submariner/submariner_controller_test.go
index 0629f92f..bd637c8a 100644
--- a/controllers/submariner/submariner_controller_test.go
+++ b/controllers/submariner/submariner_controller_test.go
@@ -25,11 +25,10 @@ import (
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
-	"github.com/submariner-io/admiral/pkg/test"
 	operatorv1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
 	"github.com/submariner-io/submariner-operator/controllers/constants"
-	"github.com/submariner-io/submariner-operator/controllers/resource"
-	submarinerController "github.com/submariner-io/submariner-operator/controllers/submariner"
+	"github.com/submariner-io/submariner-operator/controllers/test"
+	"github.com/submariner-io/submariner-operator/controllers/uninstall"
 	"github.com/submariner-io/submariner-operator/pkg/names"
 	routeagent "github.com/submariner-io/submariner/pkg/routeagent_driver/constants"
 	appsv1 "k8s.io/api/apps/v1"
@@ -58,14 +57,13 @@ func testReconciliation() {
 	t := newTestDriver()
 
 	It("should add a finalizer to the Submariner resource", func() {
-		t.assertReconcileSuccess()
-		test.AwaitFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-			submarinerName, constants.CleanupFinalizer)
+		t.AssertReconcileSuccess()
+		t.awaitFinalizer()
 	})
 
 	When("the network details are not provided", func() {
 		It("should use the detected network", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			updated := t.getSubmariner()
 			Expect(updated.Status.ServiceCIDR).To(Equal(testDetectedServiceCIDR))
@@ -75,15 +73,15 @@ func testReconciliation() {
 
 	When("the network details are provided", func() {
 		It("should use the provided ones instead of the detected ones", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			initial := t.getSubmariner()
 			initial.Spec.ServiceCIDR = testConfiguredServiceCIDR
 			initial.Spec.ClusterCIDR = testConfiguredClusterCIDR
 
-			Expect(t.fakeClient.Update(context.TODO(), initial)).To(Succeed())
+			Expect(t.Client.Update(context.TODO(), initial)).To(Succeed())
 
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			updated := t.getSubmariner()
 			Expect(updated.Status.ServiceCIDR).To(Equal(testConfiguredServiceCIDR))
@@ -93,58 +91,58 @@ func testReconciliation() {
 
 	When("the submariner gateway DaemonSet doesn't exist", func() {
 		It("should create it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 			t.assertGatewayDaemonSet()
 		})
 	})
 
 	When("the submariner gateway DaemonSet already exists", func() {
 		BeforeEach(func() {
-			t.initClientObjs = append(t.initClientObjs, t.newDaemonSet(names.GatewayComponent))
+			t.InitClientObjs = append(t.InitClientObjs, t.NewDaemonSet(names.GatewayComponent))
 		})
 
 		It("should update it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			initial := t.getSubmariner()
 			initial.Spec.ServiceCIDR = "101.96.1.0/16"
-			Expect(t.fakeClient.Update(context.TODO(), initial)).To(Succeed())
+			Expect(t.Client.Update(context.TODO(), initial)).To(Succeed())
 
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			updatedDaemonSet := t.assertDaemonSet(names.GatewayComponent)
-			Expect(envMapFrom(updatedDaemonSet)).To(HaveKeyWithValue("SUBMARINER_SERVICECIDR", initial.Spec.ServiceCIDR))
+			updatedDaemonSet := t.AssertDaemonSet(names.GatewayComponent)
+			Expect(test.EnvMapFrom(updatedDaemonSet)).To(HaveKeyWithValue("SUBMARINER_SERVICECIDR", initial.Spec.ServiceCIDR))
 		})
 	})
 
 	When("the submariner route-agent DaemonSet doesn't exist", func() {
 		It("should create it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 			t.assertRouteAgentDaemonSet()
 		})
 	})
 
 	When("the submariner route-agent DaemonSet already exists", func() {
 		BeforeEach(func() {
-			t.initClientObjs = append(t.initClientObjs, t.newDaemonSet(names.RouteAgentComponent))
+			t.InitClientObjs = append(t.InitClientObjs, t.NewDaemonSet(names.RouteAgentComponent))
 		})
 
 		It("should update it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			initial := t.getSubmariner()
 			initial.Spec.ClusterCIDR = "11.245.1.0/16"
-			Expect(t.fakeClient.Update(context.TODO(), initial)).To(Succeed())
+			Expect(t.Client.Update(context.TODO(), initial)).To(Succeed())
 
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			updatedDaemonSet := t.assertDaemonSet(names.RouteAgentComponent)
-			Expect(envMapFrom(updatedDaemonSet)).To(HaveKeyWithValue("SUBMARINER_CLUSTERCIDR", initial.Spec.ClusterCIDR))
+			updatedDaemonSet := t.AssertDaemonSet(names.RouteAgentComponent)
+			Expect(test.EnvMapFrom(updatedDaemonSet)).To(HaveKeyWithValue("SUBMARINER_CLUSTERCIDR", initial.Spec.ClusterCIDR))
 		})
 
 		When("a selected pod has a nil Started field", func() {
 			BeforeEach(func() {
-				t.initClientObjs = append(t.initClientObjs, &corev1.Pod{
+				t.InitClientObjs = append(t.InitClientObjs, &corev1.Pod{
 					ObjectMeta: metav1.ObjectMeta{
 						Namespace: t.submariner.Namespace,
 						Name:      names.RouteAgentComponent + "-pod",
@@ -164,14 +162,14 @@ func testReconciliation() {
 			})
 
 			It("should not crash", func() {
-				t.assertReconcileSuccess()
+				t.AssertReconcileSuccess()
 			})
 		})
 	})
 
 	When("the submariner globalnet DaemonSet doesn't exist", func() {
 		It("should create it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 			t.assertGlobalnetDaemonSet()
 		})
 	})
@@ -182,20 +180,20 @@ func testReconciliation() {
 		})
 
 		It("should create it", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 			t.assertNetworkPluginSyncerDeployment()
 		})
 	})
 
 	When("the Submariner resource doesn't exist", func() {
 		BeforeEach(func() {
-			t.initClientObjs = nil
+			t.InitClientObjs = nil
 		})
 
 		It("should return success without creating any resources", func() {
-			t.assertReconcileSuccess()
-			t.assertNoDaemonSet(names.GatewayComponent)
-			t.assertNoDaemonSet(names.RouteAgentComponent)
+			t.AssertReconcileSuccess()
+			t.AssertNoDaemonSet(names.GatewayComponent)
+			t.AssertNoDaemonSet(names.RouteAgentComponent)
 		})
 	})
 
@@ -206,7 +204,7 @@ func testReconciliation() {
 		})
 
 		It("should update the resource with defaults", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
 			updated := t.getSubmariner()
 			Expect(updated.Spec.Repository).To(Equal(operatorv1.DefaultRepo))
@@ -216,33 +214,33 @@ func testReconciliation() {
 
 	When("DaemonSet creation fails", func() {
 		BeforeEach(func() {
-			t.fakeClient = &failingClient{Client: t.newClient(), onCreate: reflect.TypeOf(&appsv1.DaemonSet{})}
+			t.Client = &test.FailingClient{Client: t.NewClient(), OnCreate: reflect.TypeOf(&appsv1.DaemonSet{})}
 		})
 
 		It("should return an error", func() {
-			_, err := t.doReconcile()
+			_, err := t.DoReconcile()
 			Expect(err).To(HaveOccurred())
 		})
 	})
 
 	When("DaemonSet retrieval fails", func() {
 		BeforeEach(func() {
-			t.fakeClient = &failingClient{Client: t.newClient(), onGet: reflect.TypeOf(&appsv1.DaemonSet{})}
+			t.Client = &test.FailingClient{Client: t.NewClient(), OnGet: reflect.TypeOf(&appsv1.DaemonSet{})}
 		})
 
 		It("should return an error", func() {
-			_, err := t.doReconcile()
+			_, err := t.DoReconcile()
 			Expect(err).To(HaveOccurred())
 		})
 	})
 
 	When("Submariner resource retrieval fails", func() {
 		BeforeEach(func() {
-			t.fakeClient = &failingClient{Client: t.newClient(), onGet: reflect.TypeOf(&operatorv1.Submariner{})}
+			t.Client = &test.FailingClient{Client: t.NewClient(), OnGet: reflect.TypeOf(&operatorv1.Submariner{})}
 		})
 
 		It("should return an error", func() {
-			_, err := t.doReconcile()
+			_, err := t.DoReconcile()
 			Expect(err).To(HaveOccurred())
 		})
 	})
@@ -262,60 +260,58 @@ func testDeletion() {
 		BeforeEach(func() {
 			t.clusterNetwork.NetworkPlugin = routeagent.NetworkPluginOVNKubernetes
 
-			t.initClientObjs = append(t.initClientObjs,
-				t.newDaemonSet(names.GatewayComponent),
-				t.newPodWithLabel("app", names.GatewayComponent),
-				t.newDaemonSet(names.RouteAgentComponent),
-				t.newDaemonSet(names.GlobalnetComponent),
-				t.newDeployment(names.NetworkPluginSyncerComponent))
+			t.InitClientObjs = append(t.InitClientObjs,
+				t.NewDaemonSet(names.GatewayComponent),
+				t.NewPodWithLabel("app", names.GatewayComponent),
+				t.NewDaemonSet(names.RouteAgentComponent),
+				t.NewDaemonSet(names.GlobalnetComponent),
+				t.NewDeployment(names.NetworkPluginSyncerComponent))
 		})
 
 		It("should run DaemonSets/Deployments to uninstall components", func() {
 			// The first reconcile invocation should delete the regular DaemonSets/Deployments.
-			t.assertReconcileRequeue()
+			t.AssertReconcileRequeue()
 
-			t.assertNoDaemonSet(names.GatewayComponent)
-			t.assertNoDaemonSet(names.RouteAgentComponent)
-			t.assertNoDaemonSet(names.GlobalnetComponent)
-			t.assertNoDeployment(names.NetworkPluginSyncerComponent)
+			t.AssertNoDaemonSet(names.GatewayComponent)
+			t.AssertNoDaemonSet(names.RouteAgentComponent)
+			t.AssertNoDaemonSet(names.GlobalnetComponent)
+			t.AssertNoDeployment(names.NetworkPluginSyncerComponent)
 
 			// Simulate the DaemonSet controller cleaning up its pods.
-			t.deletePods("app", names.GatewayComponent)
+			t.DeletePods("app", names.GatewayComponent)
 
 			// Next, the controller should create the corresponding uninstall DaemonSets/Deployments.
-			t.assertReconcileRequeue()
+			t.AssertReconcileRequeue()
 
 			// For the gateway DaemonSet, we'll only update it to nodes available but not yet ready at this point.
 			gatewayDS := t.assertUninstallGatewayDaemonSet()
-			t.updateDaemonSetToScheduled(gatewayDS)
+			t.UpdateDaemonSetToScheduled(gatewayDS)
 
 			// For the globalnet DaemonSet, we'll update it to observed but no nodes available - this will cause it to be deleted.
 			globalnetDS := t.assertUninstallGlobalnetDaemonSet()
-			t.updateDaemonSetToObserved(globalnetDS)
+			t.UpdateDaemonSetToObserved(globalnetDS)
 
-			t.updateDaemonSetToReady(t.assertUninstallRouteAgentDaemonSet())
-			t.updateDeploymentToReady(t.assertUninstallNetworkPluginSyncerDeployment())
+			t.UpdateDaemonSetToReady(t.assertUninstallRouteAgentDaemonSet())
+			t.UpdateDeploymentToReady(t.assertUninstallNetworkPluginSyncerDeployment())
 
 			// Next, the controller should again requeue b/c the gateway DaemonSet isn't ready yet.
-			t.assertReconcileRequeue()
+			t.AssertReconcileRequeue()
 
 			// Now update the gateway DaemonSet to ready.
-			t.updateDaemonSetToReady(gatewayDS)
+			t.UpdateDaemonSetToReady(gatewayDS)
 
 			// Ensure the finalizer is still present.
-			test.AwaitFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-				submarinerName, constants.CleanupFinalizer)
+			t.awaitFinalizer()
 
 			// Finally, the controller should delete the uninstall DaemonSets/Deployments and remove the finalizer.
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			t.assertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
-			t.assertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
-			t.assertNoDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
-			t.assertNoDeployment(names.AppendUninstall(names.NetworkPluginSyncerComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
+			t.AssertNoDeployment(names.AppendUninstall(names.NetworkPluginSyncerComponent))
 
-			test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-				submarinerName, constants.CleanupFinalizer)
+			t.awaitNoFinalizer()
 		})
 	})
 
@@ -323,27 +319,26 @@ func testDeletion() {
 		BeforeEach(func() {
 			t.submariner.Spec.GlobalCIDR = ""
 
-			t.initClientObjs = append(t.initClientObjs,
-				t.newDaemonSet(names.GatewayComponent),
-				t.newDaemonSet(names.RouteAgentComponent))
+			t.InitClientObjs = append(t.InitClientObjs,
+				t.NewDaemonSet(names.GatewayComponent),
+				t.NewDaemonSet(names.RouteAgentComponent))
 		})
 
 		It("should only create uninstall DaemonSets/Deployments for installed components", func() {
-			t.assertReconcileRequeue()
+			t.AssertReconcileRequeue()
 
-			t.updateDaemonSetToReady(t.assertUninstallGatewayDaemonSet())
-			t.updateDaemonSetToReady(t.assertUninstallRouteAgentDaemonSet())
+			t.UpdateDaemonSetToReady(t.assertUninstallGatewayDaemonSet())
+			t.UpdateDaemonSetToReady(t.assertUninstallRouteAgentDaemonSet())
 
-			t.assertNoDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
-			t.assertNoDaemonSet(names.AppendUninstall(names.NetworkPluginSyncerComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.NetworkPluginSyncerComponent))
 
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			t.assertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
-			t.assertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
 
-			test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-				submarinerName, constants.CleanupFinalizer)
+			t.awaitNoFinalizer()
 		})
 	})
 
@@ -353,22 +348,21 @@ func testDeletion() {
 		})
 
 		It("should delete it", func() {
-			t.assertReconcileRequeue()
+			t.AssertReconcileRequeue()
 
-			t.updateDaemonSetToReady(t.assertUninstallGatewayDaemonSet())
-			t.updateDaemonSetToScheduled(t.assertUninstallRouteAgentDaemonSet())
+			t.UpdateDaemonSetToReady(t.assertUninstallGatewayDaemonSet())
+			t.UpdateDaemonSetToScheduled(t.assertUninstallRouteAgentDaemonSet())
 
-			ts := metav1.NewTime(time.Now().Add(-(submarinerController.UninstallComponentReadyTimeout + 10)))
+			ts := metav1.NewTime(time.Now().Add(-(uninstall.ComponentReadyTimeout + 10)))
 			t.submariner.SetDeletionTimestamp(&ts)
-			Expect(t.fakeClient.Update(context.TODO(), t.submariner)).To(Succeed())
+			Expect(t.Client.Update(context.TODO(), t.submariner)).To(Succeed())
 
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			t.assertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
-			t.assertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
 
-			test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-				submarinerName, constants.CleanupFinalizer)
+			t.awaitNoFinalizer()
 		})
 	})
 
@@ -376,20 +370,19 @@ func testDeletion() {
 		BeforeEach(func() {
 			t.submariner.Spec.Version = "0.11.1"
 
-			t.initClientObjs = append(t.initClientObjs,
-				t.newDaemonSet(names.GatewayComponent))
+			t.InitClientObjs = append(t.InitClientObjs,
+				t.NewDaemonSet(names.GatewayComponent))
 		})
 
 		It("should not perform uninstall", func() {
-			t.assertReconcileSuccess()
+			t.AssertReconcileSuccess()
 
-			_, err := t.getDaemonSet(names.GatewayComponent)
+			_, err := t.GetDaemonSet(names.GatewayComponent)
 			Expect(err).To(Succeed())
 
-			t.assertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
+			t.AssertNoDaemonSet(names.AppendUninstall(names.GatewayComponent))
 
-			test.AwaitNoFinalizer(resource.ForControllerClient(t.fakeClient, submarinerNamespace, &operatorv1.Submariner{}),
-				submarinerName, constants.CleanupFinalizer)
+			t.awaitNoFinalizer()
 		})
 	})
 }
diff --git a/controllers/submariner/submariner_suite_test.go b/controllers/submariner/submariner_suite_test.go
index 628a1a95..ddab6605 100644
--- a/controllers/submariner/submariner_suite_test.go
+++ b/controllers/submariner/submariner_suite_test.go
@@ -21,7 +21,6 @@ package submariner_test
 import (
 	"context"
 	"fmt"
-	"reflect"
 	"strconv"
 	"testing"
 
@@ -30,22 +29,19 @@ import (
 	"github.com/submariner-io/admiral/pkg/log/kzerolog"
 	"github.com/submariner-io/admiral/pkg/syncer/broker"
 	operatorv1 "github.com/submariner-io/submariner-operator/api/submariner/v1alpha1"
+	"github.com/submariner-io/submariner-operator/controllers/constants"
 	submarinerController "github.com/submariner-io/submariner-operator/controllers/submariner"
+	"github.com/submariner-io/submariner-operator/controllers/test"
 	"github.com/submariner-io/submariner-operator/pkg/discovery/network"
 	"github.com/submariner-io/submariner-operator/pkg/names"
 	submarinerv1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/uuid"
 	"k8s.io/client-go/kubernetes/scheme"
 	controllerClient "sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
-	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
 var _ = BeforeSuite(func() {
@@ -63,52 +59,24 @@ func TestSubmariner(t *testing.T) {
 	RunSpecs(t, "Submariner Suite")
 }
 
-type failingClient struct {
-	controllerClient.Client
-	onCreate reflect.Type
-	onGet    reflect.Type
-	onUpdate reflect.Type
-}
-
-func (c *failingClient) Create(ctx context.Context, obj controllerClient.Object, opts ...controllerClient.CreateOption) error {
-	if c.onCreate == reflect.TypeOf(obj) {
-		return fmt.Errorf("Mock Create error")
-	}
-
-	return c.Client.Create(ctx, obj, opts...)
-}
-
-func (c *failingClient) Get(ctx context.Context, key controllerClient.ObjectKey, obj controllerClient.Object) error {
-	if c.onGet == reflect.TypeOf(obj) {
-		return fmt.Errorf("Mock Get error")
-	}
-
-	return c.Client.Get(ctx, key, obj)
-}
-
-func (c *failingClient) Update(ctx context.Context, obj controllerClient.Object, opts ...controllerClient.UpdateOption) error {
-	if c.onUpdate == reflect.TypeOf(obj) {
-		return fmt.Errorf("Mock Update error")
-	}
-
-	return c.Client.Update(ctx, obj, opts...)
-}
-
 type testDriver struct {
-	initClientObjs []controllerClient.Object
-	fakeClient     controllerClient.Client
+	test.Driver
 	submariner     *operatorv1.Submariner
-	controller     *submarinerController.Reconciler
 	clusterNetwork *network.ClusterNetwork
 }
 
 func newTestDriver() *testDriver {
-	t := &testDriver{}
+	t := &testDriver{
+		Driver: test.Driver{
+			Namespace:    submarinerNamespace,
+			ResourceName: submarinerName,
+		},
+	}
 
 	BeforeEach(func() {
-		t.fakeClient = nil
+		t.BeforeEach()
 		t.submariner = newSubmariner()
-		t.initClientObjs = []controllerClient.Object{t.submariner}
+		t.InitClientObjs = []controllerClient.Object{t.submariner}
 
 		t.clusterNetwork = &network.ClusterNetwork{
 			NetworkPlugin: "fake",
@@ -118,12 +86,10 @@ func newTestDriver() *testDriver {
 	})
 
 	JustBeforeEach(func() {
-		if t.fakeClient == nil {
-			t.fakeClient = t.newClient()
-		}
+		t.JustBeforeEach()
 
-		t.controller = submarinerController.NewReconciler(&submarinerController.Config{
-			Client:         t.fakeClient,
+		t.Controller = submarinerController.NewReconciler(&submarinerController.Config{
+			Client:         t.Client,
 			Scheme:         scheme.Scheme,
 			ClusterNetwork: t.clusterNetwork,
 		})
@@ -132,41 +98,24 @@ func newTestDriver() *testDriver {
 	return t
 }
 
-func (t *testDriver) newClient() controllerClient.Client {
-	return fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(t.initClientObjs...).Build()
-}
-
-func (t *testDriver) doReconcile() (reconcile.Result, error) {
-	return t.controller.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{
-		Namespace: submarinerNamespace,
-		Name:      submarinerName,
-	}})
-}
-
-func (t *testDriver) assertReconcileSuccess() {
-	r, err := t.doReconcile()
-	Expect(err).To(Succeed())
-	Expect(r.Requeue).To(BeFalse())
-	Expect(r.RequeueAfter).To(BeNumerically("==", 0))
+func (t *testDriver) awaitFinalizer() {
+	t.AwaitFinalizer(t.submariner, constants.CleanupFinalizer)
 }
 
-func (t *testDriver) assertReconcileRequeue() {
-	r, err := t.doReconcile()
-	Expect(err).To(Succeed())
-	Expect(r.RequeueAfter).To(BeNumerically(">", 0), "Expected requeue after")
-	Expect(r.Requeue).To(BeFalse())
+func (t *testDriver) awaitNoFinalizer() {
+	t.AwaitNoFinalizer(t.submariner, constants.CleanupFinalizer)
 }
 
 func (t *testDriver) getSubmariner() *operatorv1.Submariner {
 	obj := &operatorv1.Submariner{}
-	err := t.fakeClient.Get(context.TODO(), types.NamespacedName{Name: submarinerName, Namespace: submarinerNamespace}, obj)
+	err := t.Client.Get(context.TODO(), types.NamespacedName{Name: submarinerName, Namespace: submarinerNamespace}, obj)
 	Expect(err).To(Succeed())
 
 	return obj
 }
 
 func (t *testDriver) assertRouteAgentDaemonSet() {
-	daemonSet := t.assertDaemonSet(names.RouteAgentComponent)
+	daemonSet := t.AssertDaemonSet(names.RouteAgentComponent)
 
 	Expect(daemonSet.Spec.Template.Spec.Containers).To(HaveLen(1))
 	Expect(daemonSet.Spec.Template.Spec.Containers[0].Image).To(
@@ -176,7 +125,7 @@ func (t *testDriver) assertRouteAgentDaemonSet() {
 }
 
 func (t *testDriver) assertUninstallRouteAgentDaemonSet() *appsv1.DaemonSet {
-	daemonSet := t.assertDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
+	daemonSet := t.AssertDaemonSet(names.AppendUninstall(names.RouteAgentComponent))
 
 	Expect(daemonSet.Spec.Template.Spec.InitContainers).To(HaveLen(1))
 	Expect(daemonSet.Spec.Template.Spec.InitContainers[0].Image).To(
@@ -189,7 +138,7 @@ func (t *testDriver) assertUninstallRouteAgentDaemonSet() *appsv1.DaemonSet {
 }
 
 func (t *testDriver) assertRouteAgentDaemonSetEnv(submariner *operatorv1.Submariner, env []corev1.EnvVar) map[string]string {
-	envMap := envMapFromVars(env)
+	envMap := test.EnvMapFromVars(env)
 
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_NAMESPACE", submariner.Spec.Namespace))
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_CLUSTERID", submariner.Spec.ClusterID))
@@ -202,7 +151,7 @@ func (t *testDriver) assertRouteAgentDaemonSetEnv(submariner *operatorv1.Submari
 }
 
 func (t *testDriver) assertGatewayDaemonSet() {
-	daemonSet := t.assertDaemonSet(names.GatewayComponent)
+	daemonSet := t.AssertDaemonSet(names.GatewayComponent)
 	assertGatewayNodeSelector(daemonSet)
 
 	Expect(daemonSet.Spec.Template.Spec.Containers).To(HaveLen(1))
@@ -213,7 +162,7 @@ func (t *testDriver) assertGatewayDaemonSet() {
 }
 
 func (t *testDriver) assertUninstallGatewayDaemonSet() *appsv1.DaemonSet {
-	daemonSet := t.assertDaemonSet(names.AppendUninstall(names.GatewayComponent))
+	daemonSet := t.AssertDaemonSet(names.AppendUninstall(names.GatewayComponent))
 	assertGatewayNodeSelector(daemonSet)
 
 	Expect(daemonSet.Spec.Template.Spec.InitContainers).To(HaveLen(1))
@@ -227,7 +176,7 @@ func (t *testDriver) assertUninstallGatewayDaemonSet() *appsv1.DaemonSet {
 }
 
 func (t *testDriver) assertGatewayDaemonSetEnv(submariner *operatorv1.Submariner, env []corev1.EnvVar) map[string]string {
-	envMap := envMapFromVars(env)
+	envMap := test.EnvMapFromVars(env)
 
 	Expect(envMap).To(HaveKeyWithValue("CE_IPSEC_PSK", submariner.Spec.CeIPSecPSK))
 	Expect(envMap).To(HaveKeyWithValue("CE_IPSEC_IKEPORT", strconv.Itoa(submariner.Spec.CeIPSecIKEPort)))
@@ -252,7 +201,7 @@ func (t *testDriver) assertGatewayDaemonSetEnv(submariner *operatorv1.Submariner
 }
 
 func (t *testDriver) assertGlobalnetDaemonSet() {
-	daemonSet := t.assertDaemonSet(names.GlobalnetComponent)
+	daemonSet := t.AssertDaemonSet(names.GlobalnetComponent)
 	assertGatewayNodeSelector(daemonSet)
 
 	Expect(daemonSet.Spec.Template.Spec.Containers).To(HaveLen(1))
@@ -263,7 +212,7 @@ func (t *testDriver) assertGlobalnetDaemonSet() {
 }
 
 func (t *testDriver) assertUninstallGlobalnetDaemonSet() *appsv1.DaemonSet {
-	daemonSet := t.assertDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
+	daemonSet := t.AssertDaemonSet(names.AppendUninstall(names.GlobalnetComponent))
 	assertGatewayNodeSelector(daemonSet)
 
 	Expect(daemonSet.Spec.Template.Spec.InitContainers).To(HaveLen(1))
@@ -277,7 +226,7 @@ func (t *testDriver) assertUninstallGlobalnetDaemonSet() *appsv1.DaemonSet {
 }
 
 func (t *testDriver) assertGlobalnetDaemonSetEnv(submariner *operatorv1.Submariner, env []corev1.EnvVar) map[string]string {
-	envMap := envMapFromVars(env)
+	envMap := test.EnvMapFromVars(env)
 
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_NAMESPACE", submariner.Spec.Namespace))
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_CLUSTERID", submariner.Spec.ClusterID))
@@ -286,7 +235,7 @@ func (t *testDriver) assertGlobalnetDaemonSetEnv(submariner *operatorv1.Submarin
 }
 
 func (t *testDriver) assertNetworkPluginSyncerDeployment() {
-	deployment := t.assertDeployment(names.NetworkPluginSyncerComponent)
+	deployment := t.AssertDeployment(names.NetworkPluginSyncerComponent)
 
 	Expect(deployment.Spec.Template.Spec.Containers).To(HaveLen(1))
 	Expect(deployment.Spec.Template.Spec.Containers[0].Image).To(
@@ -296,7 +245,7 @@ func (t *testDriver) assertNetworkPluginSyncerDeployment() {
 }
 
 func (t *testDriver) assertUninstallNetworkPluginSyncerDeployment() *appsv1.Deployment {
-	deployment := t.assertDeployment(names.AppendUninstall(names.NetworkPluginSyncerComponent))
+	deployment := t.AssertDeployment(names.AppendUninstall(names.NetworkPluginSyncerComponent))
 
 	Expect(deployment.Spec.Template.Spec.InitContainers).To(HaveLen(1))
 	Expect(deployment.Spec.Template.Spec.InitContainers[0].Image).To(
@@ -309,7 +258,7 @@ func (t *testDriver) assertUninstallNetworkPluginSyncerDeployment() *appsv1.Depl
 }
 
 func (t *testDriver) assertNetworkPluginSyncerDeploymentEnv(submariner *operatorv1.Submariner, env []corev1.EnvVar) map[string]string {
-	envMap := envMapFromVars(env)
+	envMap := test.EnvMapFromVars(env)
 
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_NAMESPACE", submariner.Spec.Namespace))
 	Expect(envMap).To(HaveKeyWithValue("SUBMARINER_CLUSTERID", submariner.Spec.ClusterID))
@@ -322,87 +271,10 @@ func (t *testDriver) assertNetworkPluginSyncerDeploymentEnv(submariner *operator
 	return envMap
 }
 
-func (t *testDriver) getDaemonSet(name string) (*appsv1.DaemonSet, error) {
-	foundDaemonSet := &appsv1.DaemonSet{}
-	err := t.fakeClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: submarinerNamespace}, foundDaemonSet)
-
-	return foundDaemonSet, err
-}
-
-func (t *testDriver) assertDaemonSet(name string) *appsv1.DaemonSet {
-	daemonSet, err := t.getDaemonSet(name)
-	Expect(err).To(Succeed())
-
-	Expect(daemonSet.ObjectMeta.Labels).To(HaveKeyWithValue("app", name))
-	Expect(daemonSet.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", name))
-
-	for k, v := range daemonSet.Spec.Selector.MatchLabels {
-		Expect(daemonSet.Spec.Template.ObjectMeta.Labels).To(HaveKeyWithValue(k, v))
-	}
-
-	return daemonSet
-}
-
-func (t *testDriver) assertNoDaemonSet(name string) {
-	_, err := t.getDaemonSet(name)
-	Expect(errors.IsNotFound(err)).To(BeTrue(), "IsNotFound error")
-	Expect(err).To(HaveOccurred())
-}
-
-func (t *testDriver) getDeployment(name string) (*appsv1.Deployment, error) {
-	found := &appsv1.Deployment{}
-	err := t.fakeClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: submarinerNamespace}, found)
-
-	return found, err
-}
-
-func (t *testDriver) assertDeployment(name string) *appsv1.Deployment {
-	deployment, err := t.getDeployment(name)
-	Expect(err).To(Succeed())
-
-	Expect(deployment.ObjectMeta.Labels).To(HaveKeyWithValue("app", name))
-	Expect(deployment.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", name))
-	Expect(deployment.Spec.Replicas).ToNot(BeNil())
-	Expect(int(*deployment.Spec.Replicas)).To(Equal(1))
-
-	return deployment
-}
-
-func (t *testDriver) assertNoDeployment(name string) {
-	_, err := t.getDeployment(name)
-	Expect(errors.IsNotFound(err)).To(BeTrue(), "IsNotFound error")
-	Expect(err).To(HaveOccurred())
-}
-
 func assertGatewayNodeSelector(daemonSet *appsv1.DaemonSet) {
 	Expect(daemonSet.Spec.Template.Spec.NodeSelector["submariner.io/gateway"]).To(Equal("true"))
 }
 
-func (t *testDriver) updateDaemonSetToReady(daemonSet *appsv1.DaemonSet) {
-	t.updateDaemonSetToScheduled(daemonSet)
-	daemonSet.Status.NumberReady = daemonSet.Status.DesiredNumberScheduled
-	Expect(t.fakeClient.Update(context.TODO(), daemonSet)).To(Succeed())
-}
-
-func (t *testDriver) updateDaemonSetToObserved(daemonSet *appsv1.DaemonSet) {
-	daemonSet.Generation = 1
-	daemonSet.Status.ObservedGeneration = daemonSet.Generation
-	Expect(t.fakeClient.Update(context.TODO(), daemonSet)).To(Succeed())
-}
-
-func (t *testDriver) updateDaemonSetToScheduled(daemonSet *appsv1.DaemonSet) {
-	daemonSet.Generation = 1
-	daemonSet.Status.ObservedGeneration = daemonSet.Generation
-	daemonSet.Status.DesiredNumberScheduled = 1
-	Expect(t.fakeClient.Update(context.TODO(), daemonSet)).To(Succeed())
-}
-
-func (t *testDriver) updateDeploymentToReady(deployment *appsv1.Deployment) {
-	deployment.Status.ReadyReplicas = *deployment.Spec.Replicas
-	deployment.Status.AvailableReplicas = *deployment.Spec.Replicas
-	Expect(t.fakeClient.Update(context.TODO(), deployment)).To(Succeed())
-}
-
 func (t *testDriver) withNetworkDiscovery() *operatorv1.Submariner {
 	t.submariner.Status.ClusterCIDR = getClusterCIDR(t.submariner, t.clusterNetwork)
 	t.submariner.Status.ServiceCIDR = getServiceCIDR(t.submariner, t.clusterNetwork)
@@ -412,55 +284,6 @@ func (t *testDriver) withNetworkDiscovery() *operatorv1.Submariner {
 	return t.submariner
 }
 
-func (t *testDriver) newDaemonSet(name string) *appsv1.DaemonSet {
-	return &appsv1.DaemonSet{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: t.submariner.Namespace,
-			Name:      name,
-		},
-	}
-}
-
-func (t *testDriver) newDeployment(name string) *appsv1.Deployment {
-	return &appsv1.Deployment{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: t.submariner.Namespace,
-			Name:      name,
-		},
-	}
-}
-
-func (t *testDriver) newPodWithLabel(label, value string) *corev1.Pod {
-	return &corev1.Pod{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: t.submariner.Namespace,
-			Name:      string(uuid.NewUUID()),
-			Labels: map[string]string{
-				label: value,
-			},
-		},
-	}
-}
-
-func (t *testDriver) deletePods(label, value string) {
-	err := t.fakeClient.DeleteAllOf(context.TODO(), &corev1.Pod{}, controllerClient.InNamespace(t.submariner.Namespace),
-		controllerClient.MatchingLabelsSelector{Selector: labels.SelectorFromSet(map[string]string{label: value})})
-	Expect(err).To(Succeed())
-}
-
-func envMapFrom(daemonSet *appsv1.DaemonSet) map[string]string {
-	return envMapFromVars(daemonSet.Spec.Template.Spec.Containers[0].Env)
-}
-
-func envMapFromVars(env []corev1.EnvVar) map[string]string {
-	envMap := map[string]string{}
-	for _, envVar := range env {
-		envMap[envVar.Name] = envVar.Value
-	}
-
-	return envMap
-}
-
 func newSubmariner() *operatorv1.Submariner {
 	return &operatorv1.Submariner{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/controllers/test/driver.go b/controllers/test/driver.go
new file mode 100644
index 00000000..c7c97ac3
--- /dev/null
+++ b/controllers/test/driver.go
@@ -0,0 +1,222 @@
+/*
+SPDX-License-Identifier: Apache-2.0
+
+Copyright Contributors to the Submariner project.
+
+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 test
+
+import (
+	"context"
+
+	. "github.com/onsi/gomega"
+	admtest "github.com/submariner-io/admiral/pkg/test"
+	"github.com/submariner-io/submariner-operator/controllers/resource"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/uuid"
+	"k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+type Driver struct {
+	InitClientObjs []client.Object
+	Client         client.Client
+	Controller     reconcile.Reconciler
+	Namespace      string
+	ResourceName   string
+}
+
+func (d *Driver) BeforeEach() {
+	d.Client = nil
+	d.InitClientObjs = []client.Object{}
+	d.Controller = nil
+}
+
+func (d *Driver) JustBeforeEach() {
+	if d.Client == nil {
+		d.Client = d.NewClient()
+	}
+}
+
+func (d *Driver) NewClient() client.Client {
+	return fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(d.InitClientObjs...).Build()
+}
+
+func (d *Driver) DoReconcile() (reconcile.Result, error) {
+	return d.Controller.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{
+		Namespace: d.Namespace,
+		Name:      d.ResourceName,
+	}})
+}
+
+func (d *Driver) AssertReconcileSuccess() {
+	r, err := d.DoReconcile()
+	Expect(err).To(Succeed())
+	Expect(r.Requeue).To(BeFalse())
+	Expect(r.RequeueAfter).To(BeNumerically("==", 0))
+}
+
+func (d *Driver) AssertReconcileRequeue() {
+	r, err := d.DoReconcile()
+	Expect(err).To(Succeed())
+	Expect(r.RequeueAfter).To(BeNumerically(">", 0), "Expected requeue after")
+	Expect(r.Requeue).To(BeFalse())
+}
+
+func (d *Driver) AssertReconcileError() {
+	_, err := d.DoReconcile()
+	Expect(err).ToNot(Succeed())
+}
+
+func (d *Driver) GetDaemonSet(name string) (*appsv1.DaemonSet, error) {
+	foundDaemonSet := &appsv1.DaemonSet{}
+	err := d.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: d.Namespace}, foundDaemonSet)
+
+	return foundDaemonSet, err
+}
+
+func (d *Driver) AssertDaemonSet(name string) *appsv1.DaemonSet {
+	daemonSet, err := d.GetDaemonSet(name)
+	Expect(err).To(Succeed())
+
+	Expect(daemonSet.ObjectMeta.Labels).To(HaveKeyWithValue("app", name))
+	Expect(daemonSet.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", name))
+
+	for k, v := range daemonSet.Spec.Selector.MatchLabels {
+		Expect(daemonSet.Spec.Template.ObjectMeta.Labels).To(HaveKeyWithValue(k, v))
+	}
+
+	return daemonSet
+}
+
+func (d *Driver) AssertNoDaemonSet(name string) {
+	_, err := d.GetDaemonSet(name)
+	Expect(errors.IsNotFound(err)).To(BeTrue(), "IsNotFound error")
+	Expect(err).To(HaveOccurred())
+}
+
+func (d *Driver) GetDeployment(name string) (*appsv1.Deployment, error) {
+	found := &appsv1.Deployment{}
+	err := d.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: d.Namespace}, found)
+
+	return found, err
+}
+
+func (d *Driver) AssertDeployment(name string) *appsv1.Deployment {
+	deployment, err := d.GetDeployment(name)
+	Expect(err).To(Succeed())
+
+	Expect(deployment.ObjectMeta.Labels).To(HaveKeyWithValue("app", name))
+	Expect(deployment.Spec.Selector.MatchLabels).To(HaveKeyWithValue("app", name))
+	Expect(deployment.Spec.Replicas).ToNot(BeNil())
+	Expect(int(*deployment.Spec.Replicas)).To(Equal(1))
+
+	return deployment
+}
+
+func (d *Driver) AssertNoDeployment(name string) {
+	_, err := d.GetDeployment(name)
+	Expect(errors.IsNotFound(err)).To(BeTrue(), "IsNotFound error")
+	Expect(err).To(HaveOccurred())
+}
+
+func (d *Driver) UpdateDaemonSetToReady(daemonSet *appsv1.DaemonSet) {
+	d.UpdateDaemonSetToScheduled(daemonSet)
+	daemonSet.Status.NumberReady = daemonSet.Status.DesiredNumberScheduled
+	Expect(d.Client.Update(context.TODO(), daemonSet)).To(Succeed())
+}
+
+func (d *Driver) UpdateDaemonSetToObserved(daemonSet *appsv1.DaemonSet) {
+	daemonSet.Generation = 1
+	daemonSet.Status.ObservedGeneration = daemonSet.Generation
+	Expect(d.Client.Update(context.TODO(), daemonSet)).To(Succeed())
+}
+
+func (d *Driver) UpdateDaemonSetToScheduled(daemonSet *appsv1.DaemonSet) {
+	daemonSet.Generation = 1
+	daemonSet.Status.ObservedGeneration = daemonSet.Generation
+	daemonSet.Status.DesiredNumberScheduled = 1
+	Expect(d.Client.Update(context.TODO(), daemonSet)).To(Succeed())
+}
+
+func (d *Driver) UpdateDeploymentToReady(deployment *appsv1.Deployment) {
+	deployment.Status.ReadyReplicas = *deployment.Spec.Replicas
+	deployment.Status.AvailableReplicas = *deployment.Spec.Replicas
+	Expect(d.Client.Update(context.TODO(), deployment)).To(Succeed())
+}
+
+func (d *Driver) NewDaemonSet(name string) *appsv1.DaemonSet {
+	return &appsv1.DaemonSet{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: d.Namespace,
+			Name:      name,
+		},
+	}
+}
+
+func (d *Driver) NewDeployment(name string) *appsv1.Deployment {
+	return &appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: d.Namespace,
+			Name:      name,
+		},
+	}
+}
+
+func (d *Driver) NewPodWithLabel(label, value string) *corev1.Pod {
+	return &corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: d.Namespace,
+			Name:      string(uuid.NewUUID()),
+			Labels: map[string]string{
+				label: value,
+			},
+		},
+	}
+}
+
+func (d *Driver) DeletePods(label, value string) {
+	err := d.Client.DeleteAllOf(context.TODO(), &corev1.Pod{}, client.InNamespace(d.Namespace),
+		client.MatchingLabelsSelector{Selector: labels.SelectorFromSet(map[string]string{label: value})})
+	Expect(err).To(Succeed())
+}
+
+func (d *Driver) AwaitFinalizer(obj client.Object, finalizer string) {
+	admtest.AwaitFinalizer(resource.ForControllerClient(d.Client, d.Namespace, obj), obj.GetName(), finalizer)
+}
+
+func (d *Driver) AwaitNoFinalizer(obj client.Object, finalizer string) {
+	admtest.AwaitNoFinalizer(resource.ForControllerClient(d.Client, d.Namespace, obj), obj.GetName(), finalizer)
+}
+
+func EnvMapFrom(daemonSet *appsv1.DaemonSet) map[string]string {
+	return EnvMapFromVars(daemonSet.Spec.Template.Spec.Containers[0].Env)
+}
+
+func EnvMapFromVars(env []corev1.EnvVar) map[string]string {
+	envMap := map[string]string{}
+	for _, envVar := range env {
+		envMap[envVar.Name] = envVar.Value
+	}
+
+	return envMap
+}
diff --git a/controllers/test/failing_client.go b/controllers/test/failing_client.go
new file mode 100644
index 00000000..87e4bbcd
--- /dev/null
+++ b/controllers/test/failing_client.go
@@ -0,0 +1,58 @@
+/*
+SPDX-License-Identifier: Apache-2.0
+
+Copyright Contributors to the Submariner project.
+
+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 test
+
+import (
+	"context"
+	"errors"
+	"reflect"
+
+	controllerClient "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type FailingClient struct {
+	controllerClient.Client
+	OnCreate reflect.Type
+	OnGet    reflect.Type
+	OnUpdate reflect.Type
+}
+
+func (c *FailingClient) Create(ctx context.Context, obj controllerClient.Object, opts ...controllerClient.CreateOption) error {
+	if c.OnCreate == reflect.TypeOf(obj) {
+		return errors.New("mock Create error")
+	}
+
+	return c.Client.Create(ctx, obj, opts...)
+}
+
+func (c *FailingClient) Get(ctx context.Context, key controllerClient.ObjectKey, obj controllerClient.Object) error {
+	if c.OnGet == reflect.TypeOf(obj) {
+		return errors.New("mock Get error")
+	}
+
+	return c.Client.Get(ctx, key, obj)
+}
+
+func (c *FailingClient) Update(ctx context.Context, obj controllerClient.Object, opts ...controllerClient.UpdateOption) error {
+	if c.OnUpdate == reflect.TypeOf(obj) {
+		return errors.New("mock Update error")
+	}
+
+	return c.Client.Update(ctx, obj, opts...)
+}