Repository: kubernetes-sigs/security-profiles-operator
Pull request #392 (2021-04-05 08:44:10)

Bump github.com/maxbrunsfeld/counterfeiter/v6 from 6.4.0 to 6.4.1
Bumps [github.com/maxbrunsfeld/counterfeiter/v6](https://github.com/maxbrunsfeld/counterfeiter) from 6.4.0 to 6.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/maxbrunsfeld/counterfeiter/releases">github.com/maxbrunsfeld/counterfeiter/v6's releases</a>.</em></p>
<blockquote>
<h2>v6.4.1</h2>
<ul>
<li>🐛 package mode was broken, but now it's fixed (<a href="https://github.com/nkovacs"><code>@​nkovacs</code></a> supplied a patch to fix <a href="https://github-redirect.dependabot.com/maxbrunsfeld/counterfeiter/issues/140">#140</a>)</li>
<li>🐛 allow directories when using the <code>-o</code> output flag (fixes <a href="https://github-redirect.dependabot.com/maxbrunsfeld/counterfeiter/issues/172">#172</a>, thanks <a href="https://github.com/darwayne"><code>@​darwayne</code></a>)</li>
</ul>
<p>Detailed changes: <a href="https://github.com/maxbrunsfeld/counterfeiter/compare/v6.4.0...v6.4.1">https://github.com/maxbrunsfeld/counterfeiter/compare/v6.4.0...v6.4.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/maxbrunsfeld/counterfeiter/commit/ba255fdc9c60a8d957acc716f42c0c4b6a45d12a"><code>ba255fd</code></a> switch to subset of flag to insulate us from stdlib evolution</li>
<li><a href="https://github.com/maxbrunsfeld/counterfeiter/commit/f2539983b715fd2bc7fd36dd8466c3f7b43d5cf2"><code>f253998</code></a> switch from os to flag for package mode fixtures</li>
<li><a href="https://github.com/maxbrunsfeld/counterfeiter/commit/9d440a7e90434750bb8abca3abb2cfbdd56e97eb"><code>9d440a7</code></a> allow specification of a directory for output path, fix package mode</li>
<li><a href="https://github.com/maxbrunsfeld/counterfeiter/commit/0002986116a83b2f9f2ebb3d7b8e85a0cd0597e7"><code>0002986</code></a> attempt to reproduce <a href="https://github-redirect.dependabot.com/maxbrunsfeld/counterfeiter/issues/174">#174</a></li>
<li>See full diff in <a href="https://github.com/maxbrunsfeld/counterfeiter/compare/v6.4.0...v6.4.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/maxbrunsfeld/counterfeiter/v6&package-manager=go_modules&previous-version=6.4.0&new-version=6.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

k8s-ci-robot: @dependabot[bot]: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<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>
k8s-ci-robot: Hi @dependabot[bot]. 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>

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/392#" title="Author self-approved">dependabot[bot]</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **saschagrunert** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @saschagrunert` 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":["saschagrunert"]} -->
JAORMX: @dependabot rebase
JAORMX: /ok-to-test
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/392#" title="Author self-approved">dependabot[bot]</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/392#pullrequestreview-629846688" title="Approved">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":[]} -->
k8s-ci-robot: @dependabot[bot]: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 7547884b1da2d0a363f9da9ec0d6ce080b4e4f00 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/392/pull-security-profiles-operator-test-e2e/1379742796977541120) | `/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=392). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Adependabot[bot]). 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 -->

Patch:
diff --git a/api/profilebase/v1alpha1/deepcopy.go b/api/profilebase/v1alpha1/deepcopy.go
new file mode 100644
index 000000000..e75c9b75a
--- /dev/null
+++ b/api/profilebase/v1alpha1/deepcopy.go
@@ -0,0 +1,33 @@
+/*
+Copyright 2021 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 v1alpha1
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StatusBase) DeepCopyInto(out *StatusBase) {
+	*out = *in
+	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusBase.
+func (in *StatusBase) DeepCopy() *StatusBase {
+	if in == nil {
+		return nil
+	}
+	out := new(StatusBase)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/api/profilebase/v1alpha1/profilebase.go b/api/profilebase/v1alpha1/profilebase.go
new file mode 100644
index 000000000..e1e757508
--- /dev/null
+++ b/api/profilebase/v1alpha1/profilebase.go
@@ -0,0 +1,45 @@
+/*
+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 v1alpha1
+
+import (
+	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+)
+
+// StatusBase contains common attributes for a profile's status.
+type StatusBase struct {
+	rcommonv1.ConditionedStatus `json:",inline"`
+	Status                      secprofnodestatusv1alpha1.ProfileState `json:"status,omitempty"`
+}
+
+type StatusBaseUser interface {
+	metav1.Object
+	runtime.Object
+	// GetStatusBase gets an instance of the Base status that the
+	// profiles share
+	GetStatusBase() *StatusBase
+	// DeepCopyToStatusBaseIf exposes an interface to call deep copy
+	// on this StatusBaseUser interface
+	DeepCopyToStatusBaseIf() StatusBaseUser
+	// Sets the needed status that's specific to the profile
+	// implementation
+	SetImplementationStatus()
+}
diff --git a/api/seccompprofile/v1alpha1/seccompprofile_types.go b/api/seccompprofile/v1alpha1/seccompprofile_types.go
index 2562c3967..f9c18382d 100644
--- a/api/seccompprofile/v1alpha1/seccompprofile_types.go
+++ b/api/seccompprofile/v1alpha1/seccompprofile_types.go
@@ -17,11 +17,22 @@ limitations under the License.
 package v1alpha1
 
 import (
+	"path"
+	"path/filepath"
+	"strings"
+
 	"github.com/containers/common/pkg/seccomp"
-	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+	profilebasev1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilebase/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+)
+
+// Ensure SeccompProfile implements the StatusBaseUser interface.
+var _ profilebasev1alpha1.StatusBaseUser = &SeccompProfile{}
+
+const (
+	extJSON = ".json"
 )
 
 // SeccompProfileSpec defines the desired state of SeccompProfile.
@@ -91,10 +102,9 @@ type Arg struct {
 
 // SeccompProfileStatus contains status of the deployed SeccompProfile.
 type SeccompProfileStatus struct {
-	rcommonv1.ConditionedStatus `json:",inline"`
-	Path                        string                                 `json:"path,omitempty"`
-	Status                      secprofnodestatusv1alpha1.ProfileState `json:"status,omitempty"`
-	ActiveWorkloads             []string                               `json:"activeWorkloads,omitempty"`
+	profilebasev1alpha1.StatusBase `json:",inline"`
+	Path                           string   `json:"path,omitempty"`
+	ActiveWorkloads                []string `json:"activeWorkloads,omitempty"`
 	// The path that should be provided to the `securityContext.seccompProfile.localhostProfile`
 	// field of a Pod or container spec
 	LocalhostProfile string `json:"localhostProfile,omitempty"`
@@ -117,6 +127,31 @@ type SeccompProfile struct {
 	Status SeccompProfileStatus `json:"status,omitempty"`
 }
 
+func (sp *SeccompProfile) GetStatusBase() *profilebasev1alpha1.StatusBase {
+	return &sp.Status.StatusBase
+}
+
+func (sp *SeccompProfile) DeepCopyToStatusBaseIf() profilebasev1alpha1.StatusBaseUser {
+	return sp.DeepCopy()
+}
+
+func (sp *SeccompProfile) SetImplementationStatus() {
+	profilePath := sp.GetProfilePath()
+	sp.Status.LocalhostProfile = strings.TrimPrefix(profilePath, config.KubeletSeccompRootPath+"/")
+}
+
+func (sp *SeccompProfile) GetProfilePath() string {
+	pfile := sp.GetName()
+	if !strings.HasSuffix(pfile, extJSON) {
+		pfile = sp.GetName() + extJSON
+	}
+	return path.Join(
+		config.ProfilesRootPath,
+		filepath.Base(sp.GetNamespace()),
+		filepath.Base(pfile),
+	)
+}
+
 // +kubebuilder:object:root=true
 
 // SeccompProfileList contains a list of SeccompProfile.
diff --git a/api/seccompprofile/v1alpha1/zz_generated.deepcopy.go b/api/seccompprofile/v1alpha1/zz_generated.deepcopy.go
index 2f0aa362d..216f9fb5e 100644
--- a/api/seccompprofile/v1alpha1/zz_generated.deepcopy.go
+++ b/api/seccompprofile/v1alpha1/zz_generated.deepcopy.go
@@ -149,7 +149,7 @@ func (in *SeccompProfileSpec) DeepCopy() *SeccompProfileSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SeccompProfileStatus) DeepCopyInto(out *SeccompProfileStatus) {
 	*out = *in
-	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
+	in.StatusBase.DeepCopyInto(&out.StatusBase)
 	if in.ActiveWorkloads != nil {
 		in, out := &in.ActiveWorkloads, &out.ActiveWorkloads
 		*out = make([]string, len(*in))
diff --git a/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go b/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
index 98b4100ac..dcacacde8 100644
--- a/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
+++ b/api/secprofnodestatus/v1alpha1/secprofnodestatus_types.go
@@ -51,16 +51,20 @@ const (
 	StatusKindLabel = "spo.x-k8s.io/profile-kind"
 )
 
+// LowestState defines the "lowest" state for the profiles to be at.
+// All of the statuses would need to reach this for us to get here.
+const LowestState ProfileState = ProfileStateInstalled
+
 // LowerOfTwoStates is used to figure out the "lowest common state" and is used to represent
 // the overall status of a profile. The idea is that if, e.g. one in three profiles is already
 // installed, but the two others are pending, the overall state should be pending.
 func LowerOfTwoStates(currentLowest, candidate ProfileState) ProfileState {
 	orderedStates := make(map[ProfileState]int)
-	orderedStates[ProfileStateError] = 0 // error must always have the lowest index
-	orderedStates[ProfileStatePending] = 1
-	orderedStates[ProfileStateInProgress] = 2
-	orderedStates[ProfileStateInstalled] = 3
-	orderedStates[ProfileStateTerminating] = 4
+	orderedStates[ProfileStateError] = 0       // error must always have the lowest index
+	orderedStates[ProfileStateTerminating] = 1 // If one is set as terminating; all the statuses will end here too
+	orderedStates[ProfileStatePending] = 2
+	orderedStates[ProfileStateInProgress] = 3
+	orderedStates[ProfileStateInstalled] = 4
 
 	if orderedStates[currentLowest] > orderedStates[candidate] {
 		return candidate
diff --git a/api/selinuxpolicy/v1alpha1/selinuxpolicy_types.go b/api/selinuxpolicy/v1alpha1/selinuxpolicy_types.go
index f5923e32f..55a1fc3c0 100644
--- a/api/selinuxpolicy/v1alpha1/selinuxpolicy_types.go
+++ b/api/selinuxpolicy/v1alpha1/selinuxpolicy_types.go
@@ -17,12 +17,14 @@ limitations under the License.
 package v1alpha1
 
 import (
-	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+	profilebasev1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilebase/v1alpha1"
 )
 
+// Ensure SelinuxPolicy implements the StatusBaseUser interface.
+var _ profilebasev1alpha1.StatusBaseUser = &SelinuxPolicy{}
+
 // SelinuxPolicySpec defines the desired state of SelinuxPolicy.
 type SelinuxPolicySpec struct {
 	Apply  bool   `json:"apply,omitempty"`
@@ -31,12 +33,10 @@ type SelinuxPolicySpec struct {
 
 // SelinuxPolicyStatus defines the observed state of SelinuxPolicy.
 type SelinuxPolicyStatus struct {
-	rcommonv1.ConditionedStatus `json:",inline"`
+	profilebasev1alpha1.StatusBase `json:",inline"`
 	// Represents the string that the SelinuxPolicy object can be
 	// referenced as in a pod seLinuxOptions section.
 	Usage string `json:"usage,omitempty"`
-	// Represents the state that the profile is in.
-	State secprofnodestatusv1alpha1.ProfileState `json:"state,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -46,7 +46,7 @@ type SelinuxPolicyStatus struct {
 // +kubebuilder:resource:path=selinuxpolicies,scope=Namespaced
 // +kubebuilder:printcolumn:name="Usage",type="string",JSONPath=`.status.usage`
 // +kubebuilder:printcolumn:name="Apply",type="boolean",JSONPath=`.spec.apply`
-// +kubebuilder:printcolumn:name="State",type="string",JSONPath=`.status.state`
+// +kubebuilder:printcolumn:name="State",type="string",JSONPath=`.status.status`
 type SelinuxPolicy struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -55,6 +55,30 @@ type SelinuxPolicy struct {
 	Status SelinuxPolicyStatus `json:"status,omitempty"`
 }
 
+func (sp *SelinuxPolicy) GetStatusBase() *profilebasev1alpha1.StatusBase {
+	return &sp.Status.StatusBase
+}
+
+func (sp *SelinuxPolicy) DeepCopyToStatusBaseIf() profilebasev1alpha1.StatusBaseUser {
+	return sp.DeepCopy()
+}
+
+func (sp *SelinuxPolicy) SetImplementationStatus() {
+	sp.Status.Usage = sp.GetPolicyUsage()
+}
+
+// GetPolicyName gets the policy module name in the format that
+// we're expecting for parsing.
+func (sp *SelinuxPolicy) GetPolicyName() string {
+	return sp.GetName() + "_" + sp.GetNamespace()
+}
+
+// GetPolicyUsage is the representation of how a pod will call this
+// SELinux module.
+func (sp *SelinuxPolicy) GetPolicyUsage() string {
+	return sp.GetPolicyName() + ".process"
+}
+
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
 // SelinuxPolicyList contains a list of SelinuxPolicy.
diff --git a/api/selinuxpolicy/v1alpha1/zz_generated.deepcopy.go b/api/selinuxpolicy/v1alpha1/zz_generated.deepcopy.go
index af4287ced..975ecba1a 100644
--- a/api/selinuxpolicy/v1alpha1/zz_generated.deepcopy.go
+++ b/api/selinuxpolicy/v1alpha1/zz_generated.deepcopy.go
@@ -101,7 +101,7 @@ func (in *SelinuxPolicySpec) DeepCopy() *SelinuxPolicySpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SelinuxPolicyStatus) DeepCopyInto(out *SelinuxPolicyStatus) {
 	*out = *in
-	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
+	in.StatusBase.DeepCopyInto(&out.StatusBase)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SelinuxPolicyStatus.
diff --git a/api/spod/v1alpha1/spod_types.go b/api/spod/v1alpha1/spod_types.go
index ae83ce5a8..2efef3b85 100644
--- a/api/spod/v1alpha1/spod_types.go
+++ b/api/spod/v1alpha1/spod_types.go
@@ -63,7 +63,7 @@ type SPODStatus struct {
 
 // SecurityProfilesOperatorDaemon is the Schema to configure the spod deployment.
 // +kubebuilder:subresource:status
-// +kubebuilder:resource:path=securityprofilesoperatordaemons,scope=Cluster,shortName=spod
+// +kubebuilder:resource:path=securityprofilesoperatordaemons,shortName=spod
 // +kubebuilder:printcolumn:name="State",type="string",JSONPath=`.status.state`
 type SecurityProfilesOperatorDaemon struct {
 	metav1.TypeMeta   `json:",inline"`
diff --git a/cmd/security-profiles-operator/main.go b/cmd/security-profiles-operator/main.go
index 02d25b6b5..048d93dfd 100644
--- a/cmd/security-profiles-operator/main.go
+++ b/cmd/security-profiles-operator/main.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/go-logr/logr"
@@ -30,6 +31,7 @@ import (
 	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"
 	"sigs.k8s.io/controller-runtime/pkg/scheme"
@@ -45,7 +47,9 @@ import (
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/profilerecorder"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/seccompprofile"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/selinuxpolicy"
+	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/nonrootenabler"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/version"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/webhooks/binding"
@@ -187,10 +191,7 @@ func runManager(ctx *cli.Context) error {
 		return err
 	}
 
-	// Given that the operator manager only deploys the SPOd and currently
-	// doesn't hold any webhooks, we only listen on the operator's
-	// namespace.
-	ctrlOpts.Namespace = config.GetOperatorNamespace()
+	setControllerOptionsForNamespaces(&ctrlOpts)
 
 	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
 	if err != nil {
@@ -206,6 +207,14 @@ func runManager(ctx *cli.Context) error {
 	if err := seccompprofilev1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
 		return errors.Wrap(err, "add seccompprofile API to scheme")
 	}
+	if err := selinuxpolicyv1alpha1.AddToScheme(mgr.GetScheme()); err != nil {
+		return errors.Wrap(err, "add selinuxpolicy 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")
@@ -215,6 +224,14 @@ func runManager(ctx *cli.Context) error {
 		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")
+	}
+
 	setupLog.Info("starting manager")
 	if err := mgr.Start(sigHandler); err != nil {
 		return errors.Wrap(err, "controller manager error")
@@ -259,6 +276,34 @@ func createConfigIfNotExist(ctx context.Context, c client.Client) error {
 	return nil
 }
 
+func setControllerOptionsForNamespaces(opts *ctrl.Options) {
+	namespace := os.Getenv(config.RestrictNamespaceEnvKey)
+
+	// listen globally
+	if namespace == "" {
+		opts.Namespace = namespace
+		return
+	}
+
+	// ensure we listen to our own namespace
+	if !strings.Contains(namespace, config.GetOperatorNamespace()) {
+		namespace = namespace + "," + config.GetOperatorNamespace()
+	}
+
+	namespaceList := strings.Split(namespace, ",")
+	// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
+	// Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate
+	// Also note that you may face performance issues when using this with a high number of namespaces.
+	// More Info: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder
+	// Adding "" adds cluster namespaced resources
+	if strings.Contains(namespace, ",") {
+		opts.NewCache = cache.MultiNamespacedCacheBuilder(namespaceList)
+	} else {
+		// listen to a specific namespace only
+		opts.Namespace = namespace
+	}
+}
+
 func getTunables() (dt spod.DaemonTunables, err error) {
 	dt.WatchNamespace = os.Getenv(config.RestrictNamespaceEnvKey)
 
@@ -293,7 +338,7 @@ func getEnabledControllers(ctx *cli.Context) []*controllerSettings {
 		&controllerSettings{
 			name:          "recorder-spod",
 			setupFn:       profilerecorder.Setup,
-			schemaBuilder: seccompprofilev1alpha1.SchemeBuilder,
+			schemaBuilder: profilerecording1alpha1.SchemeBuilder,
 		},
 	)
 
@@ -328,9 +373,7 @@ func runDaemon(ctx *cli.Context) error {
 		SyncPeriod: &sync,
 	}
 
-	if os.Getenv(config.RestrictNamespaceEnvKey) != "" {
-		ctrlOpts.Namespace = os.Getenv(config.RestrictNamespaceEnvKey)
-	}
+	setControllerOptionsForNamespaces(&ctrlOpts)
 
 	mgr, err := ctrl.NewManager(cfg, ctrlOpts)
 	if err != nil {
diff --git a/deploy/base/crd.yaml b/deploy/base/crd.yaml
index 6f74320e9..3522a04d9 100644
--- a/deploy/base/crd.yaml
+++ b/deploy/base/crd.yaml
@@ -16,7 +16,7 @@ spec:
     shortNames:
     - spod
     singular: securityprofilesoperatordaemon
-  scope: Cluster
+  scope: Namespaced
   versions:
   - additionalPrinterColumns:
     - jsonPath: .status.state
@@ -479,7 +479,7 @@ spec:
     - jsonPath: .spec.apply
       name: Apply
       type: boolean
-    - jsonPath: .status.state
+    - jsonPath: .status.status
       name: State
       type: string
     name: v1alpha1
@@ -543,8 +543,11 @@ spec:
                   - type
                   type: object
                 type: array
-              state:
-                description: Represents the state that the profile is in.
+              status:
+                description: ProfileState defines the state that the profile is in.
+                  A profile in this context refers to a SeccompProfile or a SELinux
+                  profile, the states are shared between them as well as the management
+                  API.
                 type: string
               usage:
                 description: Represents the string that the SelinuxPolicy object can
diff --git a/deploy/base/profiles/selinuxd.cil b/deploy/base/profiles/selinuxd.cil
index 0e69bd504..0e42778dc 100644
--- a/deploy/base/profiles/selinuxd.cil
+++ b/deploy/base/profiles/selinuxd.cil
@@ -13,7 +13,7 @@
     (allow process file_context_t ( sock_file ( append getattr open read write )))
     (allow process security_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))
     (allow process security_t ( security ( load_policy )))
-    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))
+    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))
     (allow process selinux_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))
     (allow process selinux_config_t ( sock_file ( append getattr open read write )))
     (allow process selinux_login_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))
diff --git a/deploy/base/role.yaml b/deploy/base/role.yaml
index e3ea86dda..e145cab15 100644
--- a/deploy/base/role.yaml
+++ b/deploy/base/role.yaml
@@ -6,6 +6,54 @@ metadata:
   creationTimestamp: null
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - events
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -17,6 +65,31 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/status
+  verbs:
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - securityprofilenodestatuses
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -44,19 +117,10 @@ rules:
   - get
   - patch
   - update
-
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  creationTimestamp: null
-  name: security-profiles-operator
-  namespace: security-profiles-operator
-rules:
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets
+  - selinuxpolicies
   verbs:
   - create
   - get
@@ -65,22 +129,31 @@ rules:
   - update
   - watch
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets/finalizers
+  - selinuxpolicies/finalizers
   verbs:
   - delete
   - get
   - patch
   - update
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - deployments
+  - selinuxpolicies/status
   verbs:
   - get
-  - list
-  - watch
+  - patch
+  - update
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  creationTimestamp: null
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+rules:
 - apiGroups:
   - coordination.k8s.io
   resources:
@@ -89,18 +162,6 @@ rules:
   - create
   - get
   - update
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - events
-  verbs:
-  - create
-  - get
-  - list
-  - patch
-  - update
-  - watch
 - apiGroups:
   - security.openshift.io
   resources:
@@ -115,6 +176,14 @@ metadata:
   creationTimestamp: null
   name: spod
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -215,6 +284,14 @@ metadata:
   name: spod
   namespace: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security.openshift.io
   resources:
diff --git a/deploy/base/webhook/kustomization.yaml b/deploy/base/webhook/kustomization.yaml
index 6ee98efca..602f7f2ef 100644
--- a/deploy/base/webhook/kustomization.yaml
+++ b/deploy/base/webhook/kustomization.yaml
@@ -11,7 +11,7 @@ images:
     # newTag: v0.3.0
 
 commonLabels:
-  app: security-profiles-operator
+  app: security-profiles-operator-webhook
 
 resources:
   - crd-binding.yaml
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 4d71b7e28..7c73b90af 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -16,7 +16,7 @@ spec:
     shortNames:
     - spod
     singular: securityprofilesoperatordaemon
-  scope: Cluster
+  scope: Namespaced
   versions:
   - additionalPrinterColumns:
     - jsonPath: .status.state
@@ -424,7 +424,7 @@ spec:
     - jsonPath: .spec.apply
       name: Apply
       type: boolean
-    - jsonPath: .status.state
+    - jsonPath: .status.status
       name: State
       type: string
     name: v1alpha1
@@ -479,8 +479,8 @@ spec:
                   - type
                   type: object
                 type: array
-              state:
-                description: Represents the state that the profile is in.
+              status:
+                description: ProfileState defines the state that the profile is in. A profile in this context refers to a SeccompProfile or a SELinux profile, the states are shared between them as well as the management API.
                 type: string
               usage:
                 description: Represents the string that the SelinuxPolicy object can be referenced as in a pod seLinuxOptions section.
@@ -529,6 +529,54 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - events
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -540,6 +588,31 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/status
+  verbs:
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - securityprofilenodestatuses
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -567,20 +640,10 @@ rules:
   - get
   - patch
   - update
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  creationTimestamp: null
-  labels:
-    app: security-profiles-operator
-  name: security-profiles-operator
-  namespace: security-profiles-operator
-rules:
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets
+  - selinuxpolicies
   verbs:
   - create
   - get
@@ -589,22 +652,32 @@ rules:
   - update
   - watch
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets/finalizers
+  - selinuxpolicies/finalizers
   verbs:
   - delete
   - get
   - patch
   - update
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - deployments
+  - selinuxpolicies/status
   verbs:
   - get
-  - list
-  - watch
+  - patch
+  - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  creationTimestamp: null
+  labels:
+    app: security-profiles-operator
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+rules:
 - apiGroups:
   - coordination.k8s.io
   resources:
@@ -613,18 +686,6 @@ rules:
   - create
   - get
   - update
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - events
-  verbs:
-  - create
-  - get
-  - list
-  - patch
-  - update
-  - watch
 - apiGroups:
   - security.openshift.io
   resources:
@@ -640,6 +701,14 @@ metadata:
     app: security-profiles-operator
   name: spod
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -741,6 +810,14 @@ metadata:
   name: spod
   namespace: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security.openshift.io
   resources:
@@ -943,7 +1020,7 @@ data:
         }
       ]
     }
