Repository: kubernetes-sigs/security-profiles-operator
Pull request #180 (2021-01-07 09:40:50)

Change controller manager to be a Deployment
Change controller manager to be a Deployment

Why?
====

While the current approach of deploying a DaemonSet directly works, it
has some limitations:

* It's not trivial to customize what to deploy (e.g. if we want to
  disable AppArmor)

* It's not directly supported by some operator catalogs (e.g.
  via the Operator Lifecycle Manager)

This instead proposes the usage of a Deployment.

This Deployment listens for a Configuration called "config", which
will allow us to customize the DaemonSet (e.g. enable/disable
features).

On the other hand, with we'll be able to use this Deployment to serve
webhooks (like the one that's currently missing to validate the SELinux
policy namespaces).

What's missing?
===============

Actually add configuration options
----------------------------------

Currently the controller only deploys the DaemonSet... only configuration
is taken into account: `SPOdImagePullPolicy`.

This was required for the kind-based e2e tests.

Handle configuration updates
----------------------------

Once we have configurations, we need to handle updates.

Better bindata management
-------------------------

Currently the DaemonSet is a hardcoded string, it would be better to
have an appropriate bindata solution which would read a yaml file and
update go code to handle it automatically.

Separate RBAC roles
----------------------------

Currently, both the controller manager and the daemon use the same ServiceAccount
with the same role. It would be ideal to separate them.

Merge SELinux to the DaemonSet
------------------------------

Currently SELinux works on a cluster level, and so it's part of the main
Deployment. This needs to be changed so SELinux module installation
should be done as part of the DaemonSet instead. This will require us to
change the inner workings of the SELinux bits (probably using selinuxd
[1])

Discussion
========

* How do folks feel about the configuration being a ConfigMap. Is that fine for everybody?
  or would folks prefer to have a dedicated CustomResource for this?

[1] https://github.com/JAORMX/selinuxd

```release-note
The main deployment method is now a Deployment object that requires a ConfigMap
called "config".
```

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/180#" title="Author self-approved">JAORMX</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **hasheddan** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @hasheddan` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["hasheddan"]} -->
k8s-ci-robot: Hi @JAORMX. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

pjbgf: /ok-to-test
k8s-ci-robot: @JAORMX: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | d3e2db722fc31cbd41ed92bc0c05b63ef5dd7342 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-verify/1347134917007380480) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
JAORMX: Pull-request updated, HEAD is now ab783b89aa6a1cf145c10778e0f07df61927e033
JAORMX: Pull-request updated, HEAD is now c953a36e50a0560b0dc79face965b6e5e3ac9905
k8s-ci-robot: @JAORMX: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | c953a36e50a0560b0dc79face965b6e5e3ac9905 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-test-e2e/1347136187642417152) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
JAORMX: Pull-request updated, HEAD is now a53cd0f885438f891f6f1879e89ccd560b24051d
k8s-ci-robot: @JAORMX: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | c953a36e50a0560b0dc79face965b6e5e3ac9905 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-test-e2e/1347136187642417152) | `/test pull-security-profiles-operator-test-e2e`
pull-security-profiles-operator-verify | a53cd0f885438f891f6f1879e89ccd560b24051d | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-verify/1347142141553414144) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
JAORMX: Pull-request updated, HEAD is now bef1618270b4717a075c5991e16797684660a7a1
k8s-ci-robot: @JAORMX: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | bef1618270b4717a075c5991e16797684660a7a1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-test-e2e/1347143141391601664) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @JAORMX: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 42aa1ceba0493a09549e811dccaac7180c182241 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-test-e2e/1347153683476910080) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @JAORMX: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | ce33e71a96242619e2916049ce0d34d276d9a7e2 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/180/pull-security-profiles-operator-test-e2e/1347155571505106944) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=180). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3AJAORMX). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
JAORMX: Ready for review. Sorry for the noise, had to fix the e2e tests
saschagrunert: In general I like the idea of switching to a Deployment. If @pjbgf has no concerns then I think we can get this in.
JAORMX: > I'm definitely in favor of splitting the logic between a Deployment and a DaemonSet, that will simplify a lot of things.
> 
> Has any thought been given to doing that split in the YAML manifests, rather than moving the whole DaemonSet installation into a controller loop? That would simplify the configuration problem a little. I'm not sure I understand what managing the DaemonSet in a controller buys us.

That could have been a possible approach, however, I'm leaning more towards reconciling the DaemonSet based on configuration values since it would have some advantages:
* Being able to turn off/on entire containers from the DS (e.g. SELinux/AppArmor pieces)
* Being able to manage future upgrade logic as part of the reconcile loop

> 
> > How do folks feel about the configuration being a ConfigMap. Is that fine for everybody?
> > or would folks prefer to have a dedicated CustomResource for this?
> 
> Config in a ConfigMap seems the most sensible to me, I don't think a custom CRD would improve usability very much here.

I wasn't sure. The ConfigMap is an easy way to provide configurations, and no other CRD would be needed. But, a CRD could provide us with SPO status, as well as better validation of the configuration. However, I don't have a strong opinion on this, and the ConfigMap was an easy start.
saschagrunert: > > Config in a ConfigMap seems the most sensible to me, I don't think a custom CRD would improve usability very much here.
> 
> I wasn't sure. The ConfigMap is an easy way to provide configurations, and no other CRD would be needed. But, a CRD could provide us with SPO status, as well as better validation of the configuration. However, I don't have a strong opinion on this, and the ConfigMap was an easy start.

If we're validating/applying the configuration only on startup of the operator, then a ConfigMap seems fine to me. I see the drawback of manually editing structures like JSON or even YAML in a ConfigMap, so I still lean towards a CRD. It might introduce a too big overhead in the first place, but later it may save us painful migration paths. Any thoughts on that?
JAORMX: > > > Config in a ConfigMap seems the most sensible to me, I don't think a custom CRD would improve usability very much here.
> > 
> > 
> > I wasn't sure. The ConfigMap is an easy way to provide configurations, and no other CRD would be needed. But, a CRD could provide us with SPO status, as well as better validation of the configuration. However, I don't have a strong opinion on this, and the ConfigMap was an easy start.
> 
> If we're validating/applying the configuration only on startup of the operator, then a ConfigMap seems fine to me. I see the drawback of manually editing structures like JSON or even YAML in a ConfigMap, so I still lean towards a CRD. It might introduce a too big overhead in the first place, but later it may save us painful migration paths. Any thoughts on that?

The CRD will also make the reconciler code easier since we would get everything parsed correctly in the first place. But I suggest the following: Let's gather use-cases for the CRD and start drafting a proposal. Once we have those, we can migrate from this ConfigMap to the CRD if we see fit. It could turn out that the use-cases aren't many and we don't need to migrate. But it could also make it clearer why a CRD would be a better choice.

In the meantime, what if we leave this PR as-is so it doesn't become bigger? We're not doing a stable release yet, so we can iterate on further PRs.
saschagrunert: > In the meantime, what if we leave this PR as-is so it doesn't become bigger? We're not doing a stable release yet, so we can iterate on further PRs.

