Repository: kubernetes-sigs/security-profiles-operator
Pull request #374 (2021-03-17 12:35:14)

Refactor webhook utils and add tests
#### What type of PR is this?


/kind cleanup


#### What this PR does / why we need it:
We now refactor some utility functions into a dedicated utils package and add unit tests to them.

We also use a sync.Map for the container map in the bindings webhook.

More unit tests will follow later on.

#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->
None
#### Does this PR have test?
Yep
<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:
None
#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
None
```


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/374#" title="Author self-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: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<details>

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 53086782f4a25a6df74400da37855025a8077c64 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/374/pull-security-profiles-operator-test-e2e/1372166094978027520) | `/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=374). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

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

Patch:
diff --git a/internal/pkg/webhooks/binding/binding.go b/internal/pkg/webhooks/binding/binding.go
index a59f1b4d1..311a89cdf 100644
--- a/internal/pkg/webhooks/binding/binding.go
+++ b/internal/pkg/webhooks/binding/binding.go
@@ -21,7 +21,9 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"sync"
 
+	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
@@ -33,41 +35,46 @@ 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/webhooks/utils"
 )
 
-var log = logf.Log.WithName("pod-resource")
-
-const (
-	warnProfileAlreadySet = "cannot override existing seccomp profile for pod or container"
-	finalizer             = "active-workload-lock"
-)
+const finalizer = "active-workload-lock"
 
 type podSeccompBinder struct {
 	client  client.Client
+	log     logr.Logger
 	decoder *admission.Decoder
 }
 
 func RegisterWebhook(server *webhook.Server, c client.Client) {
-	server.Register("/mutate-v1-pod-binding", &webhook.Admission{Handler: &podSeccompBinder{client: c}})
+	server.Register(
+		"/mutate-v1-pod-binding",
+		&webhook.Admission{
+			Handler: &podSeccompBinder{
+				client: c,
+				log:    logf.Log.WithName("binding"),
+			},
+		},
+	)
 }
 
-type containerMap map[string][]*corev1.Container
+type containerList []*corev1.Container
 