-  selinuxd.cil: "(block selinuxd\n    (blockinherit container)\n    (allow process process ( capability ( dac_override dac_read_search lease audit_write audit_control ))) \n\n    (allow process default_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process default_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process default_context_t ( sock_file ( append getattr open read write )))\n    (allow process etc_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process etc_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process etc_t ( sock_file ( append getattr open read write )))\n    (allow process file_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process file_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process file_context_t ( sock_file ( append getattr open read write )))\n    (allow process security_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process security_t ( security ( load_policy )))\n    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_config_t ( sock_file ( append getattr open read write )))\n    (allow process selinux_login_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_login_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_login_config_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_read_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_read_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_read_lock_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_store_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process semanage_store_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_store_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_trans_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_trans_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_trans_lock_t ( sock_file ( append getattr open read write )))\n    (allow process sysfs_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process sysfs_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process sysfs_t ( sock_file ( append getattr open read write )))\n)\n"
+  selinuxd.cil: "(block selinuxd\n    (blockinherit container)\n    (allow process process ( capability ( dac_override dac_read_search lease audit_write audit_control ))) \n\n    (allow process default_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process default_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process default_context_t ( sock_file ( append getattr open read write )))\n    (allow process etc_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process etc_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process etc_t ( sock_file ( append getattr open read write )))\n    (allow process file_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process file_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process file_context_t ( sock_file ( append getattr open read write )))\n    (allow process security_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process security_t ( security ( load_policy )))\n    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process selinux_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_config_t ( sock_file ( append getattr open read write )))\n    (allow process selinux_login_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_login_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_login_config_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_read_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_read_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_read_lock_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_store_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process semanage_store_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_store_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_trans_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_trans_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_trans_lock_t ( sock_file ( append getattr open read write )))\n    (allow process sysfs_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process sysfs_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process sysfs_t ( sock_file ( append getattr open read write )))\n)\n"
 kind: ConfigMap
 metadata:
   labels:
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index e05d31ec1..2a48b3348 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -16,7 +16,7 @@ spec:
     shortNames:
     - spod
     singular: securityprofilesoperatordaemon
-  scope: Cluster
+  scope: Namespaced
   versions:
   - additionalPrinterColumns:
     - jsonPath: .status.state
@@ -424,7 +424,7 @@ spec:
     - jsonPath: .spec.apply
       name: Apply
       type: boolean
-    - jsonPath: .status.state
+    - jsonPath: .status.status
       name: State
       type: string
     name: v1alpha1
@@ -479,8 +479,8 @@ spec:
                   - type
                   type: object
                 type: array
-              state:
-                description: Represents the state that the profile is in.
+              status:
+                description: ProfileState defines the state that the profile is in. A profile in this context refers to a SeccompProfile or a SELinux profile, the states are shared between them as well as the management API.
                 type: string
               usage:
                 description: Represents the string that the SelinuxPolicy object can be referenced as in a pod seLinuxOptions section.
@@ -529,6 +529,54 @@ metadata:
     app: security-profiles-operator
   name: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - events
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -540,6 +588,31 @@ rules:
   - patch
   - update
   - watch
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/finalizers
+  verbs:
+  - delete
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - seccompprofiles/status
+  verbs:
+  - get
+  - patch
+  - update
+- apiGroups:
+  - security-profiles-operator.x-k8s.io
+  resources:
+  - securityprofilenodestatuses
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security-profiles-operator.x-k8s.io
   resources:
@@ -567,20 +640,10 @@ rules:
   - get
   - patch
   - update
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  creationTimestamp: null
-  labels:
-    app: security-profiles-operator
-  name: security-profiles-operator
-  namespace: security-profiles-operator
-rules:
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets
+  - selinuxpolicies
   verbs:
   - create
   - get
@@ -589,22 +652,32 @@ rules:
   - update
   - watch
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - daemonsets/finalizers
+  - selinuxpolicies/finalizers
   verbs:
   - delete
   - get
   - patch
   - update
 - apiGroups:
-  - apps
+  - security-profiles-operator.x-k8s.io
   resources:
-  - deployments
+  - selinuxpolicies/status
   verbs:
   - get
-  - list
-  - watch
+  - patch
+  - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  creationTimestamp: null
+  labels:
+    app: security-profiles-operator
+  name: security-profiles-operator
+  namespace: security-profiles-operator
+rules:
 - apiGroups:
   - coordination.k8s.io
   resources:
@@ -613,18 +686,6 @@ rules:
   - create
   - get
   - update
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - events
-  verbs:
-  - create
-  - get
-  - list
-  - patch
-  - update
-  - watch
 - apiGroups:
   - security.openshift.io
   resources:
@@ -640,6 +701,14 @@ metadata:
     app: security-profiles-operator
   name: spod
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -741,6 +810,14 @@ metadata:
   name: spod
   namespace: security-profiles-operator
 rules:
+- apiGroups:
+  - apps
+  resources:
+  - daemonsets
+  verbs:
+  - get
+  - list
+  - watch
 - apiGroups:
   - security.openshift.io
   resources:
@@ -941,7 +1018,7 @@ data:
         }
       ]
     }
-  selinuxd.cil: "(block selinuxd\n    (blockinherit container)\n    (allow process process ( capability ( dac_override dac_read_search lease audit_write audit_control ))) \n\n    (allow process default_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process default_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process default_context_t ( sock_file ( append getattr open read write )))\n    (allow process etc_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process etc_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process etc_t ( sock_file ( append getattr open read write )))\n    (allow process file_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process file_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process file_context_t ( sock_file ( append getattr open read write )))\n    (allow process security_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process security_t ( security ( load_policy )))\n    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_config_t ( sock_file ( append getattr open read write )))\n    (allow process selinux_login_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_login_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_login_config_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_read_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_read_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_read_lock_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_store_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process semanage_store_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_store_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_trans_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_trans_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_trans_lock_t ( sock_file ( append getattr open read write )))\n    (allow process sysfs_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process sysfs_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process sysfs_t ( sock_file ( append getattr open read write )))\n)\n"
+  selinuxd.cil: "(block selinuxd\n    (blockinherit container)\n    (allow process process ( capability ( dac_override dac_read_search lease audit_write audit_control ))) \n\n    (allow process default_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process default_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process default_context_t ( sock_file ( append getattr open read write )))\n    (allow process etc_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process etc_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process etc_t ( sock_file ( append getattr open read write )))\n    (allow process file_context_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process file_context_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process file_context_t ( sock_file ( append getattr open read write )))\n    (allow process security_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process security_t ( security ( load_policy )))\n    (allow process selinux_config_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process selinux_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_config_t ( sock_file ( append getattr open read write )))\n    (allow process selinux_login_config_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process selinux_login_config_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process selinux_login_config_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_read_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_read_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_read_lock_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_store_t ( dir ( add_name create getattr ioctl lock open read rename remove_name rmdir search setattr write )))\n    (allow process semanage_store_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_store_t ( sock_file ( append getattr open read write )))\n    (allow process semanage_trans_lock_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process semanage_trans_lock_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process semanage_trans_lock_t ( sock_file ( append getattr open read write )))\n    (allow process sysfs_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))\n    (allow process sysfs_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))\n    (allow process sysfs_t ( sock_file ( append getattr open read write )))\n)\n"
 kind: ConfigMap
 metadata:
   labels:
diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml
index 88c8ebec6..9d2ae0037 100644
--- a/deploy/webhook.yaml
+++ b/deploy/webhook.yaml
@@ -5,7 +5,7 @@ metadata:
     controller-gen.kubebuilder.io/version: v0.5.0
   creationTimestamp: null
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: profilebindings.security-profiles-operator.x-k8s.io
 spec:
   group: security-profiles-operator.x-k8s.io
@@ -81,7 +81,7 @@ metadata:
     controller-gen.kubebuilder.io/version: v0.5.0
   creationTimestamp: null
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: profilerecordings.security-profiles-operator.x-k8s.io
 spec:
   group: security-profiles-operator.x-k8s.io
@@ -176,7 +176,7 @@ apiVersion: v1
 kind: ServiceAccount
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-webhook
   namespace: security-profiles-operator
 ---
@@ -185,7 +185,7 @@ kind: ClusterRole
 metadata:
   creationTimestamp: null
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-webhook
 rules:
 - apiGroups:
@@ -277,7 +277,7 @@ kind: Role
 metadata:
   creationTimestamp: null
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-webhook
   namespace: security-profiles-operator
 rules:
@@ -305,7 +305,7 @@ apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-webhook
   namespace: security-profiles-operator
 roleRef:
@@ -321,7 +321,7 @@ apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-webhook
   namespace: security-profiles-operator
 roleRef:
@@ -336,21 +336,21 @@ apiVersion: apps/v1
 kind: Deployment
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: security-profiles-operator-webhook
   namespace: security-profiles-operator
 spec:
   replicas: 3
   selector:
     matchLabels:
-      app: security-profiles-operator
+      app: security-profiles-operator-webhook
       name: security-profiles-operator-webhook
   template:
     metadata:
       annotations:
         seccomp.security.alpha.kubernetes.io/pod: runtime/default
       labels:
-        app: security-profiles-operator
+        app: security-profiles-operator-webhook
         name: security-profiles-operator-webhook
     spec:
       containers:
@@ -393,7 +393,7 @@ metadata:
   annotations:
     cert-manager.io/inject-ca-from: security-profiles-operator/webhook-cert
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: spo-mutating-webhook-configuration
 webhooks:
 - admissionReviewVersions:
@@ -459,7 +459,7 @@ apiVersion: cert-manager.io/v1
 kind: Issuer
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: selfsigned-issuer
   namespace: security-profiles-operator
 spec:
@@ -469,7 +469,7 @@ apiVersion: cert-manager.io/v1
 kind: Certificate
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: webhook-cert
   namespace: security-profiles-operator
 spec:
@@ -485,7 +485,7 @@ apiVersion: v1
 kind: Service
 metadata:
   labels:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
   name: webhook-service
   namespace: security-profiles-operator
 spec:
