Repository: kubernetes-sigs/security-profiles-operator
Pull request #444 (2021-05-11 09:16:04)

Add manager controllers to `Controller` interface
#### What type of PR is this?

/kind cleanup


#### What this PR does / why we need it:
This cleanup adds the manager controller to the `Controller` interface
and reuses its init methods. Some code parts from `main.go` now move
into the actual controller to keep types private.

#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->
None
#### Does this PR have test?
None
<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:
None
#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
None
```


cmurphy: /lgtm

Patch:
diff --git a/cmd/security-profiles-operator/main.go b/cmd/security-profiles-operator/main.go
index 7d2a90b47..7e8525ff1 100644
--- a/cmd/security-profiles-operator/main.go
+++ b/cmd/security-profiles-operator/main.go
@@ -25,13 +25,9 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/urfave/cli/v2"
-	corev1 "k8s.io/api/core/v1"
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/klog/v2/klogr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 
 	profilebindingv1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilebinding/v1alpha1"
@@ -39,7 +35,6 @@ import (
 	seccompprofilev1alpha1 "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
 	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
 	selinuxprofilev1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha1"
-	spodv1alpha1 "sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/controller"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/enricher"
@@ -49,7 +44,7 @@ import (
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/selinuxprofile"
 	nodestatus "sigs.k8s.io/security-profiles-operator/internal/pkg/manager/nodestatus"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/spod"
-	wa "sigs.k8s.io/security-profiles-operator/internal/pkg/manager/workloadannotator"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/workloadannotator"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/nonrootenabler"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/version"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/webhooks/binding"
@@ -59,7 +54,6 @@ import (
 const (
 	jsonFlag           string = "json"
 	selinuxFlag        string = "with-selinux"
-	selinuxdImageKey   string = "RELATED_IMAGE_SELINUXD"
 	defaultWebhookPort int    = 9443
 )
 
@@ -186,11 +180,6 @@ func runManager(ctx *cli.Context) error {
 		LeaderElectionID: "security-profiles-operator-lock",
 	}
 
-	dt, err := getTunables()
-	if err != nil {
-		return err
-	}
-
 	setControllerOptionsForNamespaces(&ctrlOpts)
 
 	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
@@ -198,9 +187,6 @@ func runManager(ctx *cli.Context) error {
 		return errors.Wrap(err, "create cluster manager")
 	}
 
-	if err := spodv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
-		return errors.Wrap(err, "add SPOD API to scheme")
-	}
 	if err := profilebindingv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
 		return errors.Wrap(err, "add profilebinding API to scheme")
 	}
@@ -211,25 +197,12 @@ func runManager(ctx *cli.Context) error {
 		return errors.Wrap(err, "add selinuxprofile API to scheme")
 	}
 
-	// This API provides status which is used by both seccomp and selinux
-	if err := secprofnodestatusv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
-		return errors.Wrap(err, "add per-node Status API to scheme")
-	}
-
-	if err := createConfigIfNotExist(ctx.Context, mgr.GetClient()); err != nil {
-		return errors.Wrap(err, "ensure SPOD config")
-	}
-
-	if err := spod.Setup(ctx.Context, mgr, &dt, ctrl.Log.WithName("spod-config")); err != nil {
-		return errors.Wrap(err, "setup profile controller")
-	}
-
-	if err := nodestatus.Setup(ctx.Context, mgr, ctrl.Log.WithName("nodestatus")); err != nil {
-		return errors.Wrap(err, "setup node status controller")
-	}
-
-	if err := wa.Setup(ctx.Context, mgr, ctrl.Log.WithName("workload-annotator")); err != nil {
-		return errors.Wrap(err, "setup workload annotator controller")
+	if err := setupEnabledControllers(ctx.Context, []controller.Controller{
+		nodestatus.NewController(),
+		spod.NewController(),
+		workloadannotator.NewController(),
+	}, mgr, nil); err != nil {
+		return errors.Wrap(err, "enable controllers")
 	}
 
 	setupLog.Info("starting manager")
@@ -241,41 +214,6 @@ func runManager(ctx *cli.Context) error {
 	return nil
 }
 
-func createConfigIfNotExist(ctx context.Context, c client.Client) error {
-	obj := &spodv1alpha1.SecurityProfilesOperatorDaemon{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "spod",
-			Namespace: config.GetOperatorNamespace(),
-			Labels:    map[string]string{"app": config.OperatorName},
-		},
-		Spec: spodv1alpha1.SPODSpec{
-			EnableSelinux:     false,
-			EnableLogEnricher: false,
-			Tolerations: []corev1.Toleration{
-				{
-					Key:      "node-role.kubernetes.io/master",
-					Operator: corev1.TolerationOpExists,
-					Effect:   corev1.TaintEffectNoSchedule,
-				},
-				{
-					Key:      "node-role.kubernetes.io/control-plane",
-					Operator: corev1.TolerationOpExists,
-					Effect:   corev1.TaintEffectNoSchedule,
-				},
-				{
-					Key:      "node.kubernetes.io/not-ready",
-					Operator: corev1.TolerationOpExists,
-					Effect:   corev1.TaintEffectNoExecute,
-				},
-			},
-		},
-	}
-	if err := c.Create(ctx, obj); !k8serrors.IsAlreadyExists(err) {
-		return errors.Wrap(err, "create SecurityProfilesOperatorDaemon object")
-	}
-	return nil
-}
-
 func setControllerOptionsForNamespaces(opts *ctrl.Options) {
 	namespace := os.Getenv(config.RestrictNamespaceEnvKey)
 
@@ -304,18 +242,6 @@ func setControllerOptionsForNamespaces(opts *ctrl.Options) {
 	}
 }
 
-func getTunables() (dt spod.DaemonTunables, err error) {
-	dt.WatchNamespace = os.Getenv(config.RestrictNamespaceEnvKey)
-
-	selinuxdImage := os.Getenv(selinuxdImageKey)
-	if selinuxdImage == "" {
-		return dt, errors.New("invalid selinuxd image")
-	}
-	dt.SelinuxdImage = selinuxdImage
-
-	return dt, nil
-}
-
 func getEnabledControllers(ctx *cli.Context) []controller.Controller {
 	controllers := make([]controller.Controller, 0, 2)
 
@@ -374,8 +300,7 @@ func runDaemon(ctx *cli.Context) error {
 		return errors.Wrap(err, "add per-node Status API to scheme")
 	}
 
-	err = setupEnabledControllers(ctx.Context, enabledControllers, mgr, met)
-	if err != nil {
+	if err := setupEnabledControllers(ctx.Context, enabledControllers, mgr, met); err != nil {
 		return errors.Wrap(err, "enable controllers")
 	}
 
@@ -444,16 +369,20 @@ func setupEnabledControllers(
 	met *metrics.Metrics,
 ) error {
 	for _, enableCtrl := range enabledControllers {
-		if err := enableCtrl.SchemeBuilder().AddToScheme(mgr.GetScheme()); err != nil {
-			return errors.Wrap(err, "add core operator APIs to scheme")
+		if enableCtrl.SchemeBuilder() != nil {
+			if err := enableCtrl.SchemeBuilder().AddToScheme(mgr.GetScheme()); err != nil {
+				return errors.Wrap(err, "add core operator APIs to scheme")
+			}
 		}
 
-		if err := enableCtrl.Setup(ctx, mgr, ctrl.Log.WithName(enableCtrl.Name()), met); err != nil {
+		if err := enableCtrl.Setup(ctx, mgr, met); err != nil {
 			return errors.Wrapf(err, "setup %s controller", enableCtrl.Name())
 		}
 
-		if err := mgr.AddHealthzCheck(enableCtrl.Name(), enableCtrl.Healthz); err != nil {
-			return errors.Wrap(err, "add readiness check to controller")
+		if met != nil {
+			if err := mgr.AddHealthzCheck(enableCtrl.Name(), enableCtrl.Healthz); err != nil {
+				return errors.Wrap(err, "add readiness check to controller")
+			}
 		}
 	}
 
diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go
index 2904e31af..a53f0576f 100644
--- a/internal/pkg/controller/controller.go
+++ b/internal/pkg/controller/controller.go
@@ -20,7 +20,6 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/go-logr/logr"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/scheme"
 
@@ -36,7 +35,7 @@ type Controller interface {
 	SchemeBuilder() *scheme.Builder
 
 	// Setup is the initialization of the controller.
-	Setup(context.Context, ctrl.Manager, logr.Logger, *metrics.Metrics) error
+	Setup(context.Context, ctrl.Manager, *metrics.Metrics) error
 
 	// Healthz is the liveness probe endpoint of the controller.
 	Healthz(*http.Request) error
diff --git a/internal/pkg/daemon/profilerecorder/profilerecorder.go b/internal/pkg/daemon/profilerecorder/profilerecorder.go
index 637e9a56b..d31e0e919 100644
--- a/internal/pkg/daemon/profilerecorder/profilerecorder.go
+++ b/internal/pkg/daemon/profilerecorder/profilerecorder.go
@@ -95,7 +95,6 @@ func (r *RecorderReconciler) SchemeBuilder() *scheme.Builder {
 func (r *RecorderReconciler) Setup(
 	ctx context.Context,
 	mgr ctrl.Manager,
-	l logr.Logger,
 	met *metrics.Metrics,
 ) error {
 	const name = "profilerecorder"
@@ -113,10 +112,11 @@ func (r *RecorderReconciler) Setup(
 		return errors.Wrap(err, errGetNode)
 	}
 
+	r.log = ctrl.Log.WithName(r.Name())
 	nodeAddresses := []string{}
 	for _, addr := range node.Status.Addresses {
 		if addr.Type == corev1.NodeInternalIP {
-			l.Info("Setting up profile recorder", "Node", addr.Address)
+			r.log.Info("Setting up profile recorder", "Node", addr.Address)
 			nodeAddresses = append(nodeAddresses, addr.Address)
 			break
 		}
@@ -127,7 +127,6 @@ func (r *RecorderReconciler) Setup(
 	}
 
 	r.client = mgr.GetClient()
-	r.log = l
 	r.nodeAddresses = nodeAddresses
 	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor(name))
 
diff --git a/internal/pkg/daemon/seccompprofile/seccompprofile.go b/internal/pkg/daemon/seccompprofile/seccompprofile.go
index 4ebab8888..40082af58 100644
--- a/internal/pkg/daemon/seccompprofile/seccompprofile.go
+++ b/internal/pkg/daemon/seccompprofile/seccompprofile.go
@@ -105,11 +105,10 @@ func (r *Reconciler) SchemeBuilder() *scheme.Builder {
 func (r *Reconciler) Setup(
 	ctx context.Context,
 	mgr ctrl.Manager,
-	l logr.Logger,
 	met *metrics.Metrics,
 ) error {
 	r.client = mgr.GetClient()
-	r.log = l
+	r.log = ctrl.Log.WithName(r.Name())
 	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("profile"))
 	r.save = saveProfileOnDisk
 	r.metrics = met
diff --git a/internal/pkg/daemon/selinuxprofile/setup.go b/internal/pkg/daemon/selinuxprofile/setup.go
index 602504cdc..dc69b8f5a 100644
--- a/internal/pkg/daemon/selinuxprofile/setup.go
+++ b/internal/pkg/daemon/selinuxprofile/setup.go
@@ -21,7 +21,6 @@ import (
 	"text/template"
 
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	ctrl "sigs.k8s.io/controller-runtime"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -36,7 +35,6 @@ var log = logf.Log.WithName("selinuxprofile")
 func (r *ReconcileSP) Setup(
 	ctx context.Context,
 	mgr ctrl.Manager,
-	l logr.Logger,
 	met *metrics.Metrics,
 ) error {
 	// Create template to wrap policies
diff --git a/internal/pkg/manager/nodestatus/nodestatus.go b/internal/pkg/manager/nodestatus/nodestatus.go
index 02e060f1c..4e3cfe987 100644
--- a/internal/pkg/manager/nodestatus/nodestatus.go
+++ b/internal/pkg/manager/nodestatus/nodestatus.go
@@ -18,6 +18,7 @@ package nodestatus
 
 import (
 	"context"
+	"net/http"
 	"time"
 
 	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
@@ -31,12 +32,14 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 
 	pbv1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilebase/v1alpha1"
 	seccompv1alpha1 "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
 	statusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
 	selinuxv1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/controller"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
 
@@ -50,6 +53,11 @@ var (
 	ErrUnkownOwnerKind = errors.New("the node status owner is of an unknown kind")
 )
 
+// NewController returns a new empty controller instance.
+func NewController() controller.Controller {
+	return &StatusReconciler{}
+}
+
 // A StatusReconciler monitors node changes and updates the profile status.
 type StatusReconciler struct {
 	client client.Client
@@ -57,6 +65,21 @@ type StatusReconciler struct {
 	record event.Recorder
 }
 
+// Name returns the name of the controller.
+func (r *StatusReconciler) Name() string {
+	return "nodestatus"
+}
+
+// SchemeBuilder returns the API scheme of the controller.
+func (r *StatusReconciler) SchemeBuilder() *scheme.Builder {
+	return statusv1alpha1.SchemeBuilder
+}
+
+// Healthz is the liveness probe endpoint of the controller.
+func (r *StatusReconciler) Healthz(*http.Request) error {
+	return nil
+}
+
 func NewStatusReconciler(cli client.Client, log logr.Logger, record event.Recorder) *StatusReconciler {
 	return &StatusReconciler{
 		client: cli,
diff --git a/internal/pkg/manager/nodestatus/setup.go b/internal/pkg/manager/nodestatus/setup.go
index 2a7d9c3ec..721f6a7bb 100644
--- a/internal/pkg/manager/nodestatus/setup.go
+++ b/internal/pkg/manager/nodestatus/setup.go
@@ -20,20 +20,25 @@ import (
 	"context"
 
 	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/go-logr/logr"
 	ctrl "sigs.k8s.io/controller-runtime"
 
 	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/metrics"
 )
 
 // Setup adds a controller that reconciles the SPOd DaemonSet.
-func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
+func (r *StatusReconciler) Setup(
+	ctx context.Context,
+	mgr ctrl.Manager,
+	met *metrics.Metrics,
+) error {
+	r.client = mgr.GetClient()
+	r.log = ctrl.Log.WithName(r.Name())
+	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor(r.Name()))
+
 	// Register a special reconciler for status events
 	return ctrl.NewControllerManagedBy(mgr).
-		Named("nodestatus").
+		Named(r.Name()).
 		For(&secprofnodestatusv1alpha1.SecurityProfileNodeStatus{}).
-		Complete(NewStatusReconciler(mgr.GetClient(),
-			l,
-			event.NewAPIRecorder(mgr.GetEventRecorderFor("nodestatus"))),
-		)
+		Complete(r)
 }
diff --git a/internal/pkg/manager/spod/setup.go b/internal/pkg/manager/spod/setup.go
index 2b9509730..d108ca0c2 100644
--- a/internal/pkg/manager/spod/setup.go
+++ b/internal/pkg/manager/spod/setup.go
@@ -18,65 +18,137 @@ package spod
 
 import (
 	"context"
+	"os"
 
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 
 	spodv1alpha1 "sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/metrics"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/spod/bindata"
 )
 
-// DaemonTunables defines the parameters to tune/modify for the
+const (
+	selinuxdImageKey string = "RELATED_IMAGE_SELINUXD"
+)
+
+// daemonTunables defines the parameters to tune/modify for the
 // Security-Profiles-Operator-Daemon.
-type DaemonTunables struct {
-	SelinuxdImage    string
-	LogEnricherImage string
-	WatchNamespace   string
+type daemonTunables struct {
+	selinuxdImage    string
+	logEnricherImage string
+	watchNamespace   string
 }
 
 // Setup adds a controller that reconciles the SPOd DaemonSet.
-func Setup(ctx context.Context, mgr ctrl.Manager, dt *DaemonTunables, l logr.Logger) error {
+func (r *ReconcileSPOd) Setup(
+	ctx context.Context,
+	mgr ctrl.Manager,
+	met *metrics.Metrics,
+) error {
+	r.client = mgr.GetClient()
+	r.log = ctrl.Log.WithName(r.Name())
+	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor(r.Name()))
+
+	if err := r.createConfigIfNotExist(ctx); err != nil {
+		return errors.Wrap(err, "create config if not existing")
+	}
+
+	dt, err := getTunables()
+	if err != nil {
+		return errors.Wrap(err, "get tunables")
+	}
+
+	r.baseSPOd = getEffectiveSPOd(dt)
+	r.scheme = mgr.GetScheme()
+	r.watchNamespace = dt.watchNamespace
+
 	return ctrl.NewControllerManagedBy(mgr).
-		Named("spod-config").
+		Named(r.Name()).
 		For(&spodv1alpha1.SecurityProfilesOperatorDaemon{}).
 		Owns(&appsv1.DaemonSet{}).
 		WithEventFilter(resource.NewPredicates(isInOperatorNamespace)).
-		Complete(&ReconcileSPOd{
-			baseSPOd:       getEffectiveSPOd(dt),
-			client:         mgr.GetClient(),
-			log:            l,
-			record:         event.NewAPIRecorder(mgr.GetEventRecorderFor("spod-config")),
-			scheme:         mgr.GetScheme(),
-			watchNamespace: dt.WatchNamespace,
-		})
+		Complete(r)
+}
+
+func (r *ReconcileSPOd) createConfigIfNotExist(ctx context.Context) error {
+	obj := &spodv1alpha1.SecurityProfilesOperatorDaemon{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "spod",
+			Namespace: config.GetOperatorNamespace(),
+			Labels:    map[string]string{"app": config.OperatorName},
+		},
+		Spec: spodv1alpha1.SPODSpec{
+			EnableSelinux:     false,
+			EnableLogEnricher: false,
+			Tolerations: []corev1.Toleration{
+				{
+					Key:      "node-role.kubernetes.io/master",
+					Operator: corev1.TolerationOpExists,
+					Effect:   corev1.TaintEffectNoSchedule,
+				},
+				{
+					Key:      "node-role.kubernetes.io/control-plane",
+					Operator: corev1.TolerationOpExists,
+					Effect:   corev1.TaintEffectNoSchedule,
+				},
+				{
+					Key:      "node.kubernetes.io/not-ready",
+					Operator: corev1.TolerationOpExists,
+					Effect:   corev1.TaintEffectNoExecute,
+				},
+			},
+		},
+	}
+
+	if err := r.client.Create(ctx, obj); !k8serrors.IsAlreadyExists(err) {
+		return errors.Wrap(err, "create SecurityProfilesOperatorDaemon object")
+	}
+
+	return nil
+}
+
+func getTunables() (*daemonTunables, error) {
+	dt := &daemonTunables{}
+	dt.watchNamespace = os.Getenv(config.RestrictNamespaceEnvKey)
+
+	selinuxdImage := os.Getenv(selinuxdImageKey)
+	if selinuxdImage == "" {
+		return dt, errors.New("invalid selinuxd image")
+	}
+	dt.selinuxdImage = selinuxdImage
+
+	return dt, nil
 }
 
-func getEffectiveSPOd(dt *DaemonTunables) *appsv1.DaemonSet {
+func getEffectiveSPOd(dt *daemonTunables) *appsv1.DaemonSet {
 	refSPOd := bindata.Manifest.DeepCopy()
 	refSPOd.SetNamespace(config.GetOperatorNamespace())
 
 	daemon := &refSPOd.Spec.Template.Spec.Containers[0]
-	if dt.WatchNamespace != "" {
+	if dt.watchNamespace != "" {
 		daemon.Env = append(daemon.Env, corev1.EnvVar{
 			Name:  config.RestrictNamespaceEnvKey,
-			Value: dt.WatchNamespace,
+			Value: dt.watchNamespace,
 		})
 	}
 
 	selinuxd := &refSPOd.Spec.Template.Spec.Containers[1]
-	selinuxd.Image = dt.SelinuxdImage
+	selinuxd.Image = dt.selinuxdImage
 
 	logEnricher := &refSPOd.Spec.Template.Spec.Containers[2]
-	logEnricher.Image = dt.LogEnricherImage
+	logEnricher.Image = dt.logEnricherImage
 
 	sepolImage := &refSPOd.Spec.Template.Spec.InitContainers[1]
-	sepolImage.Image = dt.SelinuxdImage // selinuxd ships the policies as well
+	sepolImage.Image = dt.selinuxdImage // selinuxd ships the policies as well
 	return refSPOd
 }
 
diff --git a/internal/pkg/manager/spod/spod_controller.go b/internal/pkg/manager/spod/spod_controller.go
index ccb4ecd9f..01e7ccf83 100644
--- a/internal/pkg/manager/spod/spod_controller.go
+++ b/internal/pkg/manager/spod/spod_controller.go
@@ -19,6 +19,7 @@ package spod
 import (
 	"context"
 	"fmt"
+	"net/http"
 
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/go-logr/logr"
@@ -32,10 +33,12 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 
 	sccmpv1alpha1 "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
 	spodv1alpha1 "sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/controller"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/spod/bindata"
 )
 
@@ -44,6 +47,11 @@ const (
 	reasonCannotUpdateSPOD event.Reason = "CannotUpdateSPOD"
 )
 
+// NewController returns a new empty controller instance.
+func NewController() controller.Controller {
+	return &ReconcileSPOd{}
+}
+
 // blank assignment to verify that ReconcileSPOd implements `reconcile.Reconciler`.
 var _ reconcile.Reconciler = &ReconcileSPOd{}
 
@@ -59,6 +67,21 @@ type ReconcileSPOd struct {
 	watchNamespace string
 }
 
+// Name returns the name of the controller.
+func (r *ReconcileSPOd) Name() string {
+	return "spod-config"
+}
+
+// SchemeBuilder returns the API scheme of the controller.
+func (r *ReconcileSPOd) SchemeBuilder() *scheme.Builder {
+	return spodv1alpha1.SchemeBuilder
+}
+
+// Healthz is the liveness probe endpoint of the controller.
+func (r *ReconcileSPOd) Healthz(*http.Request) error {
+	return nil
+}
+
 // Security Profiles Operator RBAC permissions to manage its own configuration
 // nolint:lll
 //
diff --git a/internal/pkg/manager/workloadannotator/setup.go b/internal/pkg/manager/workloadannotator/setup.go
index 06f0f9803..82d28ef6e 100644
--- a/internal/pkg/manager/workloadannotator/setup.go
+++ b/internal/pkg/manager/workloadannotator/setup.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -29,10 +28,20 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/metrics"
 )
 
 // Setup adds a controller that reconciles the SPOd DaemonSet.
-func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
+func (r *PodReconciler) Setup(
+	ctx context.Context,
+	mgr ctrl.Manager,
+	met *metrics.Metrics,
+) error {
+	const name = "pods"
+	r.client = mgr.GetClient()
+	r.log = ctrl.Log.WithName(r.Name())
+	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor(name))
+
 	// Index Pods using seccomp profiles
 	if err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Pod{}, spOwnerKey, func(rawObj client.Object) []string {
 		pod, ok := rawObj.(*corev1.Pod)
@@ -58,14 +67,10 @@ func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
 
 	// Register a special reconciler for pod events
 	return ctrl.NewControllerManagedBy(mgr).
-		Named("pods").
+		Named(name).
 		For(&corev1.Pod{}).
 		WithEventFilter(resource.NewPredicates(hasSeccompProfile)).
-		Complete(&PodReconciler{
-			client: mgr.GetClient(),
-			log:    l,
-			record: event.NewAPIRecorder(mgr.GetEventRecorderFor("pods")),
-		})
+		Complete(r)
 }
 
 func hasSeccompProfile(obj runtime.Object) bool {
diff --git a/internal/pkg/manager/workloadannotator/workloadannotator.go b/internal/pkg/manager/workloadannotator/workloadannotator.go
index 9132be480..3bfa2bee7 100644
--- a/internal/pkg/manager/workloadannotator/workloadannotator.go
+++ b/internal/pkg/manager/workloadannotator/workloadannotator.go
@@ -19,6 +19,7 @@ package workloadannotator
 import (
 	"context"
 	"fmt"
+	"net/http"
 	"strings"
 	"time"
 
@@ -29,8 +30,10 @@ import (
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
 
 	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/controller"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
 
@@ -40,6 +43,11 @@ const (
 	reconcileTimeout = 1 * time.Minute
 )
 
+// NewController returns a new empty controller instance.
+func NewController() controller.Controller {
+	return &PodReconciler{}
+}
+
 // A PodReconciler monitors pod changes and links them to SeccompProfiles.
 type PodReconciler struct {
 	client client.Client
@@ -47,6 +55,21 @@ type PodReconciler struct {
 	record event.Recorder
 }
 
+// Name returns the name of the controller.
+func (r *PodReconciler) Name() string {
+	return "workload-annotator"
+}
+
+// SchemeBuilder returns the API scheme of the controller.
+func (r *PodReconciler) SchemeBuilder() *scheme.Builder {
+	return nil
+}
+
+// Healthz is the liveness probe endpoint of the controller.
+func (r *PodReconciler) Healthz(*http.Request) error {
+	return nil
+}
+
 // Namespace scoped
 // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
 


Test patch:
diff --git a/internal/pkg/manager/spod/setup_test.go b/internal/pkg/manager/spod/setup_test.go
index 1390894a5..8a6677acc 100644
--- a/internal/pkg/manager/spod/setup_test.go
+++ b/internal/pkg/manager/spod/setup_test.go
@@ -29,19 +29,19 @@ func Test_getEffectiveSPOd(t *testing.T) {
 	t.Parallel()
 	tests := []struct {
 		name    string
-		dt      DaemonTunables
+		dt      daemonTunables
 		nsIsSet bool
 		wantErr bool
 	}{
 		{
 			"Should correctly set the image",
-			DaemonTunables{"foo:bar", "bar:baz", "brot:wurst"},
+			daemonTunables{"foo:bar", "bar:baz", "brot:wurst"},
 			false,
 			false,
 		},
 		{
 			"Should correctly set the namespace",
-			DaemonTunables{"foo:bar", "bar:baz", "brot:wurst"},
+			daemonTunables{"foo:bar", "bar:baz", "brot:wurst"},
 			true,
 			false,
 		},
@@ -52,12 +52,12 @@ func Test_getEffectiveSPOd(t *testing.T) {
 			t.Parallel()
 			os.Setenv("OPERATOR_NAMESPACE", "default")
 			got := getEffectiveSPOd(&tt.dt)
-			require.Equal(t, tt.dt.SelinuxdImage, got.Spec.Template.Spec.Containers[1].Image)
-			require.Equal(t, tt.dt.LogEnricherImage, got.Spec.Template.Spec.Containers[2].Image)
+			require.Equal(t, tt.dt.selinuxdImage, got.Spec.Template.Spec.Containers[1].Image)
+			require.Equal(t, tt.dt.logEnricherImage, got.Spec.Template.Spec.Containers[2].Image)
 			var found bool
 			for _, env := range got.Spec.Template.Spec.Containers[0].Env {
 				if env.Name == config.RestrictNamespaceEnvKey {
-					require.Equal(t, tt.dt.WatchNamespace, env.Value)
+					require.Equal(t, tt.dt.watchNamespace, env.Value)
 					found = true
 					break
 				}