I agree :+1: 
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/180#" title="Author self-approved">JAORMX</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/180#pullrequestreview-566053815" title="LGTM">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/cmd/security-profiles-operator/main.go b/cmd/security-profiles-operator/main.go
index cc9f4517e..dadbb9b42 100644
--- a/cmd/security-profiles-operator/main.go
+++ b/cmd/security-profiles-operator/main.go
@@ -17,6 +17,7 @@ limitations under the License.
 package main
 
 import (
+	"fmt"
 	"os"
 	"time"
 
@@ -31,12 +32,15 @@ import (
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/controllers/profile"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/controllers/selinuxpolicy"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/controllers/spod"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/version"
 )
 
 const (
-	jsonFlag      string = "json"
-	restrictNSKey string = "RESTRICT_TO_NAMESPACE"
+	jsonFlag               string = "json"
+	restrictNSKey          string = "RESTRICT_TO_NAMESPACE"
+	operatorImageKey       string = "RELATED_IMAGE_OPERATOR"
+	nonRootEnablerImageKey string = "RELATED_IMAGE_NON_ROOT_ENABLER"
 )
 
 var (
@@ -53,7 +57,6 @@ func main() {
 	app.Description = "The Security Profiles Operator makes it easier for cluster admins " +
 		"to manage their seccomp or AppArmor profiles and apply them to Kubernetes' workloads."
 	app.Version = version.Get().Version
-	app.Action = run
 	app.Commands = cli.Commands{
 		&cli.Command{
 			Name:    "version",
@@ -80,6 +83,18 @@ func main() {
 				return nil
 			},
 		},
+		&cli.Command{
+			Name:    "manager",
+			Aliases: []string{"m"},
+			Usage:   "run the security-profiles-operator manager",
+			Action:  runManager,
+		},
+		&cli.Command{
+			Name:    "daemon",
+			Aliases: []string{"d"},
+			Usage:   "run the security-profiles-operator daemon",
+			Action:  runDaemon,
+		},
 	}
 
 	if err := app.Run(os.Args); err != nil {
@@ -88,10 +103,10 @@ func main() {
 	}
 }
 
-func run(ctx *cli.Context) error {
+func printInfo(component string) {
 	v := version.Get()
 	setupLog.Info(
-		"starting security-profiles-operator",
+		fmt.Sprintf("starting component: %s", component),
 		"version", v.Version,
 		"gitCommit", v.GitCommit,
 		"gitTreeState", v.GitTreeState,
@@ -100,81 +115,113 @@ func run(ctx *cli.Context) error {
 		"compiler", v.Compiler,
 		"platform", v.Platform,
 	)
+}
+
+func runManager(ctx *cli.Context) error {
+	printInfo("security-profiles-operator")
 	cfg, err := ctrl.GetConfig()
 	if err != nil {
 		return errors.Wrap(err, "get config")
 	}
 
-	fatalErrors := make(chan error)
-	done := make(chan bool)
 	sigHandler := ctrl.SetupSignalHandler()
 
-	// Daemonset managers
-	go func() {
-		ctrlOpts := ctrl.Options{
-			SyncPeriod: &sync,
-		}
+	// cluster managers (need leader election)
+	ctrlOpts := manager.Options{
+		SyncPeriod:       &sync,
+		LeaderElection:   true,
+		LeaderElectionID: "selinux-operator-lock",
+	}
 
-		if os.Getenv(restrictNSKey) != "" {
-			ctrlOpts.Namespace = os.Getenv(restrictNSKey)
-		}
+	dt, err := getTunables()
+	if err != nil {
+		return err
+	}
 
-		mgr, err := ctrl.NewManager(cfg, ctrlOpts)
-		if err != nil {
-			fatalErrors <- errors.Wrap(err, "create manager")
-		}
+	if dt.WatchNamespace != "" {
+		ctrlOpts.Namespace = dt.WatchNamespace
+	}
 
-		if err := seccompprofilev1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
-			fatalErrors <- errors.Wrap(err, "add core operator APIs to scheme")
-		}
+	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
+	if err != nil {
+		return errors.Wrap(err, "create cluster manager")
+	}
 
-		if err := profile.Setup(ctx.Context, mgr, ctrl.Log.WithName("profile")); err != nil {
-			fatalErrors <- errors.Wrap(err, "setup profile controller")
-		}
+	if err := selinuxpolicyv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
+		return errors.Wrap(err, "add core operator APIs to scheme")
+	}
 
-		setupLog.Info("starting manager")
-		if err := mgr.Start(sigHandler); err != nil {
-			fatalErrors <- errors.Wrap(err, "controller manager error")
-		}
-		close(done)
-	}()
+	if err := selinuxpolicy.Setup(ctx.Context, mgr, ctrl.Log.WithName("selinuxpolicy")); err != nil {
+		return errors.Wrap(err, "setup profile controller")
+	}
 
-	// cluster managers (need leader election)
-	go func() {
-		ctrlOpts := manager.Options{
-			SyncPeriod:       &sync,
-			LeaderElection:   true,
-			LeaderElectionID: "selinux-operator-lock",
-			// NOTE(jaosorior): Metrics are disabled because
-			// we have two managers.
-			MetricsBindAddress: "0",
-		}
-
-		mgr, err := ctrl.NewManager(cfg, ctrlOpts)
-		if err != nil {
-			fatalErrors <- errors.Wrap(err, "create cluster manager")
-		}
-
-		if err := selinuxpolicyv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
-			fatalErrors <- errors.Wrap(err, "add core operator APIs to scheme")
-		}
-
-		if err := selinuxpolicy.Setup(ctx.Context, mgr, ctrl.Log.WithName("selinuxpolicy")); err != nil {
-			fatalErrors <- errors.Wrap(err, "setup profile controller")
-		}
-
-		setupLog.Info("starting manager")
-		if err := mgr.Start(sigHandler); err != nil {
-			fatalErrors <- errors.Wrap(err, "controller manager error")
-		}
-		close(done)
-	}()
-
-	select {
-	case err := <-fatalErrors:
-		return err
-	case <-done:
-		setupLog.Info("ending manager")
+	if err := spod.Setup(ctx.Context, mgr, dt, ctrl.Log.WithName("spod-config")); err != nil {
+		return errors.Wrap(err, "setup profile controller")
+	}
+
+	setupLog.Info("starting manager")
+	if err := mgr.Start(sigHandler); err != nil {
+		return errors.Wrap(err, "controller manager error")
+	}
+
+	setupLog.Info("ending manager")
+	return nil
+}
+
+func getTunables() (spod.DaemonTunables, error) {
+	var dt spod.DaemonTunables
+	restrictNS := os.Getenv(restrictNSKey)
+	operatorImage := os.Getenv(operatorImageKey)
+	if operatorImage == "" {
+		return dt, errors.New("invalid operator image")
 	}
+	nonRootEnableImage := os.Getenv(nonRootEnablerImageKey)
+	if operatorImage == "" {
+		return dt, errors.New("invalid operator image")
+	}
+
+	dt.DaemonImage = operatorImage
+	dt.NonRootEnablerImage = nonRootEnableImage
+	dt.WatchNamespace = restrictNS
+	return dt, nil
+}
+
+func runDaemon(ctx *cli.Context) error {
+	// security-profiles-operator-daemon
+	printInfo("spod")
+	cfg, err := ctrl.GetConfig()
+	if err != nil {
+		return errors.Wrap(err, "get config")
+	}
+
+	sigHandler := ctrl.SetupSignalHandler()
+
+	ctrlOpts := ctrl.Options{
+		SyncPeriod: &sync,
+	}
+
+	if os.Getenv(restrictNSKey) != "" {
+		ctrlOpts.Namespace = os.Getenv(restrictNSKey)
+	}
+
+	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
+	if err != nil {
+		return errors.Wrap(err, "create manager")
+	}
+
+	if err := seccompprofilev1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
+		return errors.Wrap(err, "add core operator APIs to scheme")
+	}
+
+	if err := profile.Setup(ctx.Context, mgr, ctrl.Log.WithName("profile")); err != nil {
+		return errors.Wrap(err, "setup profile controller")
+	}
+
+	setupLog.Info("starting daemon")
+	if err := mgr.Start(sigHandler); err != nil {
+		return errors.Wrap(err, "SPOd error")
+	}
+
+	setupLog.Info("ending daemon")
 	return nil
 }
diff --git a/deploy/base/config.yaml b/deploy/base/config.yaml
new file mode 100644
index 000000000..e614899c7
--- /dev/null
+++ b/deploy/base/config.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: config
+  namespace: security-profiles-operator
+  labels:
+    security-profiles-operator/config: ""
+data:
+  SPOdImagePullPolicy: Always
diff --git a/deploy/base/kustomization.yaml b/deploy/base/kustomization.yaml
index 9ee0befff..b3bcb0bf4 100644
--- a/deploy/base/kustomization.yaml
+++ b/deploy/base/kustomization.yaml
@@ -10,8 +10,23 @@ images:
     # newName: k8s.gcr.io/security-profiles-operator/security-profiles-operator
     # newTag: v0.3.0
 
-  - name: bash
-    newTag: "5.0"
+patchesStrategicMerge:
+- |-
+  apiVersion: apps/v1
+  kind: Deployment
+  metadata:
+    name: security-profiles-operator
+    namespace: security-profiles-operator
+  spec:
+    template:
+      spec:
+        containers:
+          - name: security-profiles-operator
+            env:
+              - name: RELATED_IMAGE_OPERATOR
+                value: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
+              - name: RELATED_IMAGE_NON_ROOT_ENABLER
+                value: bash:5.0
 
 commonLabels:
   app: security-profiles-operator
@@ -20,6 +35,7 @@ resources:
   - crd.yaml
   - rbac.yaml
   - operator.yaml
+  - config.yaml
 
 configMapGenerator:
   - name: security-profiles-operator-profile
diff --git a/deploy/base/operator.yaml b/deploy/base/operator.yaml
index 4641cf87c..762ec6e44 100644
--- a/deploy/base/operator.yaml
+++ b/deploy/base/operator.yaml
@@ -1,128 +1,40 @@
 ---
 apiVersion: apps/v1
-kind: DaemonSet
+kind: Deployment
 metadata:
   name: security-profiles-operator
   namespace: security-profiles-operator
 spec:
+  replicas: 3
   selector:
     matchLabels:
       name: security-profiles-operator
   template:
     metadata:
-      annotations:
-        openshift.io/scc: privileged
-        seccomp.security.alpha.kubernetes.io/pod: runtime/default
-        container.seccomp.security.alpha.kubernetes.io/security-profiles-operator: localhost/security-profiles-operator.json
       labels:
         name: security-profiles-operator
     spec:
       serviceAccountName: security-profiles-operator
-      initContainers:
-        - name: non-root-enabler
-          image: bash
-          # Creates folder /var/lib/security-profiles-operator, sets 2000:2000 as its
-          # owner and symlink it to /var/lib/kubelet/seccomp/operator. This is
-          # required to allow the main container to run as non-root.
-          command: [bash, -c]
-          args:
-            - |+
-              set -euo pipefail
-
-              if [ ! -d $KUBELET_SECCOMP_ROOT ]; then
-                /bin/mkdir -m 0744 -p $KUBELET_SECCOMP_ROOT
-              fi
-
-              /bin/mkdir -p $OPERATOR_ROOT
-              /bin/chmod 0744 $OPERATOR_ROOT
-
-              if [ ! -L $OPERATOR_SYMLINK ]; then
-                /bin/ln -s $OPERATOR_ROOT $OPERATOR_SYMLINK
-              fi
-
-              /bin/chown -R 2000:2000 $OPERATOR_ROOT
-              cp -f -v /opt/seccomp-profiles/* $KUBELET_SECCOMP_ROOT
-          env:
-            - name: KUBELET_SECCOMP_ROOT
-              value: /var/lib/kubelet/seccomp
-            - name: OPERATOR_SYMLINK
-              value: $(KUBELET_SECCOMP_ROOT)/operator
-            - name: OPERATOR_ROOT
-              value: /var/lib/security-profiles-operator
-          volumeMounts:
-            - name: host-varlib-volume
-              mountPath: /var/lib
-            - name: profile-configmap-volume
-              mountPath: /opt/seccomp-profiles
-              readOnly: true
-          securityContext:
-            allowPrivilegeEscalation: false
-            readOnlyRootFilesystem: true
-            capabilities:
-              drop: ["ALL"]
-              add: ["CHOWN", "FOWNER", "FSETID", "DAC_OVERRIDE"]
-            runAsUser: 0
-            seLinuxOptions:
-              # FIXME(jaosorior): Use a more restricted selinux type
-              type: spc_t
-          resources:
-            requests:
-              memory: "32Mi"
-              cpu: "100m"
-              ephemeral-storage: "10Mi"
-            limits:
-              memory: "64Mi"
-              cpu: "250m"
-              ephemeral-storage: "50Mi"
       containers:
         - name: security-profiles-operator
           image: security-profiles-operator
+          args:
+          - manager
           imagePullPolicy: Always
-          volumeMounts:
-          - name: host-operator-volume
-            mountPath: /var/lib/kubelet/seccomp/operator
           securityContext:
-            allowPrivilegeEscalation: false
             readOnlyRootFilesystem: true
-            runAsUser: 2000
-            runAsGroup: 2000
-            capabilities:
-              drop: ["ALL"]
-            seLinuxOptions:
-              # FIXME(jaosorior): Use a more restricted selinux type
-              type: spc_t
-          resources:
-            requests:
-              memory: "64Mi"
-              cpu: "100m"
-              ephemeral-storage: "50Mi"
-            limits:
-              memory: "128Mi"
-              cpu: "300m"
-              ephemeral-storage: "200Mi"
+            allowPrivilegeEscalation: false
           env:
-            - name: NODE_NAME
-              valueFrom:
-                fieldRef:
-                  fieldPath: spec.nodeName
-      volumes:
-      # /var/lib is used as symlinks cannot be created across different volumes
-      - name: host-varlib-volume
-        hostPath:
-          path: /var/lib
-          type: Directory
-      - name: host-operator-volume
-        hostPath:
-          path: /var/lib/security-profiles-operator
-          type: DirectoryOrCreate
-      - name: profile-configmap-volume
-        configMap:
-          name: security-profiles-operator-profile
-      tolerations:
-        - effect: NoSchedule
-          key: node-role.kubernetes.io/master
-        - effect: NoExecute
-          key: node.kubernetes.io/not-ready
-          operator: Exists
+            - name: RELATED_IMAGE_OPERATOR
+              value: security-profiles-operator
+            - name: RELATED_IMAGE_NON_ROOT_ENABLER
+              value: non-root-enabler
       nodeSelector:
-        kubernetes.io/os: linux
+        node-role.kubernetes.io/master: ""
+      tolerations:
+        - key: "node-role.kubernetes.io/master"
+          operator: "Exists"
+          effect: "NoSchedule"
+        - key: "node.kubernetes.io/not-ready"
+          operator: "Exists"
+          effect: "NoExecute"
\ No newline at end of file
diff --git a/deploy/base/rbac.yaml b/deploy/base/rbac.yaml
index 0a0186649..5306fe5fc 100644
--- a/deploy/base/rbac.yaml
+++ b/deploy/base/rbac.yaml
@@ -16,18 +16,58 @@ metadata:
   name: config-map-reader
   namespace: security-profiles-operator
 rules:
-- apiGroups: [""]
-  resources: ["configmaps", "pods", "nodes"]
-  verbs: ["get", "watch", "list"]
-- apiGroups: [""]
-  resources: ["events"]
-  verbs: ["create", "patch"]
-- apiGroups: ["security-profiles-operator.x-k8s.io"]
-  resources: ["seccompprofiles", "selinuxpolicies"]
-  verbs: ["get", "watch", "list", "update", "patch"]
-- apiGroups: ["security-profiles-operator.x-k8s.io"]
-  resources: ["seccompprofiles/status", "selinuxpolicies/status"]
-  verbs: ["get", "update", "patch"]
+- apiGroups:
+  - ""
+  resources:
+  - "configmaps"
+  - "pods"
+  - "nodes"
+  verbs:
+  - "get"
+  - "watch"
+  - "list"
+- apiGroups:
+  - ""
+  resources:
+  - "events"
+  verbs:
+  - "create"
+  - "patch"
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  - daemonsets
+  - replicasets
+  - statefulsets
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - "security-profiles-operator.x-k8s.io"
+  resources:
+  - "seccompprofiles"
+  - "selinuxpolicies"
+  verbs:
+  - "get"
+  - "watch"
+  - "list"
+  - "update"
+  - "patch"
+- apiGroups:
+  - "security-profiles-operator.x-k8s.io"
+  resources:
+  - "seccompprofiles/status"
+  - "selinuxpolicies/status"
+  verbs: 
+  - "get"
+  - "update"
+  - "patch"
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 96eb0da68..54a37ce83 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -333,6 +333,21 @@ rules:
   verbs:
   - create
   - patch
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  - daemonsets
+  - replicasets
+  - statefulsets
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -460,140 +475,61 @@ subjects:
   namespace: security-profiles-operator
 ---
 apiVersion: apps/v1
-kind: DaemonSet
+kind: Deployment
 metadata:
   labels:
     app: security-profiles-operator
   name: security-profiles-operator
   namespace: security-profiles-operator
 spec:
+  replicas: 3
   selector:
     matchLabels:
       app: security-profiles-operator
       name: security-profiles-operator
   template:
     metadata:
-      annotations:
-        container.seccomp.security.alpha.kubernetes.io/security-profiles-operator: localhost/security-profiles-operator.json
-        openshift.io/scc: privileged
-        seccomp.security.alpha.kubernetes.io/pod: runtime/default
       labels:
         app: security-profiles-operator
         name: security-profiles-operator
     spec:
       containers:
-      - env:
+      - args:
+        - manager
+        env:
         - name: RESTRICT_TO_NAMESPACE
           value: NS_REPLACE
-        - name: NODE_NAME
-          valueFrom:
-            fieldRef:
-              fieldPath: spec.nodeName
+        - name: RELATED_IMAGE_OPERATOR
+          value: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
+        - name: RELATED_IMAGE_NON_ROOT_ENABLER
+          value: bash:5.0
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
-        resources:
-          limits:
-            cpu: 300m
-            ephemeral-storage: 200Mi
-            memory: 128Mi
-          requests:
-            cpu: 100m
-            ephemeral-storage: 50Mi
-            memory: 64Mi
-        securityContext:
-          allowPrivilegeEscalation: false
-          capabilities:
-            drop:
-            - ALL
-          readOnlyRootFilesystem: true
-          runAsGroup: 2000
-          runAsUser: 2000
-          seLinuxOptions:
-            type: spc_t
-        volumeMounts:
-        - mountPath: /var/lib/kubelet/seccomp/operator
-          name: host-operator-volume
-      initContainers:
-      - args:
-        - |
-          set -euo pipefail
-
-          if [ ! -d $KUBELET_SECCOMP_ROOT ]; then
-            /bin/mkdir -m 0744 -p $KUBELET_SECCOMP_ROOT
-          fi
-
-          /bin/mkdir -p $OPERATOR_ROOT
-          /bin/chmod 0744 $OPERATOR_ROOT
-
-          if [ ! -L $OPERATOR_SYMLINK ]; then
-            /bin/ln -s $OPERATOR_ROOT $OPERATOR_SYMLINK
-          fi
-
-          /bin/chown -R 2000:2000 $OPERATOR_ROOT
-          cp -f -v /opt/seccomp-profiles/* $KUBELET_SECCOMP_ROOT
-        command:
-        - bash
-        - -c
-        env:
-        - name: KUBELET_SECCOMP_ROOT
-          value: /var/lib/kubelet/seccomp
-        - name: OPERATOR_SYMLINK
-          value: $(KUBELET_SECCOMP_ROOT)/operator
-        - name: OPERATOR_ROOT
-          value: /var/lib/security-profiles-operator
-        image: bash:5.0
-        name: non-root-enabler
-        resources:
-          limits:
-            cpu: 250m
-            ephemeral-storage: 50Mi
-            memory: 64Mi
-          requests:
-            cpu: 100m
-            ephemeral-storage: 10Mi
-            memory: 32Mi
         securityContext:
           allowPrivilegeEscalation: false
-          capabilities:
-            add:
-            - CHOWN
-            - FOWNER
-            - FSETID
-            - DAC_OVERRIDE
-            drop:
-            - ALL
           readOnlyRootFilesystem: true
-          runAsUser: 0
-          seLinuxOptions:
-            type: spc_t
-        volumeMounts:
-        - mountPath: /var/lib
-          name: host-varlib-volume
-        - mountPath: /opt/seccomp-profiles
-          name: profile-configmap-volume
-          readOnly: true
       nodeSelector:
-        kubernetes.io/os: linux
+        node-role.kubernetes.io/master: ""
       serviceAccountName: security-profiles-operator
       tolerations:
       - effect: NoSchedule
         key: node-role.kubernetes.io/master
+        operator: Exists
       - effect: NoExecute
         key: node.kubernetes.io/not-ready
         operator: Exists
-      volumes:
-      - hostPath:
-          path: /var/lib
-          type: Directory
-        name: host-varlib-volume
-      - hostPath:
-          path: /var/lib/security-profiles-operator
-          type: DirectoryOrCreate
-        name: host-operator-volume
-      - configMap:
-          name: security-profiles-operator-profile
-        name: profile-configmap-volume
+---
+apiVersion: v1
+data:
+  SPOdImagePullPolicy: Always
+kind: ConfigMap
+metadata:
+  labels:
+    app: security-profiles-operator
+    security-profiles-operator/config: ""
+  name: config
+  namespace: security-profiles-operator
 ---
 apiVersion: v1
 data:
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index af49acfa7..58ecda022 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -333,6 +333,21 @@ rules:
   verbs:
   - create
   - patch
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  - daemonsets
+  - replicasets
+  - statefulsets
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -460,138 +475,59 @@ subjects:
   namespace: security-profiles-operator
 ---
 apiVersion: apps/v1
-kind: DaemonSet
+kind: Deployment
 metadata:
   labels:
     app: security-profiles-operator
   name: security-profiles-operator
   namespace: security-profiles-operator
 spec:
+  replicas: 3
   selector:
     matchLabels:
       app: security-profiles-operator
       name: security-profiles-operator
   template:
     metadata:
-      annotations:
-        container.seccomp.security.alpha.kubernetes.io/security-profiles-operator: localhost/security-profiles-operator.json
-        openshift.io/scc: privileged
-        seccomp.security.alpha.kubernetes.io/pod: runtime/default
       labels:
         app: security-profiles-operator
         name: security-profiles-operator
     spec:
       containers:
-      - env:
-        - name: NODE_NAME
-          valueFrom:
-            fieldRef:
-              fieldPath: spec.nodeName
+      - args:
+        - manager
+        env:
+        - name: RELATED_IMAGE_OPERATOR
+          value: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
+        - name: RELATED_IMAGE_NON_ROOT_ENABLER
+          value: bash:5.0
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
-        resources:
-          limits:
-            cpu: 300m
-            ephemeral-storage: 200Mi
-            memory: 128Mi
-          requests:
-            cpu: 100m
-            ephemeral-storage: 50Mi
-            memory: 64Mi
-        securityContext:
-          allowPrivilegeEscalation: false
-          capabilities:
-            drop:
-            - ALL
-          readOnlyRootFilesystem: true
-          runAsGroup: 2000
-          runAsUser: 2000
-          seLinuxOptions:
-            type: spc_t
-        volumeMounts:
-        - mountPath: /var/lib/kubelet/seccomp/operator
-          name: host-operator-volume
-      initContainers:
-      - args:
-        - |
-          set -euo pipefail
-
-          if [ ! -d $KUBELET_SECCOMP_ROOT ]; then
-            /bin/mkdir -m 0744 -p $KUBELET_SECCOMP_ROOT
-          fi
-
-          /bin/mkdir -p $OPERATOR_ROOT
-          /bin/chmod 0744 $OPERATOR_ROOT
-
-          if [ ! -L $OPERATOR_SYMLINK ]; then
-            /bin/ln -s $OPERATOR_ROOT $OPERATOR_SYMLINK
-          fi
-
-          /bin/chown -R 2000:2000 $OPERATOR_ROOT
-          cp -f -v /opt/seccomp-profiles/* $KUBELET_SECCOMP_ROOT
-        command:
-        - bash
-        - -c
-        env:
-        - name: KUBELET_SECCOMP_ROOT
-          value: /var/lib/kubelet/seccomp
-        - name: OPERATOR_SYMLINK
-          value: $(KUBELET_SECCOMP_ROOT)/operator
-        - name: OPERATOR_ROOT
-          value: /var/lib/security-profiles-operator
-        image: bash:5.0
-        name: non-root-enabler
-        resources:
-          limits:
-            cpu: 250m
-            ephemeral-storage: 50Mi
-            memory: 64Mi
-          requests:
-            cpu: 100m
-            ephemeral-storage: 10Mi
-            memory: 32Mi
         securityContext:
           allowPrivilegeEscalation: false
-          capabilities:
-            add:
-            - CHOWN
-            - FOWNER
-            - FSETID
-            - DAC_OVERRIDE
-            drop:
-            - ALL
           readOnlyRootFilesystem: true
-          runAsUser: 0
-          seLinuxOptions:
-            type: spc_t
-        volumeMounts:
-        - mountPath: /var/lib
-          name: host-varlib-volume
-        - mountPath: /opt/seccomp-profiles
-          name: profile-configmap-volume
-          readOnly: true
       nodeSelector:
-        kubernetes.io/os: linux
+        node-role.kubernetes.io/master: ""
       serviceAccountName: security-profiles-operator
       tolerations:
       - effect: NoSchedule
         key: node-role.kubernetes.io/master
+        operator: Exists
       - effect: NoExecute
         key: node.kubernetes.io/not-ready
         operator: Exists
-      volumes:
-      - hostPath:
-          path: /var/lib
-          type: Directory
-        name: host-varlib-volume
-      - hostPath:
-          path: /var/lib/security-profiles-operator
-          type: DirectoryOrCreate
-        name: host-operator-volume
-      - configMap:
-          name: security-profiles-operator-profile
-        name: profile-configmap-volume
+---
+apiVersion: v1
+data:
+  SPOdImagePullPolicy: Always
+kind: ConfigMap
+metadata:
+  labels:
+    app: security-profiles-operator
+    security-profiles-operator/config: ""
+  name: config
+  namespace: security-profiles-operator
 ---
 apiVersion: v1
 data:
diff --git a/deploy/overlays/namespaced/daemonset.yaml b/deploy/overlays/namespaced/deployment.yaml
similarity index 94%
rename from deploy/overlays/namespaced/daemonset.yaml
rename to deploy/overlays/namespaced/deployment.yaml
index a2cb7e561..5d45d365d 100644
--- a/deploy/overlays/namespaced/daemonset.yaml
+++ b/deploy/overlays/namespaced/deployment.yaml
@@ -1,5 +1,5 @@
 apiVersion: apps/v1
-kind: DaemonSet
+kind: Deployment
 metadata:
   name: security-profiles-operator
   namespace: security-profiles-operator
diff --git a/deploy/overlays/namespaced/kustomization.yaml b/deploy/overlays/namespaced/kustomization.yaml
index c38dbc8c5..343d74165 100644
--- a/deploy/overlays/namespaced/kustomization.yaml
+++ b/deploy/overlays/namespaced/kustomization.yaml
@@ -7,7 +7,7 @@ bases:
   - ../cluster
 
 patchesStrategicMerge:
-  - daemonset.yaml
+  - deployment.yaml
 
 patchesJson6902:
   - target:
diff --git a/examples/config.yaml b/examples/config.yaml
new file mode 100644
index 000000000..8e686c4ac
--- /dev/null
+++ b/examples/config.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: config
+  namespace: security-profiles-operator
+  labels:
+    security-profiles-operator/config: ""
+data: {}
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 6047d4daa..0c6cbe2ce 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -32,3 +32,16 @@ const (
 	// the name of the current node.
 	NodeNameEnvKey = "NODE_NAME"
 )
+
+// GetOperatorNamespace gets the namespace that the operator is currently running on.
+func GetOperatorNamespace() string {
+	// TODO(jaosorior): Get a method to return the current operator
+	// namespace.
+	//
+	// operatorNs, err := k8sutil.GetOperatorNamespace()
+	// if err != nil {
+	// 	return "security-profiles-operator"
+	// }
+	// return operatorNs
+	return "security-profiles-operator"
+}
diff --git a/internal/pkg/controllers/selinuxpolicy/configmap_controller.go b/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
index abf720478..cd9629c84 100644
--- a/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
+++ b/internal/pkg/controllers/selinuxpolicy/configmap_controller.go
@@ -35,6 +35,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	spov1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxpolicy/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 )
 
 // blank assignment to verify that ReconcileConfigMap implements `reconcile.Reconciler`.
@@ -179,7 +180,7 @@ func newPodForPolicy(name, ns string, node *corev1.Node) *corev1.Pod {
 	return &corev1.Pod{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      GetInstallerPodName(name, ns, node),
-			Namespace: GetOperatorNamespace(),
+			Namespace: config.GetOperatorNamespace(),
 			Labels:    labels,
 		},
 		Spec: corev1.PodSpec{
diff --git a/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go b/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
index a90802890..80f77468a 100644
--- a/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
+++ b/internal/pkg/controllers/selinuxpolicy/selinuxpolicy_controller.go
@@ -35,6 +35,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	spov1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxpolicy/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 )
 
 // The underscore is not a valid character in a pod, so we can
@@ -207,7 +208,7 @@ func (r *ReconcileSP) newConfigMapForPolicy(cr *spov1alpha1.SelinuxPolicy) *core
 	return &corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      GetPolicyConfigMapName(cr.Name, cr.Namespace),
-			Namespace: GetOperatorNamespace(),
+			Namespace: config.GetOperatorNamespace(),
 			Labels:    labels,
 			Finalizers: []string{
 				metav1.FinalizerDeleteDependents,
diff --git a/internal/pkg/controllers/selinuxpolicy/setup.go b/internal/pkg/controllers/selinuxpolicy/setup.go
index 3bf16199e..e6fc4bf50 100644
--- a/internal/pkg/controllers/selinuxpolicy/setup.go
+++ b/internal/pkg/controllers/selinuxpolicy/setup.go
@@ -30,6 +30,7 @@ import (
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 	spov1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxpolicy/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 )
 
 var log = logf.Log.WithName("selinuxpolicy")
@@ -75,7 +76,7 @@ func hasSELinuxLabel(obj runtime.Object) bool {
 		return false
 	}
 	// we only care about this namespace
-	if cm.GetNamespace() != GetOperatorNamespace() {
+	if cm.GetNamespace() != config.GetOperatorNamespace() {
 		return false
 	}
 	labels := cm.GetLabels()
diff --git a/internal/pkg/controllers/selinuxpolicy/utils.go b/internal/pkg/controllers/selinuxpolicy/utils.go
index f9b02a8c9..4637436c0 100644
--- a/internal/pkg/controllers/selinuxpolicy/utils.go
+++ b/internal/pkg/controllers/selinuxpolicy/utils.go
@@ -74,19 +74,6 @@ func GetPolicyConfigMapName(name, ns string) string {
 	return namePrefix + "-" + GetPolicyK8sName(name, ns)
 }
 
-// GetOperatorNamespace gets the namespace that the operator is currently running on.
-func GetOperatorNamespace() string {
-	// TODO(jaosorior): Get a method to return the current operator
-	// namespace.
-	//
-	// operatorNs, err := k8sutil.GetOperatorNamespace()
-	// if err != nil {
-	// 	return "security-profiles-operator"
-	// }
-	// return operatorNs
-	return "security-profiles-operator"
-}
-
 // SliceContainsString helper function to check if a string is in a slice of strings.
 func SliceContainsString(slice []string, s string) bool {
 	for _, item := range slice {
diff --git a/internal/pkg/controllers/spod/bindata/bindata.go b/internal/pkg/controllers/spod/bindata/bindata.go
new file mode 100644
index 000000000..ec293cc69
--- /dev/null
+++ b/internal/pkg/controllers/spod/bindata/bindata.go
@@ -0,0 +1,34 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package bindata
+
+import (
+	"fmt"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
+)
+
+func rawObjectToUnstructured(in string) (*unstructured.Unstructured, error) {
+	obj := &unstructured.Unstructured{}
+	dec := k8syaml.NewYAMLToJSONDecoder(strings.NewReader(in))
+	if err := dec.Decode(obj); err != nil {
+		return nil, fmt.Errorf("couldn't decode YAML: %w", err)
+	}
+	return obj, nil
+}
diff --git a/internal/pkg/controllers/spod/bindata/spod.go b/internal/pkg/controllers/spod/bindata/spod.go
new file mode 100644
index 000000000..c87c52a42
--- /dev/null
+++ b/internal/pkg/controllers/spod/bindata/spod.go
@@ -0,0 +1,171 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package bindata
+
+import (
+	"fmt"
+
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// NOTE(jaosorior): We should switch this for a proper bindata solution.
+// nolint: lll
+const spodManifest = `---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+spec:
+  selector:
+    matchLabels:
+      app: spod
+  template:
+    metadata:
+      annotations:
+        openshift.io/scc: privileged
+        seccomp.security.alpha.kubernetes.io/pod: runtime/default
+        container.seccomp.security.alpha.kubernetes.io/security-profiles-operator: localhost/security-profiles-operator.json
+      labels:
+        app: spod
+    spec:
+      serviceAccountName: security-profiles-operator
+      initContainers:
+        - name: non-root-enabler
+          image: bash
+          # Creates folder /var/lib/security-profiles-operator, sets 2000:2000 as its
+          # owner and symlink it to /var/lib/kubelet/seccomp/operator. This is
+          # required to allow the main container to run as non-root.
+          command: [bash, -c]
+          args:
+            - |+
+              set -euo pipefail
+
+              if [ ! -d $KUBELET_SECCOMP_ROOT ]; then
+                /bin/mkdir -m 0744 -p $KUBELET_SECCOMP_ROOT
+              fi
+
+              /bin/mkdir -p $OPERATOR_ROOT
+              /bin/chmod 0744 $OPERATOR_ROOT
+
+              if [ ! -L $OPERATOR_SYMLINK ]; then
+                /bin/ln -s $OPERATOR_ROOT $OPERATOR_SYMLINK
+              fi
+
+              /bin/chown -R 2000:2000 $OPERATOR_ROOT
+              cp -f -v /opt/seccomp-profiles/* $KUBELET_SECCOMP_ROOT
+          env:
+            - name: KUBELET_SECCOMP_ROOT
+              value: /var/lib/kubelet/seccomp
+            - name: OPERATOR_SYMLINK
+              value: $(KUBELET_SECCOMP_ROOT)/operator
+            - name: OPERATOR_ROOT
+              value: /var/lib/security-profiles-operator
+          volumeMounts:
+            - name: host-varlib-volume
+              mountPath: /var/lib
+            - name: profile-configmap-volume
+              mountPath: /opt/seccomp-profiles
+              readOnly: true
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop: ["ALL"]
+              add: ["CHOWN", "FOWNER", "FSETID", "DAC_OVERRIDE"]
+            runAsUser: 0
+            seLinuxOptions:
+              # FIXME(jaosorior): Use a more restricted selinux type
+              type: spc_t
+          resources:
+            requests:
+              memory: "32Mi"
+              cpu: "100m"
+              ephemeral-storage: "10Mi"
+            limits:
+              memory: "64Mi"
+              cpu: "250m"
+              ephemeral-storage: "50Mi"
+      containers:
+        - name: security-profiles-operator
+          image: security-profiles-operator
+          args:
+          - daemon
+          imagePullPolicy: Always
+          volumeMounts:
+          - name: host-operator-volume
+            mountPath: /var/lib/kubelet/seccomp/operator
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            runAsUser: 2000
+            runAsGroup: 2000
+            capabilities:
+              drop: ["ALL"]
+            seLinuxOptions:
+              # FIXME(jaosorior): Use a more restricted selinux type
+              type: spc_t
+          resources:
+            requests:
+              memory: "64Mi"
+              cpu: "100m"
+              ephemeral-storage: "50Mi"
+            limits:
+              memory: "128Mi"
+              cpu: "300m"
+              ephemeral-storage: "200Mi"
+          env:
+            - name: NODE_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: spec.nodeName
+      volumes:
+      # /var/lib is used as symlinks cannot be created across different volumes
+      - name: host-varlib-volume
+        hostPath:
+          path: /var/lib
+          type: Directory
+      - name: host-operator-volume
+        hostPath:
+          path: /var/lib/security-profiles-operator
+          type: DirectoryOrCreate
+      - name: profile-configmap-volume
+        configMap:
+          name: security-profiles-operator-profile
+      tolerations:
+        - effect: NoSchedule
+          key: node-role.kubernetes.io/master
+        - effect: NoExecute
+          key: node.kubernetes.io/not-ready
+          operator: Exists
+      nodeSelector:
+        kubernetes.io/os: linux
+`
+
+func GetReferenceSPOd() (*appsv1.DaemonSet, error) {
+	rawDS, err := rawObjectToUnstructured(spodManifest)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing DS manifest: %w", err)
+	}
+	spod := &appsv1.DaemonSet{}
+	err = runtime.DefaultUnstructuredConverter.FromUnstructured(rawDS.Object, spod)
+	if err != nil {
+		return nil, fmt.Errorf("error converting unstructured DS to implementation object: %w", err)
+	}
+	return spod, nil
+}
diff --git a/internal/pkg/controllers/spod/configmap_controller.go b/internal/pkg/controllers/spod/configmap_controller.go
new file mode 100644
index 000000000..04fee4938
--- /dev/null
+++ b/internal/pkg/controllers/spod/configmap_controller.go
@@ -0,0 +1,105 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spod
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/go-logr/logr"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"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/security-profiles-operator/internal/pkg/config"
+)
+
+// blank assignment to verify that ReconcileConfigMap implements `reconcile.Reconciler`.
+var _ reconcile.Reconciler = &ReconcileSPOd{}
+
+// ReconcileSPOd reconciles the SPOd DaemonSet object.
+type ReconcileSPOd struct {
+	// This client, initialized using mgr.Client() above, is a split client
+	// that reads objects from the cache and writes to the apiserver
+	client   client.Client
+	scheme   *runtime.Scheme
+	baseSPOd *appsv1.DaemonSet
+	record   event.Recorder
+	log      logr.Logger
+}
+
+// Reconcile reads that state of the cluster for a ConfigMap object and makes changes based on the state read
+// and what is in the `ConfigMap.Spec`.
+func (r *ReconcileSPOd) Reconcile(request reconcile.Request) (reconcile.Result, error) {
+	ctx := context.Background()
+	// Fetch the ConfigMap instance
+	cminstance := &corev1.ConfigMap{}
+	if err := r.client.Get(ctx, request.NamespacedName, cminstance); err != nil {
+		if kerrors.IsNotFound(err) {
+			return reconcile.Result{}, nil
+		}
+		return reconcile.Result{}, fmt.Errorf("error getting spod configuration: %w", err)
+	}
+	spodKey := types.NamespacedName{
+		Name:      r.baseSPOd.GetName(),
+		Namespace: config.GetOperatorNamespace(),
+	}
+	foundSPOd := &appsv1.DaemonSet{}
+	if getErr := r.client.Get(ctx, spodKey, foundSPOd); getErr != nil {
+		if kerrors.IsNotFound(getErr) {
+			return r.handleCreate(ctx, cminstance)
+		}
+		return reconcile.Result{}, fmt.Errorf("error getting spod DaemonSet: %w", getErr)
+	}
+
+	// NOTE(jaosorior): We gotta handle updates
+	return reconcile.Result{}, nil
+}
+
+func (r *ReconcileSPOd) handleCreate(ctx context.Context, cfg *corev1.ConfigMap) (reconcile.Result, error) {
+	r.log.Info("Creating SPOd")
+	newSPOd := r.getConfiguredSPOd(cfg)
+
+	if err := controllerutil.SetControllerReference(cfg, newSPOd, r.scheme); err != nil {
+		return reconcile.Result{}, fmt.Errorf("error setting spod controller reference: %w", err)
+	}
+
+	if createErr := r.client.Create(ctx, newSPOd); createErr != nil {
+		if kerrors.IsAlreadyExists(createErr) {
+			return reconcile.Result{}, nil
+		}
+		return reconcile.Result{}, fmt.Errorf("error creating spod DaemonSet: %w", createErr)
+	}
+	return reconcile.Result{}, nil
+}
+
+func (r *ReconcileSPOd) getConfiguredSPOd(cfg *corev1.ConfigMap) *appsv1.DaemonSet {
+	newSPOd := r.baseSPOd.DeepCopy()
+
+	imagePullPolicyStr, found := cfg.Data["SPOdImagePullPolicy"]
+	if found {
+		pullPolicy := corev1.PullPolicy(imagePullPolicyStr)
+		newSPOd.Spec.Template.Spec.Containers[0].ImagePullPolicy = pullPolicy
+	}
+	return newSPOd
+}
diff --git a/internal/pkg/controllers/spod/setup.go b/internal/pkg/controllers/spod/setup.go
new file mode 100644
index 000000000..70582840b
--- /dev/null
+++ b/internal/pkg/controllers/spod/setup.go
@@ -0,0 +1,103 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spod
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+	"github.com/go-logr/logr"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/controllers/spod/bindata"
+)
+
+// TODO(jaosorior): Use better method than a label constant.
+const cmIsSPOdConfig = "security-profiles-operator/config"
+
+// SPOdTunables defines the parameters to tune/modify for the
+// Security-Profiles-Operator-Daemon.
+type DaemonTunables struct {
+	DaemonImage         string
+	NonRootEnablerImage 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 {
+	refSPOd, err := getEffectiveSPOd(dt)
+	if err != nil {
+		return err
+	}
+	return ctrl.NewControllerManagedBy(mgr).
+		Named("spod-config").
+		For(&corev1.ConfigMap{}).
+		WithEventFilter(resource.NewPredicates(isSPOdConfig)).
+		Owns(&appsv1.DaemonSet{}).
+		Complete(&ReconcileSPOd{
+			baseSPOd: refSPOd,
+			client:   mgr.GetClient(),
+			log:      l,
+			record:   event.NewAPIRecorder(mgr.GetEventRecorderFor("spod-config")),
+			scheme:   mgr.GetScheme(),
+		})
+}
+
+func getEffectiveSPOd(dt DaemonTunables) (*appsv1.DaemonSet, error) {
+	refSPOd, err := bindata.GetReferenceSPOd()
+	if err != nil {
+		return nil, fmt.Errorf("couldn't get reference SPO DaemonSet: %w", err)
+	}
+	cnt := &refSPOd.Spec.Template.Spec.Containers[0]
+	cnt.Image = dt.DaemonImage
+	if dt.WatchNamespace != "" {
+		cnt.Env = append(cnt.Env, corev1.EnvVar{
+			Name:  "RESTRICT_TO_NAMESPACE",
+			Value: dt.WatchNamespace,
+		})
+	}
+
+	initcnt := &refSPOd.Spec.Template.Spec.InitContainers[0]
+	initcnt.Image = dt.NonRootEnablerImage
+	return refSPOd, nil
+}
+
+func isSPOdConfig(obj runtime.Object) bool {
+	cm, ok := obj.(*corev1.ConfigMap)
+	if !ok {
+		return false
+	}
+	// we only care about this namespace
+	if cm.GetNamespace() != config.GetOperatorNamespace() {
+		return false
+	}
+	if cm.GetName() != "config" {
+		return false
+	}
+	labels := cm.GetLabels()
+	if labels == nil {
+		return false
+	}
+	_, ok = labels[cmIsSPOdConfig]
+	return ok
+}


Test patch:
diff --git a/internal/pkg/controllers/spod/setup_test.go b/internal/pkg/controllers/spod/setup_test.go
new file mode 100644
index 000000000..1cc74ff0e
--- /dev/null
+++ b/internal/pkg/controllers/spod/setup_test.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package spod
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_getEffectiveSPOd(t *testing.T) {
+	tests := []struct {
+		name    string
+		dt      DaemonTunables
+		nsIsSet bool
+		wantErr bool
+	}{
+		{
+			"Should correctly set the image",
+			DaemonTunables{"foo:bar", "bar:baz", ""},
+			false,
+			false,
+		},
+		{
+			"Should correctly set the namespace",
+			DaemonTunables{"foo:bar", "bar:baz", "my-ns"},
+			true,
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := getEffectiveSPOd(tt.dt)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("getEffectiveSPOd() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			require.Equal(t, tt.dt.DaemonImage, got.Spec.Template.Spec.Containers[0].Image)
+			require.Equal(t, tt.dt.NonRootEnablerImage, got.Spec.Template.Spec.InitContainers[0].Image)
+			var found bool
+			for _, env := range got.Spec.Template.Spec.Containers[0].Env {
+				if env.Name == "RESTRICT_TO_NAMESPACE" {
+					require.Equal(t, tt.dt.WatchNamespace, env.Value)
+					found = true
+					break
+				}
+			}
+			if tt.nsIsSet && !found {
+				t.Errorf("RESTRICT_TO_NAMESPACE env variable wasn't set")
+			}
+		})
+	}
+}
diff --git a/test/e2e_test.go b/test/e2e_test.go
index b0d28a25b..be628e363 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -22,6 +22,7 @@ import (
 	"io/ioutil"
 	"os"
 	"strings"
+	"time"
 
 	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
 )
@@ -37,6 +38,7 @@ const (
 	// migrate to a single daemonset-based implementation for the
 	// SELinux pieces.
 	defaultSelinuxOpTimeout = "180s"
+	defaultWaitTime         = 5 * time.Second
 )
 
 func (e *e2e) TestSecurityProfilesOperator() {
@@ -127,12 +129,20 @@ func (e *e2e) deployOperator(manifest, profiles string) {
 		fmt.Sprintf("s;imagePullPolicy: Always;imagePullPolicy: %s;g", e.pullPolicy),
 		manifest,
 	)
+	e.run(
+		"sed", "-i", fmt.Sprintf("s;SPOdImagePullPolicy: Always;SPOdImagePullPolicy: %s;g", e.pullPolicy),
+		manifest,
+	)
 
 	// Update the image name to match the test image
 	e.run(
 		"sed", "-i", fmt.Sprintf("s;image: .*gcr.io/.*;image: %s;g", e.testImage),
 		manifest,
 	)
+	e.run(
+		"sed", "-i", fmt.Sprintf("s;value: .*gcr.io/.*;value: %s;g", e.testImage),
+		manifest,
+	)
 
 	// Deploy the operator
 	e.logf("Deploying operator")
@@ -144,7 +154,13 @@ func (e *e2e) deployOperator(manifest, profiles string) {
 
 	// Wait for the operator to be ready
 	e.logf("Waiting for operator to be ready")
-	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "--all")
+	// Wait for deployment
+	e.kubectlOperatorNS("wait", "--for", "condition=available", "deployment", "-l", "app=security-profiles-operator")
+	// Wait for all pods in deployment
+	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "app=security-profiles-operator")
+	// Wait for all pods in DaemonSet
+	time.Sleep(defaultWaitTime)
+	e.kubectlOperatorNS("wait", "--for", "condition=ready", "pod", "-l", "app=spod")
 }
 
 func (e *e2e) cleanupOperator(manifest, profiles string) {
diff --git a/test/suite_test.go b/test/suite_test.go
index 50fa67b1f..663d39da1 100644
--- a/test/suite_test.go
+++ b/test/suite_test.go
@@ -278,8 +278,8 @@ func (e *e2e) kubectlFailure(args ...string) string {
 	return e.runFailure(e.kubectlPath, args...)
 }
 
-func (e *e2e) kubectlOperatorNS(args ...string) string {
-	return e.kubectl(
+func (e *e2e) kubectlOperatorNS(args ...string) {
+	e.kubectl(
 		append([]string{"-n", config.OperatorName}, args...)...,
 	)
 }