-func newContainerMap(spec *corev1.PodSpec) containerMap {
-	res := make(containerMap)
+func initContainerMap(m *sync.Map, spec *corev1.PodSpec) {
 	if spec.Containers != nil {
 		for i := range spec.Containers {
 			image := spec.Containers[i].Image
-			res[image] = append(res[image], &spec.Containers[i])
+			value, _ := m.LoadOrStore(image, containerList{})
+			m.Store(image, append(value.(containerList), &spec.Containers[i]))
 		}
 	}
 	if spec.InitContainers != nil {
 		for i := range spec.InitContainers {
 			image := spec.InitContainers[i].Image
-			res[image] = append(res[image], &spec.InitContainers[i])
+			value, _ := m.LoadOrStore(image, containerList{})
+			m.Store(image, append(value.(containerList), &spec.InitContainers[i]))
 		}
 	}
-	return res
 }
 
 // Security Profiles Operator Webhook RBAC permissions
@@ -84,30 +91,33 @@ func newContainerMap(spec *corev1.PodSpec) containerMap {
 // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch
 // +kubebuilder:rbac:groups=coordination.k8s.io,namespace="security-profiles-operator",resources=leases,verbs=create;get;update;
 
-func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) admission.Response { //nolint:gocritic
+// nolint: gocritic
+func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) admission.Response {
 	profileBindings := &profilebindingv1alpha1.ProfileBindingList{}
 	err := p.client.List(ctx, profileBindings, client.InNamespace(req.Namespace))
 	if err != nil {
-		log.Error(err, "could not list profile bindings")
+		p.log.Error(err, "could not list profile bindings")
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 	profilebindings := profileBindings.Items
 	podChanged := false
 	podID := req.Namespace + "/" + req.Name
 	pod := &corev1.Pod{}
-	containers := containerMap{}
+
+	var containers sync.Map
 	if req.Operation != "DELETE" {
 		err := p.decoder.Decode(req, pod)
 		if err != nil {
-			log.Error(err, "failed to decode pod")
+			p.log.Error(err, "failed to decode pod")
 			return admission.Errored(http.StatusBadRequest, err)
 		}
-		containers = newContainerMap(&pod.Spec)
+		initContainerMap(&containers, &pod.Spec)
 	}
+
 	for i := range profilebindings {
 		// TODO(cmurphy): handle profiles kinds other than SeccompProfile
 		if profilebindings[i].Spec.ProfileRef.Kind != "SeccompProfile" {
-			log.Info(fmt.Sprintf("profile kind %s not yet supported", profilebindings[i].Spec.ProfileRef.Kind))
+			p.log.Info(fmt.Sprintf("profile kind %s not yet supported", profilebindings[i].Spec.ProfileRef.Kind))
 			continue
 		}
 		profileName := profilebindings[i].Spec.ProfileRef.Name
@@ -117,19 +127,24 @@ func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) ad
 			}
 			continue
 		}
-		c, ok := containers[profilebindings[i].Spec.Image]
+		value, ok := containers.Load(profilebindings[i].Spec.Image)
+		if !ok {
+			continue
+		}
+		containers, ok := value.(containerList)
 		if !ok {
 			continue
 		}
+
 		seccompProfile := &seccompprofilev1alpha1.SeccompProfile{}
 		namespacedName := types.NamespacedName{Namespace: req.Namespace, Name: profileName}
 		err = p.client.Get(ctx, namespacedName, seccompProfile)
 		if err != nil {
-			log.Error(err, fmt.Sprintf("failed to get SeccompProfile %#v", namespacedName))
+			p.log.Error(err, fmt.Sprintf("failed to get SeccompProfile %#v", namespacedName))
 			return admission.Errored(http.StatusInternalServerError, err)
 		}
-		for j := range c {
-			podChanged = p.addSecurityContext(c[j], seccompProfile)
+		for j := range containers {
+			podChanged = p.addSecurityContext(containers[j], seccompProfile)
 		}
 		if podChanged {
 			if err := p.addPodToBinding(ctx, podID, &profilebindings[i]); err != nil {
@@ -142,7 +157,7 @@ func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) ad
 	}
 	marshaledPod, err := json.Marshal(pod)
 	if err != nil {
-		log.Error(err, "failed to encode pod")
+		p.log.Error(err, "failed to encode pod")
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
@@ -161,7 +176,7 @@ func (p *podSeccompBinder) addSecurityContext(
 		c.SecurityContext = &corev1.SecurityContext{}
 	}
 	if c.SecurityContext.SeccompProfile != nil {
-		log.Info(warnProfileAlreadySet)
+		p.log.Info("cannot override existing seccomp profile for pod or container")
 	} else {
 		c.SecurityContext.SeccompProfile = &sp
 		podChanged = true
@@ -170,56 +185,34 @@ func (p *podSeccompBinder) addSecurityContext(
 }
 
 func (p *podSeccompBinder) addPodToBinding(
-	ctx context.Context, podID string, pb *profilebindingv1alpha1.ProfileBinding) error {
-	pb.Status.ActiveWorkloads = appendIfNotExists(pb.Status.ActiveWorkloads, podID)
-	if err := updateResource(ctx, p.client.Status(), pb, "profilebinding status"); err != nil {
-		return err
+	ctx context.Context,
+	podID string,
+	pb *profilebindingv1alpha1.ProfileBinding,
+) error {
+	pb.Status.ActiveWorkloads = utils.AppendIfNotExists(pb.Status.ActiveWorkloads, podID)
+	if err := utils.UpdateResource(ctx, p.log, p.client.Status(), pb, "profilebinding status"); err != nil {
+		return errors.Wrap(err, "add pod to binding")
 	}
 	if !controllerutil.ContainsFinalizer(pb, finalizer) {
 		controllerutil.AddFinalizer(pb, finalizer)
 	}
-	return updateResource(ctx, p.client, pb, "profilebinding")
+	return utils.UpdateResource(ctx, p.log, p.client, pb, "profilebinding")
 }
 
 func (p *podSeccompBinder) removePodFromBinding(
-	ctx context.Context, podID string, pb *profilebindingv1alpha1.ProfileBinding) error {
-	pb.Status.ActiveWorkloads = removeIfExists(pb.Status.ActiveWorkloads, podID)
-	if err := updateResource(ctx, p.client.Status(), pb, "profilebinding status"); err != nil {
-		return err
+	ctx context.Context,
+	podID string,
+	pb *profilebindingv1alpha1.ProfileBinding,
+) error {
+	pb.Status.ActiveWorkloads = utils.RemoveIfExists(pb.Status.ActiveWorkloads, podID)
+	if err := utils.UpdateResource(ctx, p.log, p.client.Status(), pb, "profilebinding status"); err != nil {
+		return errors.Wrap(err, "remove pod from binding")
 	}
 	if len(pb.Status.ActiveWorkloads) == 0 &&
 		controllerutil.ContainsFinalizer(pb, finalizer) {
 		controllerutil.RemoveFinalizer(pb, finalizer)
 	}
-	return updateResource(ctx, p.client, pb, "profilebinding")
-}
-
-func updateResource(
-	ctx context.Context, c client.StatusWriter, pb *profilebindingv1alpha1.ProfileBinding, resource string) error {
-	if err := c.Update(ctx, pb); err != nil {
-		msg := fmt.Sprintf("failed to update %s", resource)
-		log.Error(err, msg)
-		return errors.Wrap(err, msg)
-	}
-	return nil
-}
-
-func appendIfNotExists(list []string, item string) []string {
-	for _, s := range list {
-		if s == item {
-			return list
-		}
-	}
-	return append(list, item)
-}
-
-func removeIfExists(list []string, item string) []string {
-	for i := range list {
-		if list[i] == item {
-			return append(list[:i], list[i+1:]...)
-		}
-	}
-	return list
+	return utils.UpdateResource(ctx, p.log, p.client, pb, "profilebinding")
 }
 
 func (p *podSeccompBinder) InjectDecoder(d *admission.Decoder) error {
diff --git a/internal/pkg/webhooks/recording/recording.go b/internal/pkg/webhooks/recording/recording.go
index 401ccc590..b5b8c0b15 100644
--- a/internal/pkg/webhooks/recording/recording.go
+++ b/internal/pkg/webhooks/recording/recording.go
@@ -23,6 +23,7 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	admissionv1 "k8s.io/api/admission/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -36,21 +37,26 @@ import (
 
 	profilerecordingv1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilerecording/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/webhooks/utils"
 )
 
 const finalizer = "active-seccomp-profile-recording-lock"
 
-var log = logf.Log.WithName("pod-resource")
-
 type podSeccompRecorder struct {
 	client  client.Client
+	log     logr.Logger
 	decoder *admission.Decoder
 }
 
 func RegisterWebhook(server *webhook.Server, c client.Client) {
 	server.Register(
 		"/mutate-v1-pod-recording",
-		&webhook.Admission{Handler: &podSeccompRecorder{client: c}},
+		&webhook.Admission{
+			Handler: &podSeccompRecorder{
+				client: c,
+				log:    logf.Log.WithName("recording"),
+			},
+		},
 	)
 }
 
@@ -77,7 +83,7 @@ func (p *podSeccompRecorder) Handle(
 	if err := p.client.List(
 		ctx, profileRecordings, client.InNamespace(req.Namespace),
 	); err != nil {
-		log.Error(err, "Could not list profile recordings")
+		p.log.Error(err, "Could not list profile recordings")
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
@@ -88,7 +94,7 @@ func (p *podSeccompRecorder) Handle(
 	if req.Operation != admissionv1.Delete {
 		err := p.decoder.Decode(req, pod)
 		if err != nil {
-			log.Error(err, "Failed to decode pod")
+			p.log.Error(err, "Failed to decode pod")
 			return admission.Errored(http.StatusBadRequest, err)
 		}
 	}
@@ -98,7 +104,7 @@ func (p *podSeccompRecorder) Handle(
 
 	for i := range items {
 		if items[i].Spec.Kind != "SeccompProfile" {
-			log.Info(fmt.Sprintf(
+			p.log.Info(fmt.Sprintf(
 				"recording kind %s not supported", items[i].Spec.Kind,
 			))
 			continue
@@ -108,7 +114,7 @@ func (p *podSeccompRecorder) Handle(
 			&items[i].Spec.PodSelector,
 		)
 		if err != nil {
-			log.Error(
+			p.log.Error(
 				err, "Could not get label selector from profile recording",
 			)
 			return admission.Errored(http.StatusInternalServerError, err)
@@ -138,7 +144,7 @@ func (p *podSeccompRecorder) Handle(
 
 	marshaledPod, err := json.Marshal(pod)
 	if err != nil {
-		log.Error(err, "Failed to encode pod")
+		p.log.Error(err, "Failed to encode pod")
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
@@ -171,7 +177,7 @@ func (p *podSeccompRecorder) addAnnotations(
 				pod.Annotations = make(map[string]string)
 			}
 			pod.Annotations[key] = value
-			log.Info(fmt.Sprintf(
+			p.log.Info(fmt.Sprintf(
 				"adding seccomp recording annotation %s=%s to pod %s",
 				key, value, pod.Name,
 			))
@@ -180,7 +186,7 @@ func (p *podSeccompRecorder) addAnnotations(
 		}
 
 		if existingValue != value {
-			log.Error(
+			p.log.Error(
 				errors.New("existing annotation"),
 				fmt.Sprintf(
 					"workload %s already has annotation %s (not mutating to %s).",
@@ -200,12 +206,12 @@ func (p *podSeccompRecorder) addPod(
 	podID string,
 	profileRecording *profilerecordingv1alpha1.ProfileRecording,
 ) error {
-	profileRecording.Status.ActiveWorkloads = appendIfNotExists(
+	profileRecording.Status.ActiveWorkloads = utils.AppendIfNotExists(
 		profileRecording.Status.ActiveWorkloads, podID,
 	)
 
-	if err := updateResource(
-		ctx, p.client.Status(), profileRecording, "profilerecording status",
+	if err := utils.UpdateResource(
+		ctx, p.log, p.client.Status(), profileRecording, "profilerecording status",
 	); err != nil {
 		return errors.Wrap(err, "update resource on adding pod")
 	}
@@ -214,7 +220,7 @@ func (p *podSeccompRecorder) addPod(
 		controllerutil.AddFinalizer(profileRecording, finalizer)
 	}
 
-	return updateResource(ctx, p.client, profileRecording, "profilerecording")
+	return utils.UpdateResource(ctx, p.log, p.client, profileRecording, "profilerecording")
 }
 
 func (p *podSeccompRecorder) removePod(
@@ -222,12 +228,12 @@ func (p *podSeccompRecorder) removePod(
 	podID string,
 	profileRecording *profilerecordingv1alpha1.ProfileRecording,
 ) error {
-	profileRecording.Status.ActiveWorkloads = removeIfExists(
+	profileRecording.Status.ActiveWorkloads = utils.RemoveIfExists(
 		profileRecording.Status.ActiveWorkloads, podID,
 	)
 
-	if err := updateResource(
-		ctx, p.client.Status(), profileRecording, "profilerecording status",
+	if err := utils.UpdateResource(
+		ctx, p.log, p.client.Status(), profileRecording, "profilerecording status",
 	); err != nil {
 		return errors.Wrap(err, "update resource on removing pod")
 	}
@@ -237,39 +243,7 @@ func (p *podSeccompRecorder) removePod(
 		controllerutil.RemoveFinalizer(profileRecording, finalizer)
 	}
 
-	return updateResource(ctx, p.client, profileRecording, "profilerecording")
-}
-
-func appendIfNotExists(list []string, item string) []string {
-	for _, s := range list {
-		if s == item {
-			return list
-		}
-	}
-	return append(list, item)
-}
-
-func removeIfExists(list []string, item string) []string {
-	for i := range list {
-		if list[i] == item {
-			return append(list[:i], list[i+1:]...)
-		}
-	}
-	return list
-}
-
-func updateResource(
-	ctx context.Context,
-	c client.StatusWriter,
-	profileRecording *profilerecordingv1alpha1.ProfileRecording,
-	resource string,
-) error {
-	if err := c.Update(ctx, profileRecording); err != nil {
-		msg := fmt.Sprintf("failed to update %s", resource)
-		log.Error(err, msg)
-		return errors.Wrap(err, msg)
-	}
-	return nil
+	return utils.UpdateResource(ctx, p.log, p.client, profileRecording, "profilerecording")
 }
 
 func (p *podSeccompRecorder) InjectDecoder(d *admission.Decoder) error {
diff --git a/internal/pkg/webhooks/utils/utils.go b/internal/pkg/webhooks/utils/utils.go
new file mode 100644
index 000000000..0c2dd0ef7
--- /dev/null
+++ b/internal/pkg/webhooks/utils/utils.go
@@ -0,0 +1,64 @@
+/*
+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 utils
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// AppendIfNotExists adds an item to the provided list if it not exists.
+func AppendIfNotExists(list []string, item string) []string {
+	for _, s := range list {
+		if s == item {
+			return list
+		}
+	}
+	return append(list, item)
+}
+
+// RemoveIfExists removes an item from the provided list if it exists.
+func RemoveIfExists(list []string, item string) []string {
+	for i := range list {
+		if list[i] == item {
+			return append(list[:i], list[i+1:]...)
+		}
+	}
+	return list
+}
+
+// UpdateResource tries to update the provided object by using the
+// client.StatusWriter. If the update fails, it automatically logs to the
+// provided logger.
+func UpdateResource(
+	ctx context.Context,
+	logger logr.Logger,
+	c client.StatusWriter,
+	object client.Object,
+	name string,
+) error {
+	if err := c.Update(ctx, object); err != nil {
+		msg := fmt.Sprintf("failed to update resource %s", name)
+		logger.Error(err, msg)
+		return errors.Wrap(err, msg)
+	}
+	return nil
+}


Test patch:
diff --git a/internal/pkg/webhooks/binding/binding_test.go b/internal/pkg/webhooks/binding/binding_test.go
index 157e6ca81..75743f06f 100644
--- a/internal/pkg/webhooks/binding/binding_test.go
+++ b/internal/pkg/webhooks/binding/binding_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package binding
 
 import (
+	"sync"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -29,12 +30,12 @@ func TestNewContainerMap(t *testing.T) {
 	cases := []struct {
 		name    string
 		podSpec *corev1.PodSpec
-		want    containerMap
+		want    map[string]containerList
 	}{
 		{
 			name:    "NoContainers",
 			podSpec: &corev1.PodSpec{},
-			want:    map[string][]*corev1.Container{},
+			want:    map[string]containerList{},
 		},
 		{
 			name: "OnlyContainers",
@@ -50,7 +51,7 @@ func TestNewContainerMap(t *testing.T) {
 					},
 				},
 			},
-			want: map[string][]*corev1.Container{
+			want: map[string]containerList{
 				"nginx": {
 					{
 						Name:  "web",
@@ -79,7 +80,7 @@ func TestNewContainerMap(t *testing.T) {
 					},
 				},
 			},
-			want: map[string][]*corev1.Container{
+			want: map[string]containerList{
 				"busybox": {
 					{
 						Name:  "step1",
@@ -106,7 +107,7 @@ func TestNewContainerMap(t *testing.T) {
 					Image: "nginx",
 				}},
 			},
-			want: map[string][]*corev1.Container{
+			want: map[string]containerList{
 				"bash": {
 					{
 						Name:  "init",
@@ -133,7 +134,7 @@ func TestNewContainerMap(t *testing.T) {
 					Image: "bash",
 				}},
 			},
-			want: map[string][]*corev1.Container{
+			want: map[string]containerList{
 				"bash": {
 					{
 						Name:  "app",
@@ -153,10 +154,12 @@ func TestNewContainerMap(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
-			result := newContainerMap(tc.podSpec)
-			for k, v := range result {
-				require.Equal(t, tc.want[k], v)
-			}
+			var result sync.Map
+			initContainerMap(&result, tc.podSpec)
+			result.Range(func(k, v interface{}) bool {
+				require.Equal(t, tc.want[k.(string)], v.(containerList))
+				return true
+			})
 		})
 	}
 }
diff --git a/internal/pkg/webhooks/utils/utils_test.go b/internal/pkg/webhooks/utils/utils_test.go
new file mode 100644
index 000000000..b5af2828b
--- /dev/null
+++ b/internal/pkg/webhooks/utils/utils_test.go
@@ -0,0 +1,137 @@
+/*
+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 utils_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/webhooks/utils"
+)
+
+func TestAppendIfNotExists(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		provided, expected []string
+		item               string
+	}{
+		{
+			provided: []string{},
+			item:     "1",
+			expected: []string{"1"},
+		},
+		{
+			provided: []string{"1"},
+			item:     "1",
+			expected: []string{"1"},
+		},
+		{
+			provided: []string{"2"},
+			item:     "1",
+			expected: []string{"2", "1"},
+		},
+	} {
+		res := utils.AppendIfNotExists(tc.provided, tc.item)
+		require.Equal(t, tc.expected, res)
+	}
+}
+
+func TestRemoveIfExists(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		provided, expected []string
+		item               string
+	}{
+		{
+			provided: []string{},
+			item:     "1",
+			expected: []string{},
+		},
+		{
+			provided: []string{"1"},
+			item:     "1",
+			expected: []string{},
+		},
+		{
+			provided: []string{"2"},
+			item:     "1",
+			expected: []string{"2"},
+		},
+		{
+			provided: []string{"1", "2", "3"},
+			item:     "2",
+			expected: []string{"1", "3"},
+		},
+	} {
+		res := utils.RemoveIfExists(tc.provided, tc.item)
+		require.Equal(t, tc.expected, res)
+	}
+}
+
+type fakeClient struct {
+	updateFails bool
+}
+
+func (f *fakeClient) Update(
+	ctx context.Context,
+	_ client.Object,
+	_ ...client.UpdateOption,
+) error {
+	if f.updateFails {
+		return errors.New("test")
+	}
+	return nil
+}
+
+func (*fakeClient) Patch(
+	ctx context.Context,
+	_ client.Object,
+	_ client.Patch,
+	_ ...client.PatchOption,
+) error {
+	return nil
+}
+
+func TestUpdateResource(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		client    *fakeClient
+		shouldErr bool
+	}{
+		{ // success
+			client: &fakeClient{},
+		},
+		{ // update fails
+			client:    &fakeClient{updateFails: true},
+			shouldErr: true,
+		},
+	} {
+		err := utils.UpdateResource(
+			context.Background(), logr.Discard(), tc.client, nil, "",
+		)
+		if tc.shouldErr {
+			require.NotNil(t, err)
+		} else {
+			require.Nil(t, err)
+		}
+	}
+}