@@ -493,5 +493,5 @@ spec:
   - port: 443
     targetPort: 9443
   selector:
-    app: security-profiles-operator
+    app: security-profiles-operator-webhook
     name: security-profiles-operator-webhook
diff --git a/go.mod b/go.mod
index 76974f3e4..ee43217f1 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/containers/common v0.35.2
 	github.com/crossplane/crossplane-runtime v0.13.0
 	github.com/go-logr/logr v0.4.0
-	github.com/maxbrunsfeld/counterfeiter/v6 v6.4.0
+	github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.7.0
 	github.com/urfave/cli/v2 v2.3.0
diff --git a/go.sum b/go.sum
index b72184b82..8cbcd11db 100644
--- a/go.sum
+++ b/go.sum
@@ -579,8 +579,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.3/go.mod h1:1ftk08SazyElaaNvmqAfZWGwJzshjCfBXDLoQtPAMNk=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0/go.mod h1:fcEyUyXZXoV4Abw8DX0t7wyL8mCDxXyU4iAFZfT3IHw=
-github.com/maxbrunsfeld/counterfeiter/v6 v6.4.0 h1:yQ9bMx6/ubkDPprq4U3RILwS1Y8K0P6eLifsH2T9Pvs=
-github.com/maxbrunsfeld/counterfeiter/v6 v6.4.0/go.mod h1:DK1Cjkc0E49ShgRVs5jy5ASrM15svSnem3K/hiSGD8o=
+github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1 h1:hZD/8vBuw7x1WqRXD/WGjVjipbbo/HcDBgySYYbrUSk=
+github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1/go.mod h1:DK1Cjkc0E49ShgRVs5jy5ASrM15svSnem3K/hiSGD8o=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 4aa1608bf..46105668b 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -49,6 +49,10 @@ const (
 	// the name of the current node.
 	NodeNameEnvKey = "NODE_NAME"
 
+	// OperatorNamespaceEnvKey is the default environment variable key for retrieving
+	// the operator's namespace.
+	OperatorNamespaceEnvKey = "OPERATOR_NAMESPACE"
+
 	// RestrictNamespaceEnvKey is the environment variable key for restricting
 	// the operator to work on only a single Kubernetes namespace.
 	RestrictNamespaceEnvKey = "RESTRICT_TO_NAMESPACE"
@@ -79,7 +83,7 @@ func GetOperatorNamespace() string {
 func TryToGetOperatorNamespace() (string, error) {
 	// This is OPERATOR_NAMESPACE should have been set by the downward API to identify
 	// the namespace which this controller is running from
-	operatorNS := util.EnvDefault("OPERATOR_NAMESPACE", "")
+	operatorNS := util.EnvDefault(OperatorNamespaceEnvKey, "")
 	if operatorNS == "" {
 		return "", ErrPodNamespaceEnvNotFound
 	}
diff --git a/internal/pkg/daemon/profilerecorder/profilerecorder.go b/internal/pkg/daemon/profilerecorder/profilerecorder.go
index 4b1999b85..e6c9d0a45 100644
--- a/internal/pkg/daemon/profilerecorder/profilerecorder.go
+++ b/internal/pkg/daemon/profilerecorder/profilerecorder.go
@@ -149,6 +149,7 @@ func (r *RecorderReconciler) isPodWithTraceAnnotation(obj runtime.Object) bool {
 	return false
 }
 
+// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
 func (r *RecorderReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
 	logger := r.log.WithValues("pod", req.Name, "namespace", req.Namespace)
 
diff --git a/internal/pkg/daemon/seccompprofile/seccompprofile.go b/internal/pkg/daemon/seccompprofile/seccompprofile.go
index 322e02bff..b035e3720 100644
--- a/internal/pkg/daemon/seccompprofile/seccompprofile.go
+++ b/internal/pkg/daemon/seccompprofile/seccompprofile.go
@@ -24,25 +24,18 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
-	"path/filepath"
-	"strings"
 	"time"
 
 	"github.com/containers/common/pkg/seccomp"
-	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"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"
-	kerrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
-	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+	statusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/nodestatus"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
@@ -52,8 +45,7 @@ const (
 	// default reconcile timeout.
 	reconcileTimeout = 1 * time.Minute
 
-	shortWait = 2 * time.Second
-	wait      = 30 * time.Second
+	wait = 10 * time.Second
 
 	errGetProfile          = "cannot get profile"
 	errSeccompProfileNil   = "seccomp profile cannot be nil"
@@ -68,65 +60,16 @@ const (
 
 	reasonSeccompNotSupported   event.Reason = "SeccompNotSupportedOnNode"
 	reasonInvalidSeccompProfile event.Reason = "InvalidSeccompProfile"
-	reasonCannotGetProfilePath  event.Reason = "CannotGetSeccompProfilePath"
 	reasonCannotSaveProfile     event.Reason = "CannotSaveSeccompProfile"
 	reasonCannotRemoveProfile   event.Reason = "CannotRemoveSeccompProfile"
 	reasonCannotUpdateProfile   event.Reason = "CannotUpdateSeccompProfile"
+	reasonCannotUpdateStatus    event.Reason = "CannotUpdateNodeStatus"
 
 	reasonSavedProfile event.Reason = "SavedSeccompProfile"
-
-	extJSON = ".json"
-
-	spOwnerKey    = ".metadata.seccompProfileOwner"
-	linkedPodsKey = ".metadata.activeWorkloads"
 )
 
-func hasSeccompProfile(obj runtime.Object) bool {
-	pod, ok := obj.(*corev1.Pod)
-	if !ok {
-		return false
-	}
-
-	return len(getSeccompProfilesFromPod(pod)) > 0
-}
-
 // Setup adds a controller that reconciles seccomp profiles.
 func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
-	// Index Pods using seccomp profiles
-	if err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Pod{}, spOwnerKey, func(rawObj client.Object) []string {
-		pod, ok := rawObj.(*corev1.Pod)
-		if !ok {
-			return []string{}
-		}
-		return getSeccompProfilesFromPod(pod)
-	}); err != nil {
-		return errors.Wrap(err, "creating pod index")
-	}
-
-	// Index SeccompProfiles with active pods
-	if err := mgr.GetFieldIndexer().IndexField(
-		ctx, &v1alpha1.SeccompProfile{}, linkedPodsKey, func(rawObj client.Object) []string {
-			sp, ok := rawObj.(*v1alpha1.SeccompProfile)
-			if !ok {
-				return []string{}
-			}
-			return sp.Status.ActiveWorkloads
-		}); err != nil {
-		return errors.Wrap(err, "creating seccomp profile index")
-	}
-
-	// Register a special reconciler for pod events
-	if err := ctrl.NewControllerManagedBy(mgr).
-		Named("pods").
-		For(&corev1.Pod{}).
-		WithEventFilter(resource.NewPredicates(hasSeccompProfile)).
-		Complete(&PodReconciler{
-			client: mgr.GetClient(),
-			log:    l,
-			record: event.NewAPIRecorder(mgr.GetEventRecorderFor("pods")),
-		}); err != nil {
-		return errors.Wrap(err, "creating pod controller")
-	}
 	// Register the regular reconciler to manage SeccompProfiles
 	return ctrl.NewControllerManagedBy(mgr).
 		Named("profile").
@@ -149,22 +92,17 @@ type Reconciler struct {
 	save   saver
 }
 
-// A PodReconciler monitors pod changes and links them to SeccompProfiles.
-type PodReconciler struct {
-	client client.Client
-	log    logr.Logger
-	record event.Recorder
-}
-
 // Security Profiles Operator RBAC permissions to manage SeccompProfile
 // nolint:lll
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles,verbs=get;list;watch;create;update;patch
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles/finalizers,verbs=delete;get;update;patch
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=securityprofilenodestatuses,verbs=get;list;watch;create;update;patch;delete
-// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
 // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
 
+// +kubebuilder:rbac:groups=apps,namespace="security-profiles-operator",resources=daemonsets,verbs=get;list;watch
+// +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch
+
 // OpenShift ... This is ignored in other distros
 // nolint:lll
 // +kubebuilder:rbac:groups=security.openshift.io,namespace="security-profiles-operator",resources=securitycontextconstraints,verbs=use
@@ -195,7 +133,7 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
 	seccompProfile := &v1alpha1.SeccompProfile{}
 	if err := r.client.Get(ctx, req.NamespacedName, seccompProfile); err != nil {
 		// Expected to find a SeccompProfile, return an error and requeue
-		return reconcile.Result{}, errors.Wrap(ignoreNotFound(err), errGetProfile)
+		return reconcile.Result{}, errors.Wrap(util.IgnoreNotFound(err), errGetProfile)
 	}
 
 	return r.reconcileSeccompProfile(ctx, seccompProfile, logger)
@@ -267,11 +205,15 @@ func (r *Reconciler) reconcileSeccompProfile(
 	}
 	profileName := sp.Name
 
-	nodeStatus, err := nodestatus.NewForProfile(sp)
+	nodeStatus, err := nodestatus.NewForProfile(sp, r.client)
 	if err != nil {
 		return reconcile.Result{}, errors.Wrap(err, "cannot create nodeStatus")
 	}
 
+	if !sp.GetDeletionTimestamp().IsZero() { // object is being deleted
+		return r.reconcileDeletion(ctx, sp, nodeStatus)
+	}
+
 	outputProfile, err := r.mergeBaseProfile(ctx, sp, l)
 	if err != nil {
 		return reconcile.Result{RequeueAfter: wait}, nil
@@ -280,85 +222,47 @@ func (r *Reconciler) reconcileSeccompProfile(
 	if err != nil {
 		l.Error(err, "cannot validate profile "+profileName)
 		r.record.Event(sp, event.Warning(reasonInvalidSeccompProfile, err))
-		return reconcile.Result{RequeueAfter: wait}, nil
+		return reconcile.Result{}, errors.Wrap(err, "cannot validate profile")
 	}
 
-	profilePath, err := GetProfilePath(profileName, sp.ObjectMeta.Namespace)
-	if err != nil {
-		l.Error(err, "cannot get profile path")
-		r.record.Event(sp, event.Warning(reasonCannotGetProfilePath, err))
-		return reconcile.Result{RequeueAfter: wait}, nil
-	}
+	profilePath := sp.GetProfilePath()
 
-	if !sp.GetDeletionTimestamp().IsZero() { // object is being deleted
-		setTerminatingStatus := func(sp *v1alpha1.SeccompProfile) *v1alpha1.SeccompProfileStatus {
-			status := sp.Status.DeepCopy()
-			status.SetConditions(rcommonv1.Deleting())
-			status.Status = secprofnodestatusv1alpha1.ProfileStateTerminating
-			return status
-		}
+	// The object is not being deleted
+	exists, existErr := nodeStatus.Exists(ctx)
 
-		if err := r.setBothStatuses(ctx, nodeStatus, sp, setTerminatingStatus); err != nil {
-			l.Error(err, "cannot update SeccompProfile status")
-			r.record.Event(sp, event.Warning(reasonCannotUpdateProfile, err))
-			return reconcile.Result{}, errors.Wrap(err, "updating status for deleted SeccompProfile")
-		}
-
-		hasStatus, err := nodeStatus.Exists(ctx, r.client)
-		if err != nil || !hasStatus {
-			return ctrl.Result{}, errors.Wrap(err, "checking if node status exists")
-		}
-
-		if err := handleDeletion(sp, l); err != nil {
-			l.Error(err, "cannot delete profile")
-			r.record.Event(sp, event.Warning(reasonCannotRemoveProfile, err))
-			return ctrl.Result{}, errors.Wrap(err, "handling file deletion for deleted SeccompProfile")
-		}
-
-		if err := nodeStatus.Remove(ctx, r.client); err != nil {
-			l.Error(err, "cannot remove node status/finalizer from seccomp profile")
-			r.record.Event(sp, event.Warning(reasonCannotUpdateProfile, err))
-			return ctrl.Result{}, errors.Wrap(err, "deleting node status/finalizer for deleted SeccompProfile")
-		}
-		return ctrl.Result{}, nil
+	if existErr != nil {
+		return reconcile.Result{}, errors.Wrap(existErr, "checking if node status exists")
 	}
 
-	hasNodeStatus, err := r.ensureNodeStatus(ctx, nodeStatus, sp)
-	if err != nil {
-		l.Error(err, "cannot create SeccompProfile status/finalizers")
-		r.record.Event(sp, event.Warning(reasonCannotUpdateProfile, err))
-		return reconcile.Result{}, errors.Wrap(err, "adding status/finalizer for SeccompProfile")
-	}
-
-	if !hasNodeStatus {
-		l.Info("profile node status created, reconciling")
-		return reconcile.Result{RequeueAfter: shortWait}, nil
+	if !exists {
+		if err := nodeStatus.Create(ctx); err != nil {
+			return reconcile.Result{}, errors.Wrap(err, "cannot ensure node status")
+		}
+		l.Info("Created an initial status for this node")
+		return reconcile.Result{RequeueAfter: wait}, nil
 	}
 
 	updated, err := r.save(profilePath, profileContent)
 	if err != nil {
 		l.Error(err, "cannot save profile into disk")
 		r.record.Event(sp, event.Warning(reasonCannotSaveProfile, err))
-		return reconcile.Result{RequeueAfter: wait}, nil
+		return reconcile.Result{}, errors.Wrap(err, "cannot save profile into disk")
 	}
 
-	// refresh reference
-	if err := r.client.Get(ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-		return reconcile.Result{}, errors.Wrap(ignoreNotFound(err), errGetProfile)
+	isAlreadyInstalled, getErr := nodeStatus.Matches(ctx, statusv1alpha1.ProfileStateInstalled)
+	if getErr != nil {
+		l.Error(err, "couldn't get current status")
+		return reconcile.Result{}, errors.Wrap(err, "getting status for installed SeccompProfile")
 	}
 
-	setInstalledStatus := func(sp *v1alpha1.SeccompProfile) *v1alpha1.SeccompProfileStatus {
-		status := sp.Status.DeepCopy()
-		status.Path = profilePath
-		status.SetConditions(rcommonv1.Available())
-		status.Status = secprofnodestatusv1alpha1.ProfileStateInstalled
-		status.LocalhostProfile = strings.TrimPrefix(profilePath, config.KubeletSeccompRootPath+"/")
-		return status
+	if isAlreadyInstalled {
+		l.Info("Already in the expected Installed state")
+		return reconcile.Result{}, nil
 	}
 
-	if err := r.setBothStatuses(ctx, nodeStatus, sp, setInstalledStatus); err != nil {
-		l.Error(err, "cannot update SeccompProfile status")
-		r.record.Event(sp, event.Warning(reasonCannotUpdateProfile, err))
+	if err := nodeStatus.SetNodeStatus(ctx, statusv1alpha1.ProfileStateInstalled); err != nil {
+		l.Error(err, "cannot update node status")
+		r.record.Event(sp, event.Warning(reasonCannotUpdateStatus, err))
 		return reconcile.Result{}, errors.Wrap(err, "updating status in SeccompProfile reconciler")
 	}
 
@@ -374,242 +278,52 @@ func (r *Reconciler) reconcileSeccompProfile(
 	return reconcile.Result{}, nil
 }
 
-// setBothStatuses checks if the node status of a SeccompProfile is in sync with the supplied
-// SeccompProfileStatus and updates the node status if not. Additionally, the status of the
-// SeccompProfile is set to the lowest common denominator as well.
-func (r *Reconciler) setBothStatuses(
-	ctx context.Context, ns *nodestatus.StatusClient,
-	sp *v1alpha1.SeccompProfile, setStatusFn func(sp *v1alpha1.SeccompProfile) *v1alpha1.SeccompProfileStatus,
-) error {
-	if retryErr := util.Retry(func() error {
-		if err := r.client.Get(ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-			return errors.Wrap(err, "retrieving profile")
-		}
-
-		status := setStatusFn(sp)
-
-		profileStatus, err := ns.SetReturnGlobal(ctx, r.client, status.Status)
-		if err != nil {
-			return errors.Wrap(err, "setting per-node status")
-		}
-		status.Status = profileStatus
-		return r.setStatus(ctx, sp, status)
-	}, util.IsNotFoundOrConflict); retryErr != nil {
-		return errors.Wrap(retryErr, "updating profile status")
-	}
-
-	return nil
-}
-
-func (r *Reconciler) ensureNodeStatus(
-	ctx context.Context, nodeStatus *nodestatus.StatusClient, sp *v1alpha1.SeccompProfile) (bool, error) {
-	nodeStatusExists, err := nodeStatus.Exists(ctx, r.client)
+func (r *Reconciler) reconcileDeletion(
+	ctx context.Context,
+	sp *v1alpha1.SeccompProfile,
+	nsc *nodestatus.StatusClient,
+) (reconcile.Result, error) {
+	hasStatus, err := nsc.Exists(ctx)
 	if err != nil {
-		return false, errors.Wrap(err, "Retrieving node status")
-	}
-
-	if !nodeStatusExists {
-		if err := nodeStatus.Create(ctx, r.client); err != nil {
-			return nodeStatusExists, errors.Wrap(err, "Creating node status")
-		}
+		return ctrl.Result{}, errors.Wrap(err, "checking if node status exists")
 	}
 
-	if err := util.Retry(func() error {
-		if sp.Status.Status != "" {
-			return nil
+	// Set the status if it hasn't been deleted already
+	if hasStatus {
+		isTerminating, getErr := nsc.Matches(ctx, statusv1alpha1.ProfileStateTerminating)
+		if getErr != nil {
+			r.log.Error(err, "couldn't get current status")
+			return reconcile.Result{}, errors.Wrap(err, "getting status for deleted SeccompProfile")
 		}
 
-		profileCopy := sp.DeepCopy()
-		profileCopy.Status.Status = secprofnodestatusv1alpha1.ProfileStatePending
-		profileCopy.Status.SetConditions(rcommonv1.Creating())
-
-		updateErr := r.client.Status().Update(context.TODO(), profileCopy)
-		if updateErr != nil {
-			if err := r.client.Get(
-				ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-				return errors.Wrap(err, "retrieving profile")
+		if !isTerminating {
+			r.log.Info("setting status to terminating")
+			if err := nsc.SetNodeStatus(ctx, statusv1alpha1.ProfileStateTerminating); err != nil {
+				r.log.Error(err, "cannot update SeccompProfile status")
+				r.record.Event(sp, event.Warning(reasonCannotUpdateProfile, err))
+				return reconcile.Result{}, errors.Wrap(err, "updating status for deleted SeccompProfile")
 			}
+			return reconcile.Result{Requeue: true, RequeueAfter: wait}, nil
 		}
-		return errors.Wrap(updateErr, "updating to initial state")
-	}, util.IsNotFoundOrConflict); err != nil {
-		return nodeStatusExists, errors.Wrap(err, "Updating seccomp status to PENDING")
 	}
 
-	return nodeStatusExists, nil
-}
-
-// setStatus checks if the Status of a SeccompProfile is in sync with the supplied
-// SeccompProfileStatus and updates the SeccompProfile if not.
-func (r *Reconciler) setStatus(
-	ctx context.Context, sp *v1alpha1.SeccompProfile, status *v1alpha1.SeccompProfileStatus) error {
-	if sp.Status.Status == status.Status &&
-		sp.Status.Path == status.Path &&
-		sp.Status.LocalhostProfile == status.LocalhostProfile {
-		return nil
-	}
-
-	sp.Status = *status
-	if err := r.client.Status().Update(ctx, sp); err != nil {
-		return errors.Wrap(err, "setting SeccompProfile status")
-	}
-	return nil
-}
-
-// Namespace scoped
-// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
-
-// Reconcile reacts to pod events and updates SeccompProfiles if in use or no longer in use by a pod.
-func (r *PodReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
-	logger := r.log.WithValues("pod", req.Name, "namespace", req.Namespace)
-
-	ctx, cancel := context.WithTimeout(context.Background(), reconcileTimeout)
-	defer cancel()
-
-	podID := req.Namespace + "/" + req.Name
-
-	pod := &corev1.Pod{}
-	var err error
-	if err = r.client.Get(ctx, req.NamespacedName, pod); ignoreNotFound(err) != nil {
-		logger.Error(err, "could not get pod")
-		return reconcile.Result{}, errors.Wrap(err, "looking up pod in pod reconciler")
-	}
-	if kerrors.IsNotFound(err) { // this is a pod deletion, so update all seccomp profiles that were using it
-		seccompProfiles := &v1alpha1.SeccompProfileList{}
-		if err = r.client.List(ctx, seccompProfiles, client.MatchingFields{linkedPodsKey: podID}); err != nil {
-			return reconcile.Result{}, errors.Wrap(err, "listing SeccompProfiles for deleted pod")
-		}
-		for i := range seccompProfiles.Items {
-			if err = r.updatePodReferences(ctx, &seccompProfiles.Items[i]); err != nil {
-				return reconcile.Result{}, errors.Wrap(err, "updating SeccompProfile for deleted pod")
-			}
-		}
-		return reconcile.Result{}, nil
-	}
-	// pod is being created or updated so ensure it is linked to a seccomp profile
-	for _, profileIndex := range getSeccompProfilesFromPod(pod) {
-		profileElements := strings.Split(profileIndex, "/")
-		profileNamespace := profileElements[1]
-		profileName := strings.TrimSuffix(profileElements[2], ".json")
-		seccompProfile := &v1alpha1.SeccompProfile{}
-		if err := r.client.Get(ctx, util.NamespacedName(profileName, profileNamespace), seccompProfile); err != nil {
-			logger.Error(err, "could not get seccomp profile for pod")
-			return reconcile.Result{}, errors.Wrap(err, "looking up SeccompProfile for new or updated pod")
-		}
-		if err := r.updatePodReferences(ctx, seccompProfile); err != nil {
-			logger.Error(err, "could not update seccomp profile for pod")
-			return reconcile.Result{}, errors.Wrap(err, "updating SeccompProfile pod references for new or updated pod")
-		}
-	}
-	return reconcile.Result{}, nil
-}
-
-// updatePodReferences updates a SeccompProfile with the identifiers of pods using it and ensures
-// it has a finalizer indicating it is in use to prevent it from being deleted.
-func (r *PodReconciler) updatePodReferences(ctx context.Context, sp *v1alpha1.SeccompProfile) error {
-	linkedPods := &corev1.PodList{}
-	profileReference := fmt.Sprintf("operator/%s/%s.json", sp.GetNamespace(), sp.GetName())
-	err := r.client.List(ctx, linkedPods, client.MatchingFields{spOwnerKey: profileReference})
-	if ignoreNotFound(err) != nil {
-		return errors.Wrap(err, "listing pods to update seccompProfile")
-	}
-	podList := make([]string, len(linkedPods.Items))
-	for i := range linkedPods.Items {
-		pod := linkedPods.Items[i]
-		podList[i] = pod.ObjectMeta.Namespace + "/" + pod.ObjectMeta.Name
-	}
-	if err := util.Retry(func() error {
-		sp.Status.ActiveWorkloads = podList
-
-		updateErr := r.client.Status().Update(ctx, sp)
-		if updateErr != nil {
-			if err := r.client.Get(ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-				return errors.Wrap(err, "retrieving profile")
-			}
-		}
-
-		return errors.Wrap(updateErr, "updating profile")
-	}, util.IsNotFoundOrConflict); err != nil {
-		return errors.Wrap(err, "updating SeccompProfile status")
-	}
-	hasActivePodsFinalizerString := "in-use-by-active-pods"
-	if len(linkedPods.Items) > 0 {
-		if err := util.Retry(func() error {
-			return util.AddFinalizer(ctx, r.client, sp, hasActivePodsFinalizerString)
-		}, util.IsNotFoundOrConflict); err != nil {
-			return errors.Wrap(err, "adding finalizer")
-		}
-	} else {
-		if err := util.Retry(func() error {
-			return util.RemoveFinalizer(ctx, r.client, sp, hasActivePodsFinalizerString)
-		}, util.IsNotFoundOrConflict); err != nil {
-			return errors.Wrap(err, "removing finalizer")
-		}
+	if err := handleDeletion(sp, r.log); err != nil {
+		r.log.Error(err, "cannot delete profile")
+		r.record.Event(sp, event.Warning(reasonCannotRemoveProfile, err))
+		return ctrl.Result{}, errors.Wrap(err, "handling file deletion for deleted SeccompProfile")
 	}
-	return nil
-}
 
-// getSeccompProfilesFromPod returns a slice of strings representing seccomp profiles required by the pod.
-// It looks first at the pod spec level, then in each container and init container, then in the annotations.
-func getSeccompProfilesFromPod(pod *corev1.Pod) []string {
-	profiles := []string{}
-	// try to get profile from pod securityContext
-	sc := pod.Spec.SecurityContext
-	if sc != nil && isOperatorSeccompProfile(sc.SeccompProfile) {
-		profiles = append(profiles, *sc.SeccompProfile.LocalhostProfile)
-	}
-	// try to get profile(s) from securityContext in pods
-	containers := append(pod.Spec.Containers, pod.Spec.InitContainers...)
-	contains := func(a []string, b string) bool {
-		for _, s := range a {
-			if s == b {
-				return true
-			}
-		}
-		return false
-	}
-	for i := range containers {
-		sc := containers[i].SecurityContext
-		if sc != nil && isOperatorSeccompProfile(sc.SeccompProfile) {
-			profileString := *containers[i].SecurityContext.SeccompProfile.LocalhostProfile
-			if !contains(profiles, profileString) {
-				profiles = append(profiles, profileString)
-			}
-		}
-	}
-	// try to get profile from annotations
-	annotation, hasAnnotation := pod.GetAnnotations()[corev1.SeccompPodAnnotationKey]
-	if hasAnnotation && strings.HasPrefix(annotation, "localhost/") {
-		profileString := strings.TrimPrefix(annotation, "localhost/")
-		if !contains(profiles, profileString) {
-			profiles = append(profiles, profileString)
-		}
+	if err := nsc.Remove(ctx, r.client); err != nil {
+		r.log.Error(err, "cannot remove node status/finalizer from seccomp profile")
+		r.record.Event(sp, event.Warning(reasonCannotUpdateStatus, err))
+		return ctrl.Result{}, errors.Wrap(err, "deleting node status/finalizer for deleted SeccompProfile")
 	}
-	return profiles
-}
-
-// isOperatorSeccompProfile checks whether a corev1.SeccompProfile object belongs to the operator.
-// SeccompProfiles controlled by the operator are of type "Localhost" and have a path of the form
-// "operator/namespace/profile-name.json".
-func isOperatorSeccompProfile(sp *corev1.SeccompProfile) bool {
-	if sp == nil || sp.Type != "Localhost" {
-		return false
-	}
-	if !strings.HasPrefix(*sp.LocalhostProfile, "operator/") {
-		return false
-	}
-	if !strings.HasSuffix(*sp.LocalhostProfile, ".json") {
-		return false
-	}
-	const pathParts = 3
-	return len(strings.Split(*sp.LocalhostProfile, "/")) == pathParts
+	return ctrl.Result{}, nil
 }
 
 func handleDeletion(sp *v1alpha1.SeccompProfile, l logr.Logger) error {
-	profilePath, err := GetProfilePath(sp.GetName(), sp.GetNamespace())
-	if err != nil {
-		return err
-	}
-	err = os.Remove(profilePath)
+	profilePath := sp.GetProfilePath()
+	err := os.Remove(profilePath)
 	if os.IsNotExist(err) {
 		return nil
 	}
@@ -636,22 +350,3 @@ func saveProfileOnDisk(fileName string, content []byte) (updated bool, err error
 
 	return true, nil
 }
-
-// GetProfilePath returns the full path for the provided profile name and config.
-func GetProfilePath(profileName, namespace string) (string, error) {
-	if filepath.Ext(profileName) != extJSON {
-		profileName += extJSON
-	}
-	return path.Join(
-		config.ProfilesRootPath,
-		filepath.Base(namespace),
-		filepath.Base(profileName),
-	), nil
-}
-
-func ignoreNotFound(err error) error {
-	if kerrors.IsNotFound(err) {
-		return nil
-	}
-	return err
-}
diff --git a/internal/pkg/daemon/selinuxpolicy/selinuxpolicy_controller.go b/internal/pkg/daemon/selinuxpolicy/selinuxpolicy_controller.go
index aa0e985a2..12b343564 100644
--- a/internal/pkg/daemon/selinuxpolicy/selinuxpolicy_controller.go
+++ b/internal/pkg/daemon/selinuxpolicy/selinuxpolicy_controller.go
@@ -30,7 +30,6 @@ import (
 	"text/template"
 	"time"
 
-	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
@@ -39,11 +38,10 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
-	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
+	statusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
 	selinuxpolicyv1alpha1 "sigs.k8s.io/security-profiles-operator/api/selinuxpolicy/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/spod/bindata"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/nodestatus"
-	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
 
 // The underscore is not a valid character in a pod, so we can
@@ -106,36 +104,30 @@ func (r *ReconcileSP) Reconcile(_ context.Context, request reconcile.Request) (r
 		return reconcile.Result{}, IgnoreNotFound(err)
 	}
 
-	nodeStatus, err := nodestatus.NewForProfile(instance)
+	nodeStatus, err := nodestatus.NewForProfile(instance, r.client)
 	if err != nil {
-		return reconcile.Result{}, errors.Wrap(err, "cannot create nodeStatus")
-	}
-
-	if instance.Status.Usage != GetPolicyUsage(instance.Name, instance.Namespace) {
-		if err := r.addUsageStatus(instance); err != nil {
-			return reconcile.Result{}, err
-		}
-		return reconcile.Result{Requeue: true}, nil
+		return reconcile.Result{}, errors.Wrap(err, "cannot create nodeStatus instance")
 	}
 
 	if instance.ObjectMeta.DeletionTimestamp.IsZero() {
+		ctx := context.Background()
 		// The object is not being deleted
-		if err := r.ensureNodeStatus(context.TODO(), nodeStatus, instance); err != nil {
-			return reconcile.Result{}, errors.Wrap(err, "ensuring node status exists")
+		exists, existErr := nodeStatus.Exists(ctx)
+
+		if existErr != nil {
+			return reconcile.Result{}, errors.Wrap(existErr, "checking if node status exists")
 		}
 
-		return r.reconcilePolicy(instance, nodeStatus, reqLogger)
-	}
+		if !exists {
+			if err := nodeStatus.Create(ctx); err != nil {
+				return reconcile.Result{}, errors.Wrap(err, "cannot ensure node status")
+			}
+		}
 
-	// The object is being deleted
-	setTerminatingStatus := func(sp *selinuxpolicyv1alpha1.SelinuxPolicy) *selinuxpolicyv1alpha1.SelinuxPolicyStatus {
-		statusCopy := instance.Status.DeepCopy()
-		statusCopy.SetConditions(rcommonv1.Deleting())
-		statusCopy.State = secprofnodestatusv1alpha1.ProfileStateTerminating
-		return statusCopy
+		return r.reconcilePolicy(instance, nodeStatus, reqLogger)
 	}
 
-	if err := r.setBothStatuses(context.TODO(), nodeStatus, instance, setTerminatingStatus); err != nil {
+	if err := nodeStatus.SetNodeStatus(context.TODO(), statusv1alpha1.ProfileStateTerminating); err != nil {
 		reqLogger.Error(err, "cannot update SELinux profile status")
 		return reconcile.Result{}, errors.Wrap(err, "updating status for deleted SELinux profile")
 	}
@@ -143,7 +135,7 @@ func (r *ReconcileSP) Reconcile(_ context.Context, request reconcile.Request) (r
 	// since the nodeStatus API always removes both the node status and the node's finalizer in sync,
 	// this condition will only be true after both are gone and therefore when the profile is really
 	// gone from the node
-	hasStatus, err := nodeStatus.Exists(context.TODO(), r.client)
+	hasStatus, err := nodeStatus.Exists(context.TODO())
 	if err != nil || !hasStatus {
 		return reconcile.Result{}, errors.Wrap(err, "asserting if node status exists")
 	}
@@ -161,109 +153,6 @@ func (r *ReconcileSP) Reconcile(_ context.Context, request reconcile.Request) (r
 	return reconcile.Result{}, nil
 }
 
-func (r *ReconcileSP) ensureNodeStatus(
-	ctx context.Context, nodeStatus *nodestatus.StatusClient, sp *selinuxpolicyv1alpha1.SelinuxPolicy,
-) error {
-	if err := nodeStatus.Create(ctx, r.client); err != nil {
-		return errors.Wrap(err, "cannot ensure node status")
-	}
-
-	// Retry makes sense here as we're updating the shared state directly from multiple nodes
-	if err := util.Retry(func() error {
-		if sp.Status.State != "" && sp.Spec.Apply {
-			return nil
-		}
-
-		policyCopy := sp.DeepCopy()
-		if sp.Status.State == "" {
-			// Set up an initial state
-			policyCopy.Status.State = secprofnodestatusv1alpha1.ProfileStatePending
-			policyCopy.Status.SetConditions(rcommonv1.Creating())
-		} else if !sp.Spec.Apply {
-			// If "apply" is false, no need to do anything, let the deployer
-			// review it.
-			policyCopy.Status.State = secprofnodestatusv1alpha1.ProfileStatePending
-			policyCopy.Status.SetConditions(rcommonv1.Unavailable())
-		}
-
-		updateErr := r.client.Status().Update(context.TODO(), policyCopy)
-		if updateErr != nil {
-			if err := r.client.Get(
-				ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-				return errors.Wrap(err, "retrieving profile")
-			}
-		}
-		return errors.Wrap(updateErr, "updating to initial state")
-	}, util.IsNotFoundOrConflict); err != nil {
-		return errors.Wrap(err, "Updating SelinuxPolicy status to PENDING")
-	}
-
-	return nil
-}
-
-// setBothStatuses checks if the node status of a SELinux profile is in sync with the supplied
-// SELinux profile and updates the node status if not. Additionally, the status of the
-// SELinux profile is set to the lowest common denominator as well.
-func (r *ReconcileSP) setBothStatuses(
-	ctx context.Context, ns *nodestatus.StatusClient,
-	sp *selinuxpolicyv1alpha1.SelinuxPolicy,
-	setStatusFn func(sp *selinuxpolicyv1alpha1.SelinuxPolicy) *selinuxpolicyv1alpha1.SelinuxPolicyStatus,
-) error {
-	if retryErr := util.Retry(func() error {
-		if err := r.client.Get(ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
-			return errors.Wrap(err, "retrieving profile")
-		}
-
-		status := setStatusFn(sp)
-
-		policyState, err := ns.SetReturnGlobal(ctx, r.client, status.State)
-		if err != nil {
-			return errors.Wrap(err, "setting per-node status")
-		}
-
-		if sp.Status.State == policyState {
-			// avoid needless writes
-			return nil
-		}
-
-		// this should already be a deep-copy and the only thing we're changing is a scalar..
-		sp.Status = *status
-		if err := r.client.Status().Update(ctx, sp); err != nil {
-			return errors.Wrap(err, "setting SELinux profile status")
-		}
-		return nil
-	}, util.IsNotFoundOrConflict); retryErr != nil {
-		return errors.Wrap(retryErr, "updating policy status")
-	}
-
-	return nil
-}
-
-func (r *ReconcileSP) reconcileInProgressPolicy(
-	nodeStatus *nodestatus.StatusClient, sp *selinuxpolicyv1alpha1.SelinuxPolicy,
-) error {
-	setInProgressStatus := func(sp *selinuxpolicyv1alpha1.SelinuxPolicy) *selinuxpolicyv1alpha1.SelinuxPolicyStatus {
-		statusCopy := sp.Status.DeepCopy()
-		statusCopy.SetConditions(rcommonv1.Creating())
-		statusCopy.State = secprofnodestatusv1alpha1.ProfileStateInProgress
-		return statusCopy
-	}
-
-	if err := r.setBothStatuses(context.TODO(), nodeStatus, sp, setInProgressStatus); err != nil {
-		return errors.Wrap(err, "setting both statuses to in progress")
-	}
-	return nil
-}
-
-func (r *ReconcileSP) addUsageStatus(sp *selinuxpolicyv1alpha1.SelinuxPolicy) error {
-	spcopy := sp.DeepCopy()
-	spcopy.Status.Usage = GetPolicyUsage(spcopy.Name, spcopy.Namespace)
-	if err := r.client.Status().Update(context.Background(), spcopy); err != nil {
-		return errors.Wrap(err, "Updating SelinuxPolicy usage status")
-	}
-	return nil
-}
-
 func (r *ReconcileSP) reconcilePolicy(
 	sp *selinuxpolicyv1alpha1.SelinuxPolicy, nodeStatus *nodestatus.StatusClient, l logr.Logger,
 ) (reconcile.Result, error) {
@@ -281,43 +170,39 @@ func (r *ReconcileSP) reconcilePolicy(
 		return reconcile.Result{}, errors.Wrap(err, "Creating policy file")
 	}
 
-	l.Info("Checking if policy is installed", "policyName", sp.Name)
+	l.Info("Checking if policy deployed", "policyName", sp.Name)
 	polStatus, err := getPolicyStatus(sp)
 	if err != nil {
 		return reconcile.Result{}, errors.Wrap(err, "Looking up policy status")
 	}
 
 	if polStatus == nil {
-		if err := r.reconcileInProgressPolicy(nodeStatus, sp); err != nil {
-			return reconcile.Result{}, errors.Wrap(err, "updating in progress status")
+		if err := nodeStatus.SetNodeStatus(context.TODO(), statusv1alpha1.ProfileStateInProgress); err != nil {
+			return reconcile.Result{}, errors.Wrap(err, "setting node status to in progress")
 		}
 		return reconcile.Result{Requeue: true}, nil
 	}
 
-	l.Info("Policy installed")
-	setInstallationStatus := func(sp *selinuxpolicyv1alpha1.SelinuxPolicy) *selinuxpolicyv1alpha1.SelinuxPolicyStatus {
-		statusCopy := sp.Status.DeepCopy()
+	var polState statusv1alpha1.ProfileState
 
-		switch polStatus.Status {
-		case installedStatus:
-			statusCopy.State = secprofnodestatusv1alpha1.ProfileStateInstalled
-			statusCopy.SetConditions(rcommonv1.Available())
-		case failedStatus:
-			statusCopy.State = secprofnodestatusv1alpha1.ProfileStateError
-			statusCopy.SetConditions(rcommonv1.Unavailable())
-		}
-		return statusCopy
+	switch polStatus.Status {
+	case installedStatus:
+		polState = statusv1alpha1.ProfileStateInstalled
+	case failedStatus:
+		polState = statusv1alpha1.ProfileStateError
 	}
 
-	if err := r.setBothStatuses(context.TODO(), nodeStatus, sp, setInstallationStatus); err != nil {
-		return reconcile.Result{}, errors.Wrap(err, "Updating SELinux policy with installation success")
+	l.Info("Policy deployed", "status", polState)
+
+	if err := nodeStatus.SetNodeStatus(context.TODO(), polState); err != nil {
+		return reconcile.Result{}, errors.Wrap(err, "setting profile status")
 	}
 
 	return reconcile.Result{}, nil
 }
 
 func (r *ReconcileSP) reconcilePolicyFile(sp *selinuxpolicyv1alpha1.SelinuxPolicy, l logr.Logger) error {
-	policyPath := path.Join(bindata.SelinuxDropDirectory, GetPolicyName(sp.Name, sp.Namespace)+".cil")
+	policyPath := path.Join(bindata.SelinuxDropDirectory, sp.GetPolicyName()+".cil")
 	policyContent := []byte(r.wrapPolicy(sp))
 
 	l.Info("Writing to policy file", "policyPath", policyPath)
@@ -386,13 +271,7 @@ func (r *ReconcileSP) reconcileDeletePolicy(
 			l.Info("Policy still installed, requeue")
 			return reconcile.Result{Requeue: true}, nil
 		case failedStatus:
-			setFailedStatus := func(sp *selinuxpolicyv1alpha1.SelinuxPolicy) *selinuxpolicyv1alpha1.SelinuxPolicyStatus {
-				statusCopy := sp.Status.DeepCopy()
-				statusCopy.State = secprofnodestatusv1alpha1.ProfileStateError
-				statusCopy.SetConditions(rcommonv1.Unavailable())
-				return statusCopy
-			}
-			if err := r.setBothStatuses(context.TODO(), nodeStatus, sp, setFailedStatus); err != nil {
+			if err := nodeStatus.SetNodeStatus(context.TODO(), statusv1alpha1.ProfileStateError); err != nil {
 				return reconcile.Result{}, errors.Wrap(err, "Updating SELinux policy with installation success")
 			}
 
@@ -406,7 +285,7 @@ func (r *ReconcileSP) reconcileDeletePolicy(
 
 func (r *ReconcileSP) reconcileDeletePolicyFile(sp *selinuxpolicyv1alpha1.SelinuxPolicy,
 	l logr.Logger) (reconcile.Result, error) {
-	policyPath := path.Join(bindata.SelinuxDropDirectory, GetPolicyName(sp.Name, sp.Namespace)+".cil")
+	policyPath := path.Join(bindata.SelinuxDropDirectory, sp.GetPolicyName()+".cil")
 
 	l.Info("Removing policy file", "policyPath", policyPath)
 	err := os.Remove(policyPath)
@@ -486,7 +365,7 @@ func isSelinuxdReady() (bool, error) {
 }
 
 func getPolicyStatus(sp *selinuxpolicyv1alpha1.SelinuxPolicy) (*sePolStatus, error) {
-	polURL := selinuxdPoliciesBaseURL + GetPolicyName(sp.Name, sp.Namespace)
+	polURL := selinuxdPoliciesBaseURL + sp.GetPolicyName()
 	response, err := selinuxdGetRequest(polURL)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to send a request to selinuxd")
diff --git a/internal/pkg/daemon/selinuxpolicy/utils.go b/internal/pkg/daemon/selinuxpolicy/utils.go
index 4637436c0..82f52104d 100644
--- a/internal/pkg/daemon/selinuxpolicy/utils.go
+++ b/internal/pkg/daemon/selinuxpolicy/utils.go
@@ -30,18 +30,6 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 )
 
-// GetPolicyName gets the policy module name in the format that
-// we're expecting for parsing.
-func GetPolicyName(name, ns string) string {
-	return name + "_" + ns
-}
-
-// GetPolicyUsage is the representation of how a pod will call this
-// SELinux module.
-func GetPolicyUsage(name, ns string) string {
-	return GetPolicyName(name, ns) + ".process"
-}
-
 // GetPolicyK8sName gets the policy name in a format that's OK for k8s names.
 func GetPolicyK8sName(name, ns string) string {
 	return name + "-" + ns
diff --git a/internal/pkg/manager/nodestatus/nodestatus.go b/internal/pkg/manager/nodestatus/nodestatus.go
new file mode 100644
index 000000000..3309a35d8
--- /dev/null
+++ b/internal/pkg/manager/nodestatus/nodestatus.go
@@ -0,0 +1,282 @@
+/*
+Copyright 2021 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 nodestatus
+
+import (
+	"context"
+	"time"
+
+	rcommonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/selection"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	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/selinuxpolicy/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
+)
+
+const (
+	reconcileTimeout = 1 * time.Minute
+	dsWait           = 30 * time.Second
+)
+
+var (
+	ErrNoOwnerProfile  = errors.New("No owner profile defined for this status")
+	ErrUnkownOwnerKind = errors.New("the node status owner is of an unknown kind")
+)
+
+// A StatusReconciler monitors node changes and updates the profile status.
+type StatusReconciler struct {
+	client client.Client
+	log    logr.Logger
+	record event.Recorder
+}
+
+func NewStatusReconciler(cli client.Client, log logr.Logger, record event.Recorder) *StatusReconciler {
+	return &StatusReconciler{
+		client: cli,
+		log:    log,
+		record: record,
+	}
+}
+
+// Security Profiles Operator RBAC permissions to manage SelinuxPolicy
+// nolint:lll
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=selinuxpolicies,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=selinuxpolicies/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=selinuxpolicies/finalizers,verbs=delete;get;update;patch
+
+// Security Profiles Operator RBAC permissions to manage SeccompProfile
+// nolint:lll
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=seccompprofiles/finalizers,verbs=delete;get;update;patch
+
+// Security Profiles Operator RBAC permissions to manage Node Statuses
+// nolint:lll
+// +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=securityprofilenodestatuses,verbs=get;list;watch
+
+// Reconcile reconciles a NodeStatus.
+func (r *StatusReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), reconcileTimeout)
+	defer cancel()
+
+	logger := r.log.WithValues("nodeStatus", req.Name, "namespace", req.Namespace)
+	logger.Info("Reconciling node status")
+
+	// get the status to be reconciled
+	instance := &statusv1alpha1.SecurityProfileNodeStatus{}
+	if err := r.client.Get(ctx, req.NamespacedName, instance); err != nil {
+		// Expected to find a node profile, return an error and requeue
+		// TODO(jhrozek): ignore not found?
+		return reconcile.Result{}, util.IgnoreNotFound(err)
+	}
+
+	prof, getProfErr := r.getProfileFromStatus(ctx, instance)
+	if getProfErr != nil {
+		return reconcile.Result{}, getProfErr
+	}
+
+	lprof := logger.WithValues(
+		"Profile.Name", prof.GetName(),
+		"Profile.Namespace", prof.GetNamespace(),
+		"Profile.Kind", prof.GetObjectKind().GroupVersionKind(),
+	)
+
+	// get all the other statuses
+	profName := instance.Labels[statusv1alpha1.StatusToProfLabel]
+	if profName == "" {
+		return reconcile.Result{}, errors.New("Unlabeled node status")
+	}
+
+	if prof.GetName() != profName {
+		return reconcile.Result{}, errors.New("status doesn't match owner")
+	}
+
+	// Initialize status if it hasn't happened already
+	if prof.GetStatusBase().Status == "" {
+		lprof.Info("Initializing Profile status")
+
+		targetStatus := statusv1alpha1.ProfileStatePending
+		if instance.Status != "" {
+			targetStatus = instance.Status
+		}
+		return r.reconcileStatus(ctx, prof, targetStatus, lprof)
+	}
+
+	nodeStatusList, err := listStatusesForProfile(ctx, r.client, profName)
+	if err != nil {
+		return reconcile.Result{}, errors.Wrap(err, "cannot list the node statuses")
+	}
+
+	// get the DS
+	spodDS, err := r.getDS(ctx, config.GetOperatorNamespace(), lprof)
+	if err != nil {
+		return reconcile.Result{}, errors.Wrap(err, "cannot get the DS")
+	}
+
+	if !daemonSetIsReady(spodDS) || daemonSetIsUpdating(spodDS) {
+		// If the DS is not ready or updating, don't bother updating the status
+		logger.Info("Not updating policy because the SPOd is not ready")
+		return reconcile.Result{RequeueAfter: dsWait}, nil
+	}
+
+	// make sure we have all the statuses already
+	hasStatuses := len(nodeStatusList.Items)
+	wantsStatuses := spodDS.Status.DesiredNumberScheduled
+	if wantsStatuses != int32(hasStatuses) {
+		logger.Info("Not updating policy: not all statuses are ready",
+			"has", hasStatuses, "wants", wantsStatuses)
+		// Don't reconcile again, let's just wait for another update
+		return reconcile.Result{}, nil
+	}
+
+	var lowestCommonState statusv1alpha1.ProfileState = statusv1alpha1.LowestState
+	for i := range nodeStatusList.Items {
+		lowestCommonState = statusv1alpha1.LowerOfTwoStates(lowestCommonState, nodeStatusList.Items[i].Status)
+	}
+	logger.Info("Setting the status to", "Status", lowestCommonState)
+
+	return r.reconcileStatus(ctx, prof, lowestCommonState, lprof)
+}
+
+func (r *StatusReconciler) getDS(ctx context.Context, namespace string, l logr.Logger) (*appsv1.DaemonSet, error) {
+	dsSelect := labels.NewSelector()
+	spodDSFilter, err := labels.NewRequirement("spod", selection.Exists, []string{})
+	if err != nil {
+		return nil, errors.Wrap(err, "cannot create DS list label")
+	}
+	dsSelect.Add(*spodDSFilter)
+	dsListOpts := client.ListOptions{
+		LabelSelector: dsSelect,
+		Namespace:     namespace,
+	}
+
+	spodDSList := appsv1.DaemonSetList{}
+	if err := r.client.List(ctx, &spodDSList, &dsListOpts); err != nil {
+		return nil, errors.Wrap(err, "cannot list DS")
+	}
+
+	if len(spodDSList.Items) != 1 {
+		retErr := errors.New("Did not find exactly one DS")
+		l.Error(retErr, "Expected to find 1 DS", "len(dsList.Items)", len(spodDSList.Items))
+		return nil, errors.Wrap(retErr, "listing DS")
+	}
+
+	return &spodDSList.Items[0], nil
+}
+
+func (r *StatusReconciler) getProfileFromStatus(
+	ctx context.Context,
+	s *statusv1alpha1.SecurityProfileNodeStatus,
+) (pbv1alpha1.StatusBaseUser, error) {
+	ctrl := metav1.GetControllerOf(s)
+	if ctrl == nil {
+		return nil, errors.Wrapf(ErrNoOwnerProfile, "getting owner profile")
+	}
+
+	key := types.NamespacedName{
+		Name:      ctrl.Name,
+		Namespace: s.GetNamespace(),
+	}
+	var prof pbv1alpha1.StatusBaseUser
+	switch ctrl.Kind {
+	case "SeccompProfile":
+		prof = &seccompv1alpha1.SeccompProfile{}
+	case "SelinuxPolicy":
+		prof = &selinuxv1alpha1.SelinuxPolicy{}
+	default:
+		return nil, errors.Wrapf(ErrUnkownOwnerKind, "getting owner profile")
+	}
+	if err := r.client.Get(ctx, key, prof); err != nil {
+		return nil, errors.Wrapf(err, "getting owner profile: %s/%s", s.GetNamespace(), ctrl.Name)
+	}
+	return prof, nil
+}
+
+func (r *StatusReconciler) reconcileStatus(
+	ctx context.Context,
+	prof pbv1alpha1.StatusBaseUser,
+	state statusv1alpha1.ProfileState,
+	l logr.Logger,
+) (reconcile.Result, error) {
+	pCopy := prof.DeepCopyToStatusBaseIf()
+
+	// We always set this status
+	pCopy.SetImplementationStatus()
+
+	outStatus := pCopy.GetStatusBase()
+	switch state {
+	case statusv1alpha1.ProfileStatePending, "":
+		outStatus.Status = statusv1alpha1.ProfileStatePending
+		outStatus.SetConditions(rcommonv1.Creating())
+	case statusv1alpha1.ProfileStateInProgress:
+		outStatus.SetConditions(rcommonv1.Creating())
+		outStatus.Status = statusv1alpha1.ProfileStateInProgress
+	case statusv1alpha1.ProfileStateInstalled:
+		outStatus.Status = statusv1alpha1.ProfileStateInstalled
+		outStatus.SetConditions(rcommonv1.Available())
+	case statusv1alpha1.ProfileStateTerminating:
+		outStatus.Status = statusv1alpha1.ProfileStateTerminating
+		outStatus.SetConditions(rcommonv1.Deleting())
+	case statusv1alpha1.ProfileStateError:
+		outStatus.Status = statusv1alpha1.ProfileStateError
+		outStatus.SetConditions(rcommonv1.Unavailable())
+	}
+
+	l.Info("Updating status")
+	if updateErr := r.client.Status().Update(ctx, pCopy); updateErr != nil {
+		return reconcile.Result{}, errors.Wrapf(updateErr, "Updating policy status")
+	}
+
+	return reconcile.Result{}, nil
+}
+
+func daemonSetIsReady(ds *appsv1.DaemonSet) bool {
+	return ds.Status.DesiredNumberScheduled > 0 && ds.Status.DesiredNumberScheduled == ds.Status.NumberAvailable
+}
+
+func daemonSetIsUpdating(ds *appsv1.DaemonSet) bool {
+	return ds.Status.UpdatedNumberScheduled > 0 &&
+		(ds.Status.UpdatedNumberScheduled < ds.Status.DesiredNumberScheduled || ds.Status.NumberUnavailable > 0)
+}
+
+func listStatusesForProfile(
+	ctx context.Context, c client.Client, polName string,
+) (*statusv1alpha1.SecurityProfileNodeStatusList, error) {
+	statusList := statusv1alpha1.SecurityProfileNodeStatusList{}
+	allStatusesForProf := client.MatchingLabels{
+		statusv1alpha1.StatusToProfLabel: polName,
+	}
+
+	if err := c.List(ctx, &statusList, allStatusesForProf); err != nil {
+		return nil, errors.Wrap(err, "listing statuses")
+	}
+
+	return &statusList, nil
+}
diff --git a/internal/pkg/manager/nodestatus/setup.go b/internal/pkg/manager/nodestatus/setup.go
new file mode 100644
index 000000000..2a7d9c3ec
--- /dev/null
+++ b/internal/pkg/manager/nodestatus/setup.go
@@ -0,0 +1,39 @@
+/*
+Copyright 2021 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 nodestatus
+
+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"
+)
+
+// Setup adds a controller that reconciles the SPOd DaemonSet.
+func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
+	// Register a special reconciler for status events
+	return ctrl.NewControllerManagedBy(mgr).
+		Named("nodestatus").
+		For(&secprofnodestatusv1alpha1.SecurityProfileNodeStatus{}).
+		Complete(NewStatusReconciler(mgr.GetClient(),
+			l,
+			event.NewAPIRecorder(mgr.GetEventRecorderFor("nodestatus"))),
+		)
+}
diff --git a/internal/pkg/manager/spod/bindata/spod.go b/internal/pkg/manager/spod/bindata/spod.go
index 9b0b2b7ba..fc7c39c60 100644
--- a/internal/pkg/manager/spod/bindata/spod.go
+++ b/internal/pkg/manager/spod/bindata/spod.go
@@ -241,6 +241,14 @@ semodule -i /opt/spo-profiles/selinuxd.cil
 									},
 								},
 							},
+							{
+								Name: "OPERATOR_NAMESPACE",
+								ValueFrom: &v1.EnvVarSource{
+									FieldRef: &v1.ObjectFieldSelector{
+										FieldPath: "metadata.namespace",
+									},
+								},
+							},
 						},
 					},
 					{
diff --git a/internal/pkg/manager/spod/setup.go b/internal/pkg/manager/spod/setup.go
index c3285dc23..2b9509730 100644
--- a/internal/pkg/manager/spod/setup.go
+++ b/internal/pkg/manager/spod/setup.go
@@ -20,9 +20,11 @@ import (
 	"context"
 
 	"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"
 
 	spodv1alpha1 "sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1"
@@ -44,6 +46,7 @@ func Setup(ctx context.Context, mgr ctrl.Manager, dt *DaemonTunables, l logr.Log
 		Named("spod-config").
 		For(&spodv1alpha1.SecurityProfilesOperatorDaemon{}).
 		Owns(&appsv1.DaemonSet{}).
+		WithEventFilter(resource.NewPredicates(isInOperatorNamespace)).
 		Complete(&ReconcileSPOd{
 			baseSPOd:       getEffectiveSPOd(dt),
 			client:         mgr.GetClient(),
@@ -56,6 +59,7 @@ func Setup(ctx context.Context, mgr ctrl.Manager, dt *DaemonTunables, l logr.Log
 
 func getEffectiveSPOd(dt *DaemonTunables) *appsv1.DaemonSet {
 	refSPOd := bindata.Manifest.DeepCopy()
+	refSPOd.SetNamespace(config.GetOperatorNamespace())
 
 	daemon := &refSPOd.Spec.Template.Spec.Containers[0]
 	if dt.WatchNamespace != "" {
@@ -75,3 +79,12 @@ func getEffectiveSPOd(dt *DaemonTunables) *appsv1.DaemonSet {
 	sepolImage.Image = dt.SelinuxdImage // selinuxd ships the policies as well
 	return refSPOd
 }
+
+func isInOperatorNamespace(obj runtime.Object) bool {
+	spod, ok := obj.(*spodv1alpha1.SecurityProfilesOperatorDaemon)
+	if !ok {
+		return false
+	}
+
+	return spod.GetNamespace() == config.GetOperatorNamespace()
+}
diff --git a/internal/pkg/manager/spod/spod_controller.go b/internal/pkg/manager/spod/spod_controller.go
index e2d5c3978..ea4fe1899 100644
--- a/internal/pkg/manager/spod/spod_controller.go
+++ b/internal/pkg/manager/spod/spod_controller.go
@@ -63,16 +63,16 @@ type ReconcileSPOd struct {
 // nolint:lll
 //
 // Used for leader election:
-// +kubebuilder:rbac:groups=core,namespace="security-profiles-operator",resources=configmaps;events,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=core,resources=configmaps;events,verbs=get;list;watch;create;update;patch
 //
 // Operand:
-// +kubebuilder:rbac:groups=apps,namespace="security-profiles-operator",resources=daemonsets,verbs=get;list;watch;create;update;patch
-// +kubebuilder:rbac:groups=apps,namespace="security-profiles-operator",resources=daemonsets/finalizers,verbs=delete;get;update;patch
+// +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=apps,resources=daemonsets/finalizers,verbs=delete;get;update;patch
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=securityprofilesoperatordaemons,verbs=get;list;watch;create;update
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=securityprofilesoperatordaemons/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=security-profiles-operator.x-k8s.io,resources=securityprofilesoperatordaemons/finalizers,verbs=delete;get;update;patch
 // Helpers:
-// +kubebuilder:rbac:groups=apps,namespace="security-profiles-operator",resources=deployments,verbs=get;list;watch;
+// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;
 // +kubebuilder:rbac:groups=coordination.k8s.io,namespace="security-profiles-operator",resources=leases,verbs=create;get;update;
 //
 // Needed for default profiles:
diff --git a/internal/pkg/manager/workloadannotator/setup.go b/internal/pkg/manager/workloadannotator/setup.go
new file mode 100644
index 000000000..06f0f9803
--- /dev/null
+++ b/internal/pkg/manager/workloadannotator/setup.go
@@ -0,0 +1,78 @@
+/*
+Copyright 2021 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 workloadannotator
+
+import (
+	"context"
+
+	"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"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+)
+
+// Setup adds a controller that reconciles the SPOd DaemonSet.
+func Setup(ctx context.Context, mgr ctrl.Manager, l logr.Logger) error {
+	// Index Pods using seccomp profiles
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Pod{}, spOwnerKey, func(rawObj client.Object) []string {
+		pod, ok := rawObj.(*corev1.Pod)
+		if !ok {
+			return []string{}
+		}
+		return getSeccompProfilesFromPod(pod)
+	}); err != nil {
+		return errors.Wrap(err, "creating pod index")
+	}
+
+	// Index SeccompProfiles with active pods
+	if err := mgr.GetFieldIndexer().IndexField(
+		ctx, &v1alpha1.SeccompProfile{}, linkedPodsKey, func(rawObj client.Object) []string {
+			sp, ok := rawObj.(*v1alpha1.SeccompProfile)
+			if !ok {
+				return []string{}
+			}
+			return sp.Status.ActiveWorkloads
+		}); err != nil {
+		return errors.Wrap(err, "creating seccomp profile index")
+	}
+
+	// Register a special reconciler for pod events
+	return ctrl.NewControllerManagedBy(mgr).
+		Named("pods").
+		For(&corev1.Pod{}).
+		WithEventFilter(resource.NewPredicates(hasSeccompProfile)).
+		Complete(&PodReconciler{
+			client: mgr.GetClient(),
+			log:    l,
+			record: event.NewAPIRecorder(mgr.GetEventRecorderFor("pods")),
+		})
+}
+
+func hasSeccompProfile(obj runtime.Object) bool {
+	pod, ok := obj.(*corev1.Pod)
+	if !ok {
+		return false
+	}
+
+	return len(getSeccompProfilesFromPod(pod)) > 0
+}
diff --git a/internal/pkg/manager/workloadannotator/workloadannotator.go b/internal/pkg/manager/workloadannotator/workloadannotator.go
new file mode 100644
index 000000000..4116b078f
--- /dev/null
+++ b/internal/pkg/manager/workloadannotator/workloadannotator.go
@@ -0,0 +1,197 @@
+/*
+Copyright 2021 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 workloadannotator
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
+)
+
+const (
+	spOwnerKey       = ".metadata.seccompProfileOwner"
+	linkedPodsKey    = ".metadata.activeWorkloads"
+	reconcileTimeout = 1 * time.Minute
+)
+
+// A PodReconciler monitors pod changes and links them to SeccompProfiles.
+type PodReconciler struct {
+	client client.Client
+	log    logr.Logger
+	record event.Recorder
+}
+
+// Namespace scoped
+// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
+
+// Reconcile reacts to pod events and updates SeccompProfiles if in use or no longer in use by a pod.
+func (r *PodReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
+	logger := r.log.WithValues("pod", req.Name, "namespace", req.Namespace)
+
+	ctx, cancel := context.WithTimeout(context.Background(), reconcileTimeout)
+	defer cancel()
+
+	podID := req.Namespace + "/" + req.Name
+
+	pod := &corev1.Pod{}
+	var err error
+	if err = r.client.Get(ctx, req.NamespacedName, pod); util.IgnoreNotFound(err) != nil {
+		logger.Error(err, "could not get pod")
+		return reconcile.Result{}, errors.Wrap(err, "looking up pod in pod reconciler")
+	}
+	if kerrors.IsNotFound(err) { // this is a pod deletion, so update all seccomp profiles that were using it
+		seccompProfiles := &v1alpha1.SeccompProfileList{}
+		if err = r.client.List(ctx, seccompProfiles, client.MatchingFields{linkedPodsKey: podID}); err != nil {
+			return reconcile.Result{}, errors.Wrap(err, "listing SeccompProfiles for deleted pod")
+		}
+		for i := range seccompProfiles.Items {
+			if err = r.updatePodReferences(ctx, &seccompProfiles.Items[i]); err != nil {
+				return reconcile.Result{}, errors.Wrap(err, "updating SeccompProfile for deleted pod")
+			}
+		}
+		return reconcile.Result{}, nil
+	}
+	// pod is being created or updated so ensure it is linked to a seccomp profile
+	for _, profileIndex := range getSeccompProfilesFromPod(pod) {
+		profileElements := strings.Split(profileIndex, "/")
+		profileNamespace := profileElements[1]
+		profileName := strings.TrimSuffix(profileElements[2], ".json")
+		seccompProfile := &v1alpha1.SeccompProfile{}
+		if err := r.client.Get(ctx, util.NamespacedName(profileName, profileNamespace), seccompProfile); err != nil {
+			logger.Error(err, "could not get seccomp profile for pod")
+			return reconcile.Result{}, errors.Wrap(err, "looking up SeccompProfile for new or updated pod")
+		}
+		if err := r.updatePodReferences(ctx, seccompProfile); err != nil {
+			logger.Error(err, "could not update seccomp profile for pod")
+			return reconcile.Result{}, errors.Wrap(err, "updating SeccompProfile pod references for new or updated pod")
+		}
+	}
+	return reconcile.Result{}, nil
+}
+
+// updatePodReferences updates a SeccompProfile with the identifiers of pods using it and ensures
+// it has a finalizer indicating it is in use to prevent it from being deleted.
+func (r *PodReconciler) updatePodReferences(ctx context.Context, sp *v1alpha1.SeccompProfile) error {
+	linkedPods := &corev1.PodList{}
+	profileReference := fmt.Sprintf("operator/%s/%s.json", sp.GetNamespace(), sp.GetName())
+	err := r.client.List(ctx, linkedPods, client.MatchingFields{spOwnerKey: profileReference})
+	if util.IgnoreNotFound(err) != nil {
+		return errors.Wrap(err, "listing pods to update seccompProfile")
+	}
+	podList := make([]string, len(linkedPods.Items))
+	for i := range linkedPods.Items {
+		pod := linkedPods.Items[i]
+		podList[i] = pod.ObjectMeta.Namespace + "/" + pod.ObjectMeta.Name
+	}
+	if err := util.Retry(func() error {
+		sp.Status.ActiveWorkloads = podList
+
+		updateErr := r.client.Status().Update(ctx, sp)
+		if updateErr != nil {
+			if err := r.client.Get(ctx, util.NamespacedName(sp.GetName(), sp.GetNamespace()), sp); err != nil {
+				return errors.Wrap(err, "retrieving profile")
+			}
+		}
+
+		return errors.Wrap(updateErr, "updating profile")
+	}, util.IsNotFoundOrConflict); err != nil {
+		return errors.Wrap(err, "updating SeccompProfile status")
+	}
+	hasActivePodsFinalizerString := "in-use-by-active-pods"
+	if len(linkedPods.Items) > 0 {
+		if err := util.Retry(func() error {
+			return util.AddFinalizer(ctx, r.client, sp, hasActivePodsFinalizerString)
+		}, util.IsNotFoundOrConflict); err != nil {
+			return errors.Wrap(err, "adding finalizer")
+		}
+	} else {
+		if err := util.Retry(func() error {
+			return util.RemoveFinalizer(ctx, r.client, sp, hasActivePodsFinalizerString)
+		}, util.IsNotFoundOrConflict); err != nil {
+			return errors.Wrap(err, "removing finalizer")
+		}
+	}
+	return nil
+}
+
+// getSeccompProfilesFromPod returns a slice of strings representing seccomp profiles required by the pod.
+// It looks first at the pod spec level, then in each container and init container, then in the annotations.
+func getSeccompProfilesFromPod(pod *corev1.Pod) []string {
+	profiles := []string{}
+	// try to get profile from pod securityContext
+	sc := pod.Spec.SecurityContext
+	if sc != nil && isOperatorSeccompProfile(sc.SeccompProfile) {
+		profiles = append(profiles, *sc.SeccompProfile.LocalhostProfile)
+	}
+	// try to get profile(s) from securityContext in pods
+	containers := append(pod.Spec.Containers, pod.Spec.InitContainers...)
+	contains := func(a []string, b string) bool {
+		for _, s := range a {
+			if s == b {
+				return true
+			}
+		}
+		return false
+	}
+	for i := range containers {
+		sc := containers[i].SecurityContext
+		if sc != nil && isOperatorSeccompProfile(sc.SeccompProfile) {
+			profileString := *containers[i].SecurityContext.SeccompProfile.LocalhostProfile
+			if !contains(profiles, profileString) {
+				profiles = append(profiles, profileString)
+			}
+		}
+	}
+	// try to get profile from annotations
+	annotation, hasAnnotation := pod.GetAnnotations()[corev1.SeccompPodAnnotationKey]
+	if hasAnnotation && strings.HasPrefix(annotation, "localhost/") {
+		profileString := strings.TrimPrefix(annotation, "localhost/")
+		if !contains(profiles, profileString) {
+			profiles = append(profiles, profileString)
+		}
+	}
+	return profiles
+}
+
+// isOperatorSeccompProfile checks whether a corev1.SeccompProfile object belongs to the operator.
+// SeccompProfiles controlled by the operator are of type "Localhost" and have a path of the form
+// "operator/namespace/profile-name.json".
+func isOperatorSeccompProfile(sp *corev1.SeccompProfile) bool {
+	if sp == nil || sp.Type != "Localhost" {
+		return false
+	}
+	if !strings.HasPrefix(*sp.LocalhostProfile, "operator/") {
+		return false
+	}
+	if !strings.HasSuffix(*sp.LocalhostProfile, ".json") {
+		return false
+	}
+	const pathParts = 3
+	return len(strings.Split(*sp.LocalhostProfile, "/")) == pathParts
+}
diff --git a/internal/pkg/nodestatus/nodestatus.go b/internal/pkg/nodestatus/nodestatus.go
index 370b14d81..85161f008 100644
--- a/internal/pkg/nodestatus/nodestatus.go
+++ b/internal/pkg/nodestatus/nodestatus.go
@@ -36,9 +36,10 @@ type StatusClient struct {
 	pol             client.Object
 	nodeName        string
 	finalizerString string
+	client          client.Client
 }
 
-func NewForProfile(pol client.Object) (*StatusClient, error) {
+func NewForProfile(pol client.Object, c client.Client) (*StatusClient, error) {
 	nodeName, ok := os.LookupEnv(config.NodeNameEnvKey)
 	if !ok {
 		return nil, errors.New("cannot determine node name")
@@ -47,6 +48,7 @@ func NewForProfile(pol client.Object) (*StatusClient, error) {
 		pol:             pol,
 		nodeName:        nodeName,
 		finalizerString: nodeName + "-delete",
+		client:          c,
 	}, nil
 }
 
@@ -58,21 +60,21 @@ func (nsf *StatusClient) perNodeStatusNamespacedName() types.NamespacedName {
 	return util.NamespacedName(nsf.perNodeStatusName(), nsf.pol.GetNamespace())
 }
 
-func (nsf *StatusClient) Create(ctx context.Context, c client.Client) error {
-	if err := nsf.createFinalizer(ctx, c); err != nil {
+func (nsf *StatusClient) Create(ctx context.Context) error {
+	if err := nsf.createFinalizer(ctx); err != nil {
 		return errors.Wrapf(err, "cannot create finalizer for %s", nsf.pol.GetName())
 	}
 
 	// if object does not exist, add it
-	if err := nsf.createNodeStatus(ctx, c); err != nil {
+	if err := nsf.createNodeStatus(ctx); err != nil {
 		return errors.Wrapf(err, "cannot create node status for %s", nsf.pol.GetName())
 	}
 	return nil
 }
 
-func (nsf *StatusClient) createFinalizer(ctx context.Context, c client.Client) error {
+func (nsf *StatusClient) createFinalizer(ctx context.Context) error {
 	return util.Retry(func() error {
-		return util.AddFinalizer(ctx, c, nsf.pol, nsf.finalizerString)
+		return util.AddFinalizer(ctx, nsf.client, nsf.pol, nsf.finalizerString)
 	}, util.IsNotFoundOrConflict)
 }
 
@@ -95,8 +97,12 @@ func (nsf *StatusClient) statusObj(
 	}
 }
 
-func (nsf *StatusClient) createNodeStatus(ctx context.Context, c client.Client) error {
-	err := c.Create(ctx, nsf.statusObj(secprofnodestatusv1alpha1.ProfileStatePending))
+func (nsf *StatusClient) createNodeStatus(ctx context.Context) error {
+	s := nsf.statusObj(secprofnodestatusv1alpha1.ProfileStatePending)
+	if setCtrlErr := controllerutil.SetControllerReference(nsf.pol, s, nsf.client.Scheme()); setCtrlErr != nil {
+		return errors.Wrapf(setCtrlErr, "cannot set node status owner reference: %s", nsf.pol.GetName())
+	}
+	err := nsf.client.Create(ctx, s)
 	if err != nil && !kerrors.IsAlreadyExists(err) {
 		return errors.Wrap(err, "creating node status")
 	}
@@ -105,22 +111,22 @@ func (nsf *StatusClient) createNodeStatus(ctx context.Context, c client.Client)
 }
 
 func (nsf *StatusClient) Remove(ctx context.Context, c client.Client) error {
+	// if finalizer exists, remove it
+	if err := nsf.removeFinalizer(ctx); err != nil {
+		return errors.Wrapf(err, "cannot create finalizer for %s", nsf.pol.GetName())
+	}
+
 	// if object exists, remove it
 	if err := nsf.removeNodeStatus(ctx, c); err != nil {
 		return errors.Wrapf(err, "cannot create nodeStatus for %s", nsf.pol.GetName())
 	}
 
-	// if finalizer exists, remove it
-	if err := nsf.removeFinalizer(ctx, c); err != nil {
-		return errors.Wrapf(err, "cannot create finalizer for %s", nsf.pol.GetName())
-	}
-
 	return nil
 }
 
-func (nsf *StatusClient) removeFinalizer(ctx context.Context, c client.Client) error {
+func (nsf *StatusClient) removeFinalizer(ctx context.Context) error {
 	return util.Retry(func() error {
-		return util.RemoveFinalizer(ctx, c, nsf.pol, nsf.finalizerString)
+		return util.RemoveFinalizer(ctx, nsf.client, nsf.pol, nsf.finalizerString)
 	}, util.IsNotFoundOrConflict)
 }
 
@@ -134,9 +140,9 @@ func (nsf *StatusClient) removeNodeStatus(ctx context.Context, c client.Client)
 	return nil
 }
 
-func (nsf *StatusClient) Exists(ctx context.Context, c client.Client) (bool, error) {
+func (nsf *StatusClient) Exists(ctx context.Context) (bool, error) {
 	f := nsf.finalizerExists()
-	s, err := nsf.nodeStatusExists(ctx, c)
+	s, err := nsf.nodeStatusExists(ctx)
 
 	return s || f, err
 }
@@ -145,9 +151,9 @@ func (nsf *StatusClient) finalizerExists() bool {
 	return controllerutil.ContainsFinalizer(nsf.pol, nsf.finalizerString)
 }
 
-func (nsf *StatusClient) nodeStatusExists(ctx context.Context, c client.Client) (bool, error) {
+func (nsf *StatusClient) nodeStatusExists(ctx context.Context) (bool, error) {
 	status := secprofnodestatusv1alpha1.SecurityProfileNodeStatus{}
-	err := c.Get(ctx, nsf.perNodeStatusNamespacedName(), &status)
+	err := nsf.client.Get(ctx, nsf.perNodeStatusNamespacedName(), &status)
 	if kerrors.IsNotFound(err) {
 		return false, nil
 	} else if err != nil {
@@ -157,40 +163,11 @@ func (nsf *StatusClient) nodeStatusExists(ctx context.Context, c client.Client)
 	return true, nil
 }
 
-func (nsf *StatusClient) SetReturnGlobal(
-	ctx context.Context, c client.Client, polState secprofnodestatusv1alpha1.ProfileState,
-) (secprofnodestatusv1alpha1.ProfileState, error) {
-	if err := nsf.setNodeStatus(ctx, c, polState); err != nil {
-		return secprofnodestatusv1alpha1.ProfileStateError, errors.Wrap(err, "setting node status")
-	}
-
-	// search all statuses
-	statusList := secprofnodestatusv1alpha1.SecurityProfileNodeStatusList{}
-	allStatusesForProf := client.MatchingLabels{
-		secprofnodestatusv1alpha1.StatusToProfLabel: nsf.pol.GetName(),
-	}
-	if err := c.List(ctx, &statusList, allStatusesForProf); err != nil {
-		return secprofnodestatusv1alpha1.ProfileStateError, errors.Wrap(err, "listing statuses")
-	}
-
-	// figure out the 'lowest common one'
-	lowestCommonState := secprofnodestatusv1alpha1.ProfileStateTerminating
-	for i := range statusList.Items {
-		lowestCommonState = secprofnodestatusv1alpha1.LowerOfTwoStates(lowestCommonState, statusList.Items[i].Status)
-	}
-
-	// TODO(jhrozek): It would be nicer to come up with an interface the consumer would assign so that
-	// instead of returning a state we could just call that method to do the work..
-
-	// return the global status to the caller so that they can set it
-	return lowestCommonState, nil
-}
-
-func (nsf *StatusClient) setNodeStatus(
-	ctx context.Context, c client.Client, polState secprofnodestatusv1alpha1.ProfileState,
+func (nsf *StatusClient) SetNodeStatus(
+	ctx context.Context, polState secprofnodestatusv1alpha1.ProfileState,
 ) error {
 	status := secprofnodestatusv1alpha1.SecurityProfileNodeStatus{}
-	err := c.Get(ctx, nsf.perNodeStatusNamespacedName(), &status)
+	err := nsf.client.Get(ctx, nsf.perNodeStatusNamespacedName(), &status)
 	if kerrors.IsNotFound(err) && polState == secprofnodestatusv1alpha1.ProfileStateTerminating {
 		// it's OK if we're about to terminate a profile but it was already gone
 		return nil
@@ -200,9 +177,23 @@ func (nsf *StatusClient) setNodeStatus(
 
 	status.Status = polState
 	status.Labels[secprofnodestatusv1alpha1.StatusStateLabel] = string(polState)
-	if err := c.Update(ctx, &status); err != nil {
+	if err := nsf.client.Update(ctx, &status); err != nil {
 		return errors.Wrap(err, "updating node status")
 	}
 
 	return nil
 }
+
+func (nsf *StatusClient) Matches(
+	ctx context.Context, polState secprofnodestatusv1alpha1.ProfileState,
+) (bool, error) {
+	status := secprofnodestatusv1alpha1.SecurityProfileNodeStatus{}
+	if err := nsf.client.Get(ctx, nsf.perNodeStatusNamespacedName(), &status); err != nil {
+		if kerrors.IsNotFound(err) && polState == secprofnodestatusv1alpha1.ProfileStateTerminating {
+			// it's OK if we're about to terminate a profile but it was already gone
+			return true, nil
+		}
+		return false, errors.Wrapf(err, "getting node status for matching")
+	}
+	return status.Status == polState, nil
+}
diff --git a/internal/pkg/util/reconcileutils.go b/internal/pkg/util/reconcileutils.go
new file mode 100644
index 000000000..2f32d1160
--- /dev/null
+++ b/internal/pkg/util/reconcileutils.go
@@ -0,0 +1,28 @@
+/*
+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 util
+
+import (
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+)
+
+func IgnoreNotFound(err error) error {
+	if kerrors.IsNotFound(err) {
+		return nil
+	}
+	return err
+}
diff --git a/internal/pkg/webhooks/binding/binding.go b/internal/pkg/webhooks/binding/binding.go
index 311a89cdf..9547f53db 100644
--- a/internal/pkg/webhooks/binding/binding.go
+++ b/internal/pkg/webhooks/binding/binding.go
@@ -26,6 +26,7 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -35,11 +36,14 @@ import (
 
 	profilebindingv1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilebinding/v1alpha1"
 	seccompprofilev1alpha1 "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/webhooks/utils"
 )
 
 const finalizer = "active-workload-lock"
 
+var ErrProfWithoutStatus = errors.New("profile hasn't been initialized with status")
+
 type podSeccompBinder struct {
 	client  client.Client
 	log     logr.Logger
@@ -136,13 +140,13 @@ func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) ad
 			continue
 		}
 
-		seccompProfile := &seccompprofilev1alpha1.SeccompProfile{}
 		namespacedName := types.NamespacedName{Namespace: req.Namespace, Name: profileName}
-		err = p.client.Get(ctx, namespacedName, seccompProfile)
+		seccompProfile, err := p.getSeccompProfile(ctx, namespacedName)
 		if err != nil {
 			p.log.Error(err, fmt.Sprintf("failed to get SeccompProfile %#v", namespacedName))
 			return admission.Errored(http.StatusInternalServerError, err)
 		}
+
 		for j := range containers {
 			podChanged = p.addSecurityContext(containers[j], seccompProfile)
 		}
@@ -164,6 +168,29 @@ func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) ad
 	return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
 }
 
+func (p *podSeccompBinder) getSeccompProfile(
+	ctx context.Context,
+	key types.NamespacedName,
+) (*seccompprofilev1alpha1.SeccompProfile, error) {
+	seccompProfile := &seccompprofilev1alpha1.SeccompProfile{}
+	err := util.Retry(
+		func() error {
+			getErr := p.client.Get(ctx, key, seccompProfile)
+			if getErr != nil {
+				return errors.Wrapf(getErr, "getting profile")
+			}
+			if seccompProfile.Status.Status == "" {
+				return errors.Wrapf(ErrProfWithoutStatus, "getting profile")
+			}
+			return nil
+		}, func(inErr error) bool {
+			return errors.Is(inErr, ErrProfWithoutStatus) || kerrors.IsNotFound(inErr)
+		})
+	// nolint:wrapcheck
+	// error is already wrapped
+	return seccompProfile, err
+}
+
 func (p *podSeccompBinder) addSecurityContext(
 	c *corev1.Container, seccompProfile *seccompprofilev1alpha1.SeccompProfile) bool {
 	podChanged := false
diff --git a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/parser.go b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/parser.go
index 598751e96..557d4959f 100644
--- a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/parser.go
+++ b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/parser.go
@@ -137,26 +137,29 @@ func (a *ParsedArguments) parseFakeName(packageMode bool, fakeName string, args
 }
 
 func (a *ParsedArguments) parseOutputPath(packageMode bool, workingDir string, outputPath string, args []string) {
-	if outputPath != "" && packageMode {
-		a.OutputPath = outputPath
-		return
+	outputPathIsFilename := false
+	if strings.HasSuffix(outputPath, ".go") {
+		outputPathIsFilename = true
 	}
+	snakeCaseName := strings.ToLower(camelRegexp.ReplaceAllString(a.FakeImplName, "${1}_${2}"))
 
 	if outputPath != "" {
 		if !filepath.IsAbs(outputPath) {
 			outputPath = filepath.Join(workingDir, outputPath)
 		}
 		a.OutputPath = outputPath
+		if !outputPathIsFilename {
+			a.OutputPath = filepath.Join(a.OutputPath, snakeCaseName+".go")
+		}
 		return
 	}
 
 	if packageMode {
 		a.parseDestinationPackageName(packageMode, args)
-		a.OutputPath = path.Join(workingDir, a.DestinationPackageName)
+		a.OutputPath = path.Join(workingDir, a.DestinationPackageName, snakeCaseName+".go")
 		return
 	}
 
-	snakeCaseName := strings.ToLower(camelRegexp.ReplaceAllString(a.FakeImplName, "${1}_${2}"))
 	d := workingDir
 	if len(args) > 1 {
 		d = a.SourcePackageDir
diff --git a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/usage.go b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/usage.go
index 83d56e9c0..0d707afaa 100644
--- a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/usage.go
+++ b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/arguments/usage.go
@@ -63,6 +63,9 @@ OPTIONS
 		# writes "FakeMyInterface" to ./mySpecialFakesDir/specialFake.go
 		counterfeiter -o ./mySpecialFakesDir/specialFake.go ./mypackage MyInterface
 
+		# writes "FakeMyInterface" to ./mySpecialFakesDir/fake_my_interface.go
+		counterfeiter -o ./mySpecialFakesDir ./mypackage MyInterface
+
 	-p
 		Package mode:  When invoked in package mode, counterfeiter
 		will generate an interface and shim implementation from a
diff --git a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/fake.go b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/fake.go
index eb80f8985..bf88f18d7 100644
--- a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/fake.go
+++ b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/fake.go
@@ -136,7 +136,10 @@ func (f *Fake) Generate(runImports bool) ([]byte, error) {
 	}
 
 	b := &bytes.Buffer{}
-	tmpl.Execute(b, f)
+	err := tmpl.Execute(b, f)
+	if err != nil {
+		return nil, err
+	}
 	if runImports {
 		return imports.Process("counterfeiter_temp_process_file", b.Bytes(), nil)
 	}
diff --git a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/package_template.go b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/package_template.go
index 1aa68c36a..7c787872f 100644
--- a/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/package_template.go
+++ b/vendor/github.com/maxbrunsfeld/counterfeiter/v6/generator/package_template.go
@@ -9,7 +9,7 @@ var packageFuncs = template.FuncMap{
 	"ToLower":  strings.ToLower,
 	"UnExport": unexport,
 	"Replace":  strings.Replace,
-	"Generate": func() string { return "go:generate" }, // yes, this seems insane but ensures that we can use `go generate ./...` from the main package
+	"Generate": func(suffix string) string { return suffix + ":generate" }, // yes, this seems insane but ensures that we can use `go generate ./...` from the main package
 }
 
 const packageTemplate string = `{{.Header}}// Code generated by counterfeiter. DO NOT EDIT.
@@ -21,7 +21,8 @@ import (
 	{{- end}}
 )
 
-//{{.Generate}} counterfeiter . {{.Name}}
+//{{Generate "go"}} go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
+//{{Generate "counterfeiter"}} . {{.Name}}
 
 // {{.Name}} is a generated interface representing the exported functions
 // in the {{.TargetPackage}} package.
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 26b1e1cb6..652139f97 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -74,7 +74,7 @@ github.com/mattn/go-colorable
 github.com/mattn/go-isatty
 # github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369
 github.com/matttproud/golang_protobuf_extensions/pbutil
-# github.com/maxbrunsfeld/counterfeiter/v6 v6.4.0
+# github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1
 ## explicit
 github.com/maxbrunsfeld/counterfeiter/v6
 github.com/maxbrunsfeld/counterfeiter/v6/arguments


Test patch:
diff --git a/internal/pkg/daemon/seccompprofile/seccompprofile_test.go b/internal/pkg/daemon/seccompprofile/seccompprofile_test.go
index d589017ef..2d5e9e213 100644
--- a/internal/pkg/daemon/seccompprofile/seccompprofile_test.go
+++ b/internal/pkg/daemon/seccompprofile/seccompprofile_test.go
@@ -29,7 +29,6 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/require"
-	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -202,10 +201,9 @@ func TestGetProfilePath(t *testing.T) {
 	t.Parallel()
 
 	cases := []struct {
-		name    string
-		want    string
-		wantErr string
-		sp      *v1alpha1.SeccompProfile
+		name string
+		want string
+		sp   *v1alpha1.SeccompProfile
 	}{
 		{
 			name: "AppendNamespaceAndProfile",
@@ -264,46 +262,7 @@ func TestGetProfilePath(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
-			got, gotErr := GetProfilePath(tc.sp.ObjectMeta.Name, tc.sp.ObjectMeta.Namespace)
-			if tc.wantErr == "" {
-				require.NoError(t, gotErr)
-			} else {
-				require.Equal(t, tc.wantErr, gotErr.Error())
-			}
-			require.Equal(t, tc.want, got)
-		})
-	}
-}
-
-func TestIgnoreNotFound(t *testing.T) {
-	t.Parallel()
-
-	profileName := "cool-profile"
-	errOops := errors.New("oops")
-
-	cases := []struct {
-		name string
-		err  error
-		want error
-	}{
-		{
-			name: "IsErrorNotFound",
-			err:  kerrors.NewNotFound(schema.GroupResource{}, profileName),
-			want: nil,
-		},
-		{
-			name: "OtherError",
-			err:  errOops,
-			want: errOops,
-		},
-	}
-
-	for _, tc := range cases {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-
-			got := ignoreNotFound(tc.err)
+			got := tc.sp.GetProfilePath()
 			require.Equal(t, tc.want, got)
 		})
 	}
@@ -453,177 +412,3 @@ func TestUnionSyscalls(t *testing.T) {
 		})
 	}
 }
-
-func TestGetSeccompProfilesFromPod(t *testing.T) {
-	t.Parallel()
-
-	profilePath := "operator/default/test.json"
-	profilePath2 := "operator/default/test2.json"
-	cases := []struct {
-		name string
-		pod  corev1.Pod
-		want []string
-	}{
-		{
-			name: "SeccompProfileForPod",
-			pod: corev1.Pod{
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
-					SecurityContext: &corev1.PodSecurityContext{
-						SeccompProfile: &corev1.SeccompProfile{
-							Type:             "Localhost",
-							LocalhostProfile: &profilePath,
-						},
-					},
-				},
-			},
-			want: []string{profilePath},
-		},
-		{
-			name: "SeccompProfileForOneContainer",
-			pod: corev1.Pod{
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{{
-						Name:  "container1",
-						Image: "testimage",
-						SecurityContext: &corev1.SecurityContext{
-							SeccompProfile: &corev1.SeccompProfile{
-								Type:             "Localhost",
-								LocalhostProfile: &profilePath,
-							},
-						},
-					}},
-				},
-			},
-			want: []string{profilePath},
-		},
-		{
-			name: "SeccompProfileForMultipleContainers",
-			pod: corev1.Pod{
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{
-						{
-							Name:  "container1",
-							Image: "testimage",
-							SecurityContext: &corev1.SecurityContext{
-								SeccompProfile: &corev1.SeccompProfile{
-									Type:             "Localhost",
-									LocalhostProfile: &profilePath,
-								},
-							},
-						},
-						{
-							Name:  "container2",
-							Image: "testimage2",
-							SecurityContext: &corev1.SecurityContext{
-								SeccompProfile: &corev1.SeccompProfile{
-									Type:             "Localhost",
-									LocalhostProfile: &profilePath2,
-								},
-							},
-						},
-					},
-				},
-			},
-			want: []string{profilePath, profilePath2},
-		},
-		{
-			name: "SeccompProfileInAnnotation",
-			pod: corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{corev1.SeccompPodAnnotationKey: "localhost/" + profilePath},
-				},
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
-				},
-			},
-			want: []string{profilePath},
-		},
-		{
-			name: "SeccompProfileInPodAndContainerAndAnnotation",
-			pod: corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Annotations: map[string]string{corev1.SeccompPodAnnotationKey: "localhost/" + profilePath},
-				},
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{
-						{
-							Name:  "container1",
-							Image: "testimage",
-							SecurityContext: &corev1.SecurityContext{
-								SeccompProfile: &corev1.SeccompProfile{
-									Type:             "Localhost",
-									LocalhostProfile: &profilePath2,
-								},
-							},
-						},
-						{
-							Name:  "container2",
-							Image: "testimage2",
-						},
-					},
-					SecurityContext: &corev1.PodSecurityContext{
-						SeccompProfile: &corev1.SeccompProfile{
-							Type:             "Localhost",
-							LocalhostProfile: &profilePath,
-						},
-					},
-				},
-			},
-			want: []string{profilePath, profilePath2},
-		},
-	}
-
-	for _, tc := range cases {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-
-			got := getSeccompProfilesFromPod(&tc.pod)
-			require.Equal(t, tc.want, got)
-		})
-	}
-
-	badPod := corev1.Pod{
-		Spec: corev1.PodSpec{
-			Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
-			SecurityContext: &corev1.PodSecurityContext{
-				SeccompProfile: &corev1.SeccompProfile{
-					Type:             "Localhost",
-					LocalhostProfile: nil,
-				},
-			},
-		},
-	}
-	badCases := []struct {
-		name    string
-		profile string
-	}{
-		{
-			name:    "NoSuffix",
-			profile: "operator/default/test",
-		},
-		{
-			name:    "BadSuffix",
-			profile: "operator/default/test.js",
-		},
-		{
-			name:    "WrongPath",
-			profile: "foo/bar/baz",
-		},
-		{
-			name:    "NotLocalhostPath",
-			profile: "runtime/default",
-		},
-	}
-	for _, tc := range badCases {
-		tc := tc
-		badPod.Spec.SecurityContext.SeccompProfile.LocalhostProfile = &tc.profile
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-
-			got := getSeccompProfilesFromPod(&badPod)
-			require.Equal(t, []string{}, got)
-		})
-	}
-}
diff --git a/internal/pkg/manager/spod/setup_test.go b/internal/pkg/manager/spod/setup_test.go
index ba69372be..1390894a5 100644
--- a/internal/pkg/manager/spod/setup_test.go
+++ b/internal/pkg/manager/spod/setup_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package spod
 
 import (
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -49,6 +50,7 @@ func Test_getEffectiveSPOd(t *testing.T) {
 		tt := tt
 		t.Run(tt.name, func(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)
diff --git a/internal/pkg/manager/workloadannotator/workloadannotator_test.go b/internal/pkg/manager/workloadannotator/workloadannotator_test.go
new file mode 100644
index 000000000..aaadbce14
--- /dev/null
+++ b/internal/pkg/manager/workloadannotator/workloadannotator_test.go
@@ -0,0 +1,199 @@
+/*
+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 workloadannotator
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestGetSeccompProfilesFromPod(t *testing.T) {
+	t.Parallel()
+
+	profilePath := "operator/default/test.json"
+	profilePath2 := "operator/default/test2.json"
+	cases := []struct {
+		name string
+		pod  corev1.Pod
+		want []string
+	}{
+		{
+			name: "SeccompProfileForPod",
+			pod: corev1.Pod{
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
+					SecurityContext: &corev1.PodSecurityContext{
+						SeccompProfile: &corev1.SeccompProfile{
+							Type:             "Localhost",
+							LocalhostProfile: &profilePath,
+						},
+					},
+				},
+			},
+			want: []string{profilePath},
+		},
+		{
+			name: "SeccompProfileForOneContainer",
+			pod: corev1.Pod{
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{{
+						Name:  "container1",
+						Image: "testimage",
+						SecurityContext: &corev1.SecurityContext{
+							SeccompProfile: &corev1.SeccompProfile{
+								Type:             "Localhost",
+								LocalhostProfile: &profilePath,
+							},
+						},
+					}},
+				},
+			},
+			want: []string{profilePath},
+		},
+		{
+			name: "SeccompProfileForMultipleContainers",
+			pod: corev1.Pod{
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container1",
+							Image: "testimage",
+							SecurityContext: &corev1.SecurityContext{
+								SeccompProfile: &corev1.SeccompProfile{
+									Type:             "Localhost",
+									LocalhostProfile: &profilePath,
+								},
+							},
+						},
+						{
+							Name:  "container2",
+							Image: "testimage2",
+							SecurityContext: &corev1.SecurityContext{
+								SeccompProfile: &corev1.SeccompProfile{
+									Type:             "Localhost",
+									LocalhostProfile: &profilePath2,
+								},
+							},
+						},
+					},
+				},
+			},
+			want: []string{profilePath, profilePath2},
+		},
+		{
+			name: "SeccompProfileInAnnotation",
+			pod: corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{corev1.SeccompPodAnnotationKey: "localhost/" + profilePath},
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
+				},
+			},
+			want: []string{profilePath},
+		},
+		{
+			name: "SeccompProfileInPodAndContainerAndAnnotation",
+			pod: corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{corev1.SeccompPodAnnotationKey: "localhost/" + profilePath},
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "container1",
+							Image: "testimage",
+							SecurityContext: &corev1.SecurityContext{
+								SeccompProfile: &corev1.SeccompProfile{
+									Type:             "Localhost",
+									LocalhostProfile: &profilePath2,
+								},
+							},
+						},
+						{
+							Name:  "container2",
+							Image: "testimage2",
+						},
+					},
+					SecurityContext: &corev1.PodSecurityContext{
+						SeccompProfile: &corev1.SeccompProfile{
+							Type:             "Localhost",
+							LocalhostProfile: &profilePath,
+						},
+					},
+				},
+			},
+			want: []string{profilePath, profilePath2},
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			got := getSeccompProfilesFromPod(&tc.pod)
+			require.Equal(t, tc.want, got)
+		})
+	}
+
+	badPod := corev1.Pod{
+		Spec: corev1.PodSpec{
+			Containers: []corev1.Container{{Name: "container1", Image: "testimage"}},
+			SecurityContext: &corev1.PodSecurityContext{
+				SeccompProfile: &corev1.SeccompProfile{
+					Type:             "Localhost",
+					LocalhostProfile: nil,
+				},
+			},
+		},
+	}
+	badCases := []struct {
+		name    string
+		profile string
+	}{
+		{
+			name:    "NoSuffix",
+			profile: "operator/default/test",
+		},
+		{
+			name:    "BadSuffix",
+			profile: "operator/default/test.js",
+		},
+		{
+			name:    "WrongPath",
+			profile: "foo/bar/baz",
+		},
+		{
+			name:    "NotLocalhostPath",
+			profile: "runtime/default",
+		},
+	}
+	for _, tc := range badCases {
+		tc := tc
+		badPod.Spec.SecurityContext.SeccompProfile.LocalhostProfile = &tc.profile
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			got := getSeccompProfilesFromPod(&badPod)
+			require.Equal(t, []string{}, got)
+		})
+	}
+}
diff --git a/internal/pkg/util/reconcileutils_test.go b/internal/pkg/util/reconcileutils_test.go
new file mode 100644
index 000000000..9c6b7a34c
--- /dev/null
+++ b/internal/pkg/util/reconcileutils_test.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2021 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 util
+
+import (
+	"testing"
+
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+func TestIgnoreNotFound(t *testing.T) {
+	t.Parallel()
+
+	profileName := "cool-profile"
+	errOops := errors.New("oops")
+
+	cases := []struct {
+		name string
+		err  error
+		want error
+	}{
+		{
+			name: "IsErrorNotFound",
+			err:  kerrors.NewNotFound(schema.GroupResource{}, profileName),
+			want: nil,
+		},
+		{
+			name: "OtherError",
+			err:  errOops,
+			want: errOops,
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			got := IgnoreNotFound(tc.err)
+			require.Equal(t, tc.want, got)
+		})
+	}
+}
diff --git a/test/e2e_test.go b/test/e2e_test.go
index de1fc4e58..f642e8c91 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -39,8 +39,8 @@ const (
 	// migrate to a single daemonset-based implementation for the
 	// SELinux pieces.
 	defaultSelinuxOpTimeout = "360s"
-	defaultWaitTimeout      = "90s"
-	defaultWaitTime         = 5 * time.Second
+	defaultWaitTimeout      = "120s"
+	defaultWaitTime         = 10 * time.Second
 )
 
 func (e *e2e) TestSecurityProfilesOperator() {
@@ -188,6 +188,7 @@ func (e *e2e) deployOperator(manifest string) {
 	e.waitInOperatorNSFor("condition=ready", "pod", "-l", "app=security-profiles-operator")
 	// Wait for all pods in DaemonSet
 	time.Sleep(defaultWaitTime)
+	e.waitInOperatorNSFor("condition=initialized", "pod", "-l", "app=spod")
 	e.waitInOperatorNSFor("condition=ready", "pod", "-l", "app=spod")
 }
 
diff --git a/test/suite_test.go b/test/suite_test.go
index 25f3f1e3f..7fa4b9241 100644
--- a/test/suite_test.go
+++ b/test/suite_test.go
@@ -457,7 +457,7 @@ func (e *e2e) enableSelinuxInSpod() {
 	selinuxEnabledInSPODDS := e.kubectlOperatorNS("get", "ds", "spod", "-o", "yaml")
 	if !strings.Contains(selinuxEnabledInSPODDS, "--with-selinux=true") {
 		e.logf("Enable selinux in SPOD")
-		e.kubectl("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": true}}`, "--type=merge")
+		e.kubectlOperatorNS("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": true}}`, "--type=merge")
 
 		time.Sleep(defaultWaitTime)
 		e.waitInOperatorNSFor("condition=ready", "spod", "spod")
diff --git a/test/tc_base_profiles_test.go b/test/tc_base_profiles_test.go
index 377b5f205..c61a2f761 100644
--- a/test/tc_base_profiles_test.go
+++ b/test/tc_base_profiles_test.go
@@ -64,7 +64,7 @@ spec:
     seccompProfile:
       type: Localhost
       localhostProfile: operator/%s/hello.json
-  restartPolicy: Never
+  restartPolicy: OnFailure
 `
 	e.kubectl("create", "-f", baseProfilePath)
 	defer e.kubectl("delete", "-f", baseProfilePath)
diff --git a/test/tc_default_profiles_test.go b/test/tc_default_profiles_test.go
index 01189569d..c7c206cb3 100644
--- a/test/tc_default_profiles_test.go
+++ b/test/tc_default_profiles_test.go
@@ -54,6 +54,11 @@ func (e *e2e) testCaseDefaultAndExampleProfiles(nodes []string) {
 		// Default profile verification
 		namespace := e.getCurrentContextNamespace("security-profiles-operator")
 		for _, name := range defaultProfileNames {
+			e.waitFor(
+				"condition=ready",
+				"--namespace", namespace,
+				"seccompprofile", name,
+			)
 			sp := e.getSeccompProfile(name, namespace)
 			e.verifyCRDProfileContent(node, sp)
 
@@ -64,6 +69,11 @@ func (e *e2e) testCaseDefaultAndExampleProfiles(nodes []string) {
 		// Example profile verification
 		namespace = e.getCurrentContextNamespace(defaultNamespace)
 		for _, name := range exampleProfileNames {
+			e.waitFor(
+				"condition=ready",
+				"--namespace", namespace,
+				"seccompprofile", name,
+			)
 			sp := e.getSeccompProfile(name, namespace)
 			e.verifyCRDProfileContent(node, sp)
 
@@ -93,12 +103,10 @@ func (e *e2e) verifyBaseProfileContent(node string, cm *v1.ConfigMap) {
 
 func (e *e2e) verifyCRDProfileContent(node string, sp *v1alpha1.SeccompProfile) {
 	e.logf("Verifying %s profile on node %s", sp.Name, node)
-	name := sp.Name
-	profilePath, err := seccompprofile.GetProfilePath(name, sp.ObjectMeta.Namespace)
-	e.Nil(err)
+	profilePath := sp.GetProfilePath()
 	catOutput := e.execNode(node, "cat", profilePath)
 	output := seccompprofile.OutputProfile{}
-	err = json.Unmarshal([]byte(catOutput), &output)
+	err := json.Unmarshal([]byte(catOutput), &output)
 	e.Nil(err)
 	expected := seccompprofile.OutputProfile{}
 	spec, err := json.Marshal(sp.Spec)
diff --git a/test/tc_delete_profiles_test.go b/test/tc_delete_profiles_test.go
index a550cb781..8f9f4fd8e 100644
--- a/test/tc_delete_profiles_test.go
+++ b/test/tc_delete_profiles_test.go
@@ -23,7 +23,6 @@ import (
 	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 
 	secprofnodestatusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1"
-	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/seccompprofile"
 )
 
 func (e *e2e) testCaseDeleteProfiles(nodes []string) {
@@ -103,8 +102,7 @@ spec:
 
 	namespace := e.getCurrentContextNamespace(defaultNamespace)
 	sp := e.getSeccompProfile(deleteProfileName, namespace)
-	path, err := seccompprofile.GetProfilePath(deleteProfileName, sp.ObjectMeta.Namespace)
-	e.Nil(err)
+	path := sp.GetProfilePath()
 
 	e.logf("Waiting for profile to be reconciled")
 	e.waitFor("condition=ready", "seccompprofile", deleteProfileName)
diff --git a/test/tc_spod_update_selinux_test.go b/test/tc_spod_update_selinux_test.go
index 079c48138..3ef9292c1 100644
--- a/test/tc_spod_update_selinux_test.go
+++ b/test/tc_spod_update_selinux_test.go
@@ -22,7 +22,7 @@ func (e *e2e) testCaseSPODUpdateSelinux(nodes []string) {
 	e.selinuxtOnlyTestCase()
 
 	e.logf("assert selinux is enabled in the spod object")
-	selinuxEnabledInSPODObj := e.kubectl("get", "spod", "spod", "-o", "jsonpath={.spec.enableSelinux}")
+	selinuxEnabledInSPODObj := e.kubectlOperatorNS("get", "spod", "spod", "-o", "jsonpath={.spec.enableSelinux}")
 	e.Equal(selinuxEnabledInSPODObj, "true")
 
 	e.logf("assert selinux is enabled in the spod DS")
@@ -30,7 +30,7 @@ func (e *e2e) testCaseSPODUpdateSelinux(nodes []string) {
 	e.Contains(selinuxEnabledInSPODDS, "--with-selinux=true")
 
 	e.logf("Disable selinux from SPOD")
-	e.kubectl("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": false}}`, "--type=merge")
+	e.kubectlOperatorNS("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": false}}`, "--type=merge")
 
 	time.Sleep(defaultWaitTime)
 	e.waitInOperatorNSFor("condition=ready", "spod", "spod")
@@ -40,7 +40,7 @@ func (e *e2e) testCaseSPODUpdateSelinux(nodes []string) {
 	e.NotContains(selinuxDisabledInSPODDS, "--with-selinux=true")
 
 	e.logf("Re-enable selinux in SPOD")
-	e.kubectl("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": true}}`, "--type=merge")
+	e.kubectlOperatorNS("patch", "spod", "spod", "-p", `{"spec":{"enableSelinux": true}}`, "--type=merge")
 
 	time.Sleep(defaultWaitTime)
 	e.waitInOperatorNSFor("condition=ready", "spod", "spod")
@@ -48,4 +48,7 @@ func (e *e2e) testCaseSPODUpdateSelinux(nodes []string) {
 	e.logf("assert selinux is enabled in the spod DS")
 	selinuxEnabledInSPODDS = e.kubectlOperatorNS("get", "ds", "spod", "-o", "yaml")
 	e.Contains(selinuxEnabledInSPODDS, "--with-selinux=true")
+
+	e.logf("waiting for final rollout")
+	e.kubectlOperatorNS("rollout", "status", "ds", "spod", "--timeout", defaultSelinuxOpTimeout)
 }