Repository: kubernetes-sigs/security-profiles-operator
Pull request #506 (2021-06-25 09:13:35)

Add first enricher unit test
#### What type of PR is this?

/kind cleanup


#### What this PR does / why we need it:
This patch adds the necessary infra for unit testing the enricher while
implementing a first success test.

Further tests will be added in other patches.
#### 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?
Yes
<!--
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/506#" 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 | 54a17612ecd53747a0c4e0f12088f6ba6b8c4b3a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-verify/1408352669281554432) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=506). [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 tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 54a17612ecd53747a0c4e0f12088f6ba6b8c4b3a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-verify/1408352669281554432) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-build | 54a17612ecd53747a0c4e0f12088f6ba6b8c4b3a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-build/1408352669260582912) | `/test pull-security-profiles-operator-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=506). [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 tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 54a17612ecd53747a0c4e0f12088f6ba6b8c4b3a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-verify/1408352669281554432) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-build | 92ac614b16de583a258606fad47011bc4ac86525 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-build/1408352884650676224) | `/test pull-security-profiles-operator-build`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=506). [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: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/506#pullrequestreview-693850385" title="LGTM">JAORMX</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/506#" 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-test-e2e | a2688d0d401f6ea9486a1238fb1c6209f3d6a8b6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-test-e2e/1409475516833992704) | `/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=506). [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 -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
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 | a2688d0d401f6ea9486a1238fb1c6209f3d6a8b6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-test-e2e/1409479432808173568) | `/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=506). [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 -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
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 | a2688d0d401f6ea9486a1238fb1c6209f3d6a8b6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-test-e2e/1409484315439403008) | `/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=506). [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: PR needs rebase.

<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: New changes are detected. LGTM label has been removed.
saschagrunert: Had to rebase on top of the latest master branch.
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 | fedc79b49df9d7cec9c9411bc6763320b72fd17f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/506/pull-security-profiles-operator-test-e2e/1409490062692847616) | `/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=506). [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 -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
saschagrunert: @JAORMX I need another LGTM on this one :upside_down_face: 
JAORMX: /lgtm

Patch:
diff --git a/cmd/security-profiles-operator/main.go b/cmd/security-profiles-operator/main.go
index 51e3db21e..879450ae8 100644
--- a/cmd/security-profiles-operator/main.go
+++ b/cmd/security-profiles-operator/main.go
@@ -324,7 +324,7 @@ func runDaemon(ctx *cli.Context) error {
 func runLogEnricher(ctx *cli.Context) error {
 	const component = "log-enricher"
 	printInfo(component)
-	return enricher.Run(ctrl.Log.WithName(component))
+	return enricher.New(ctrl.Log.WithName(component)).Run()
 }
 
 func runNonRootEnabler(ctx *cli.Context) error {
diff --git a/internal/pkg/daemon/enricher/container.go b/internal/pkg/daemon/enricher/container.go
index 681eb64bb..6c1c759bf 100644
--- a/internal/pkg/daemon/enricher/container.go
+++ b/internal/pkg/daemon/enricher/container.go
@@ -18,17 +18,12 @@ package enricher
 
 import (
 	"bufio"
-	"context"
 	"fmt"
-	"os"
 	"path/filepath"
 	"regexp"
 
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/rest"
 
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
@@ -38,13 +33,13 @@ import (
 // Cluster scoped
 // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;
 
-func getNodeContainers(logger logr.Logger, nodeName string) (map[string]containerInfo, error) {
-	config, err := rest.InClusterConfig()
+func (e *Enricher) getNodeContainers(logger logr.Logger, nodeName string) (map[string]containerInfo, error) {
+	config, err := e.impl.InClusterConfig()
 	if err != nil {
 		return nil, errors.Wrap(err, "get in-cluster config")
 	}
 
-	clientset, err := kubernetes.NewForConfig(config)
+	clientset, err := e.impl.NewForConfig(config)
 	if err != nil {
 		return nil, errors.Wrap(err, "load in-cluster config")
 	}
@@ -52,9 +47,7 @@ func getNodeContainers(logger logr.Logger, nodeName string) (map[string]containe
 	containers := make(map[string]containerInfo)
 	err = util.Retry(
 		func() (retryErr error) {
-			pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{
-				FieldSelector: "spec.nodeName=" + nodeName,
-			})
+			pods, err := e.impl.ListPods(clientset, nodeName)
 			if err != nil {
 				return errors.Wrapf(err, "list node %s's pods", nodeName)
 			}
@@ -101,9 +94,9 @@ func getNodeContainers(logger logr.Logger, nodeName string) (map[string]containe
 	return containers, err
 }
 
-func getContainerID(processID int) (string, error) {
+func (e *Enricher) getContainerID(processID int) (string, error) {
 	cgroupFile := fmt.Sprintf("/proc/%d/cgroup", processID)
-	file, err := os.Open(filepath.Clean(cgroupFile))
+	file, err := e.impl.Open(filepath.Clean(cgroupFile))
 	if err != nil {
 		return "", errors.Errorf("could not open cgroup path %s", cgroupFile)
 	}
diff --git a/internal/pkg/daemon/enricher/enricher.go b/internal/pkg/daemon/enricher/enricher.go
index c9862699a..34ac7283a 100644
--- a/internal/pkg/daemon/enricher/enricher.go
+++ b/internal/pkg/daemon/enricher/enricher.go
@@ -17,42 +17,52 @@ limitations under the License.
 package enricher
 
 import (
-	"context"
 	"os"
-	"time"
 
 	"github.com/go-logr/logr"
 	"github.com/hpcloud/tail"
 	"github.com/pkg/errors"
-	"google.golang.org/grpc"
 
 	api "sigs.k8s.io/security-profiles-operator/api/server"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
-	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/server"
 )
 
+// Enricher is the main structure of this package.
+type Enricher struct {
+	impl   impl
+	logger logr.Logger
+}
+
+// New returns a new Enricher instance.
+func New(logger logr.Logger) *Enricher {
+	return &Enricher{
+		impl:   &defaultImpl{},
+		logger: logger,
+	}
+}
+
 // Run the log-enricher to scrap audit logs and enrich them with
 // Kubernetes data (namespace, pod and container).
-func Run(logger logr.Logger) error {
-	nodeName := os.Getenv(config.NodeNameEnvKey)
+func (e *Enricher) Run() error {
+	nodeName := e.impl.Getenv(config.NodeNameEnvKey)
 	if nodeName == "" {
 		err := errors.Errorf("%s environment variable not set", config.NodeNameEnvKey)
-		logger.Error(err, "unable to run enricher")
+		e.logger.Error(err, "unable to run enricher")
 		return err
 	}
 
-	logger.Info("Starting log-enricher on node: " + nodeName)
+	e.logger.Info("Starting log-enricher on node: " + nodeName)
 
-	logger.Info("Connecting to local GRPC server")
-	conn, err := grpc.Dial(server.Addr(), grpc.WithInsecure())
+	e.logger.Info("Connecting to local GRPC server")
+	conn, err := e.impl.Dial()
 	if err != nil {
 		return errors.Wrap(err, "connecting to local GRPC server")
 	}
-	defer conn.Close()
+	defer e.impl.Close(conn)
 	client := api.NewSecurityProfilesOperatorClient(conn)
 
 	// If the file does not exist, then tail will wait for it to appear
-	tailFile, err := tail.TailFile(
+	tailFile, err := e.impl.TailFile(
 		config.AuditLogPath,
 		tail.Config{
 			ReOpen: true,
@@ -67,10 +77,10 @@ func Run(logger logr.Logger) error {
 		return errors.Wrap(err, "tailing file")
 	}
 
-	logger.Info("Reading from file " + config.AuditLogPath)
-	for l := range tailFile.Lines {
+	e.logger.Info("Reading from file " + config.AuditLogPath)
+	for l := range e.impl.Lines(tailFile) {
 		if l.Err != nil {
-			logger.Error(l.Err, "failed to tail")
+			e.logger.Error(l.Err, "failed to tail")
 			continue
 		}
 
@@ -81,21 +91,21 @@ func Run(logger logr.Logger) error {
 
 		auditLine, err := extractAuditLine(line)
 		if err != nil {
-			logger.Error(err, "extract seccomp details from audit line")
+			e.logger.Error(err, "extract seccomp details from audit line")
 			continue
 		}
 
-		cID, err := getContainerID(auditLine.ProcessID)
+		cID, err := e.getContainerID(auditLine.ProcessID)
 		if err != nil {
-			logger.Error(err, "unable to get container ID", "processID", auditLine.ProcessID)
+			e.logger.Error(err, "unable to get container ID", "processID", auditLine.ProcessID)
 			continue
 		}
 
-		containers, err := getNodeContainers(logger, nodeName)
+		containers, err := e.getNodeContainers(e.logger, nodeName)
 		c, found := containers[cID]
 
 		if !found {
-			logger.Error(
+			e.logger.Error(
 				err, "containerID not found in cluster",
 				"processID", auditLine.ProcessID,
 				"containerID", cID,
@@ -104,7 +114,7 @@ func Run(logger logr.Logger) error {
 		}
 
 		syscallName := systemCalls[auditLine.SystemCallID]
-		logger.Info("audit",
+		e.logger.Info("audit",
 			"timestamp", auditLine.TimestampID,
 			"type", auditLine.Type,
 			"node", nodeName,
@@ -121,8 +131,8 @@ func Run(logger logr.Logger) error {
 		if auditLine.Type == AuditTypeSelinux {
 			metricsType = api.MetricsAuditRequest_SELINUX
 		}
-		if _, err := client.MetricsAuditInc(
-			context.Background(),
+		if _, err := e.impl.MetricsAuditInc(
+			client,
 			&api.MetricsAuditRequest{
 				Type:       metricsType,
 				Node:       nodeName,
@@ -133,13 +143,9 @@ func Run(logger logr.Logger) error {
 				Syscall:    syscallName,
 			},
 		); err != nil {
-			logger.Error(err, "unable to update metrics")
+			e.logger.Error(err, "unable to update metrics")
 		}
 	}
 
-	logger.Error(tailFile.Err(), "enricher failed")
-
-	for {
-		time.Sleep(time.Second)
-	}
+	return errors.Wrap(tailFile.Err(), "enricher failed")
 }
diff --git a/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go b/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go
new file mode 100644
index 000000000..c4b115853
--- /dev/null
+++ b/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go
@@ -0,0 +1,822 @@
+/*
+Copyright 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.
+*/
+
+// Code generated by counterfeiter. DO NOT EDIT.
+package enricherfakes
+
+import (
+	"os"
+	"sync"
+
+	"github.com/hpcloud/tail"
+	"google.golang.org/grpc"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	api "sigs.k8s.io/security-profiles-operator/api/server"
+)
+
+type FakeImpl struct {
+	CloseStub        func(*grpc.ClientConn) error
+	closeMutex       sync.RWMutex
+	closeArgsForCall []struct {
+		arg1 *grpc.ClientConn
+	}
+	closeReturns struct {
+		result1 error
+	}
+	closeReturnsOnCall map[int]struct {
+		result1 error
+	}
+	DialStub        func() (*grpc.ClientConn, error)
+	dialMutex       sync.RWMutex
+	dialArgsForCall []struct {
+	}
+	dialReturns struct {
+		result1 *grpc.ClientConn
+		result2 error
+	}
+	dialReturnsOnCall map[int]struct {
+		result1 *grpc.ClientConn
+		result2 error
+	}
+	GetenvStub        func(string) string
+	getenvMutex       sync.RWMutex
+	getenvArgsForCall []struct {
+		arg1 string
+	}
+	getenvReturns struct {
+		result1 string
+	}
+	getenvReturnsOnCall map[int]struct {
+		result1 string
+	}
+	InClusterConfigStub        func() (*rest.Config, error)
+	inClusterConfigMutex       sync.RWMutex
+	inClusterConfigArgsForCall []struct {
+	}
+	inClusterConfigReturns struct {
+		result1 *rest.Config
+		result2 error
+	}
+	inClusterConfigReturnsOnCall map[int]struct {
+		result1 *rest.Config
+		result2 error
+	}
+	LinesStub        func(*tail.Tail) chan *tail.Line
+	linesMutex       sync.RWMutex
+	linesArgsForCall []struct {
+		arg1 *tail.Tail
+	}
+	linesReturns struct {
+		result1 chan *tail.Line
+	}
+	linesReturnsOnCall map[int]struct {
+		result1 chan *tail.Line
+	}
+	ListPodsStub        func(*kubernetes.Clientset, string) (*v1.PodList, error)
+	listPodsMutex       sync.RWMutex
+	listPodsArgsForCall []struct {
+		arg1 *kubernetes.Clientset
+		arg2 string
+	}
+	listPodsReturns struct {
+		result1 *v1.PodList
+		result2 error
+	}
+	listPodsReturnsOnCall map[int]struct {
+		result1 *v1.PodList
+		result2 error
+	}
+	MetricsAuditIncStub        func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, ...grpc.CallOption) (*api.EmptyResponse, error)
+	metricsAuditIncMutex       sync.RWMutex
+	metricsAuditIncArgsForCall []struct {
+		arg1 api.SecurityProfilesOperatorClient
+		arg2 *api.MetricsAuditRequest
+		arg3 []grpc.CallOption
+	}
+	metricsAuditIncReturns struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}
+	metricsAuditIncReturnsOnCall map[int]struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}
+	NewForConfigStub        func(*rest.Config) (*kubernetes.Clientset, error)
+	newForConfigMutex       sync.RWMutex
+	newForConfigArgsForCall []struct {
+		arg1 *rest.Config
+	}
+	newForConfigReturns struct {
+		result1 *kubernetes.Clientset
+		result2 error
+	}
+	newForConfigReturnsOnCall map[int]struct {
+		result1 *kubernetes.Clientset
+		result2 error
+	}
+	OpenStub        func(string) (*os.File, error)
+	openMutex       sync.RWMutex
+	openArgsForCall []struct {
+		arg1 string
+	}
+	openReturns struct {
+		result1 *os.File
+		result2 error
+	}
+	openReturnsOnCall map[int]struct {
+		result1 *os.File
+		result2 error
+	}
+	TailFileStub        func(string, tail.Config) (*tail.Tail, error)
+	tailFileMutex       sync.RWMutex
+	tailFileArgsForCall []struct {
+		arg1 string
+		arg2 tail.Config
+	}
+	tailFileReturns struct {
+		result1 *tail.Tail
+		result2 error
+	}
+	tailFileReturnsOnCall map[int]struct {
+		result1 *tail.Tail
+		result2 error
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeImpl) Close(arg1 *grpc.ClientConn) error {
+	fake.closeMutex.Lock()
+	ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)]
+	fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
+		arg1 *grpc.ClientConn
+	}{arg1})
+	stub := fake.CloseStub
+	fakeReturns := fake.closeReturns
+	fake.recordInvocation("Close", []interface{}{arg1})
+	fake.closeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeImpl) CloseCallCount() int {
+	fake.closeMutex.RLock()
+	defer fake.closeMutex.RUnlock()
+	return len(fake.closeArgsForCall)
+}
+
+func (fake *FakeImpl) CloseCalls(stub func(*grpc.ClientConn) error) {
+	fake.closeMutex.Lock()
+	defer fake.closeMutex.Unlock()
+	fake.CloseStub = stub
+}
+
+func (fake *FakeImpl) CloseArgsForCall(i int) *grpc.ClientConn {
+	fake.closeMutex.RLock()
+	defer fake.closeMutex.RUnlock()
+	argsForCall := fake.closeArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeImpl) CloseReturns(result1 error) {
+	fake.closeMutex.Lock()
+	defer fake.closeMutex.Unlock()
+	fake.CloseStub = nil
+	fake.closeReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeImpl) CloseReturnsOnCall(i int, result1 error) {
+	fake.closeMutex.Lock()
+	defer fake.closeMutex.Unlock()
+	fake.CloseStub = nil
+	if fake.closeReturnsOnCall == nil {
+		fake.closeReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.closeReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeImpl) Dial() (*grpc.ClientConn, error) {
+	fake.dialMutex.Lock()
+	ret, specificReturn := fake.dialReturnsOnCall[len(fake.dialArgsForCall)]
+	fake.dialArgsForCall = append(fake.dialArgsForCall, struct {
+	}{})
+	stub := fake.DialStub
+	fakeReturns := fake.dialReturns
+	fake.recordInvocation("Dial", []interface{}{})
+	fake.dialMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) DialCallCount() int {
+	fake.dialMutex.RLock()
+	defer fake.dialMutex.RUnlock()
+	return len(fake.dialArgsForCall)
+}
+
+func (fake *FakeImpl) DialCalls(stub func() (*grpc.ClientConn, error)) {
+	fake.dialMutex.Lock()
+	defer fake.dialMutex.Unlock()
+	fake.DialStub = stub
+}
+
+func (fake *FakeImpl) DialReturns(result1 *grpc.ClientConn, result2 error) {
+	fake.dialMutex.Lock()
+	defer fake.dialMutex.Unlock()
+	fake.DialStub = nil
+	fake.dialReturns = struct {
+		result1 *grpc.ClientConn
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) DialReturnsOnCall(i int, result1 *grpc.ClientConn, result2 error) {
+	fake.dialMutex.Lock()
+	defer fake.dialMutex.Unlock()
+	fake.DialStub = nil
+	if fake.dialReturnsOnCall == nil {
+		fake.dialReturnsOnCall = make(map[int]struct {
+			result1 *grpc.ClientConn
+			result2 error
+		})
+	}
+	fake.dialReturnsOnCall[i] = struct {
+		result1 *grpc.ClientConn
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) Getenv(arg1 string) string {
+	fake.getenvMutex.Lock()
+	ret, specificReturn := fake.getenvReturnsOnCall[len(fake.getenvArgsForCall)]
+	fake.getenvArgsForCall = append(fake.getenvArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.GetenvStub
+	fakeReturns := fake.getenvReturns
+	fake.recordInvocation("Getenv", []interface{}{arg1})
+	fake.getenvMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeImpl) GetenvCallCount() int {
+	fake.getenvMutex.RLock()
+	defer fake.getenvMutex.RUnlock()
+	return len(fake.getenvArgsForCall)
+}
+
+func (fake *FakeImpl) GetenvCalls(stub func(string) string) {
+	fake.getenvMutex.Lock()
+	defer fake.getenvMutex.Unlock()
+	fake.GetenvStub = stub
+}
+
+func (fake *FakeImpl) GetenvArgsForCall(i int) string {
+	fake.getenvMutex.RLock()
+	defer fake.getenvMutex.RUnlock()
+	argsForCall := fake.getenvArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeImpl) GetenvReturns(result1 string) {
+	fake.getenvMutex.Lock()
+	defer fake.getenvMutex.Unlock()
+	fake.GetenvStub = nil
+	fake.getenvReturns = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakeImpl) GetenvReturnsOnCall(i int, result1 string) {
+	fake.getenvMutex.Lock()
+	defer fake.getenvMutex.Unlock()
+	fake.GetenvStub = nil
+	if fake.getenvReturnsOnCall == nil {
+		fake.getenvReturnsOnCall = make(map[int]struct {
+			result1 string
+		})
+	}
+	fake.getenvReturnsOnCall[i] = struct {
+		result1 string
+	}{result1}
+}
+
+func (fake *FakeImpl) InClusterConfig() (*rest.Config, error) {
+	fake.inClusterConfigMutex.Lock()
+	ret, specificReturn := fake.inClusterConfigReturnsOnCall[len(fake.inClusterConfigArgsForCall)]
+	fake.inClusterConfigArgsForCall = append(fake.inClusterConfigArgsForCall, struct {
+	}{})
+	stub := fake.InClusterConfigStub
+	fakeReturns := fake.inClusterConfigReturns
+	fake.recordInvocation("InClusterConfig", []interface{}{})
+	fake.inClusterConfigMutex.Unlock()
+	if stub != nil {
+		return stub()
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) InClusterConfigCallCount() int {
+	fake.inClusterConfigMutex.RLock()
+	defer fake.inClusterConfigMutex.RUnlock()
+	return len(fake.inClusterConfigArgsForCall)
+}
+
+func (fake *FakeImpl) InClusterConfigCalls(stub func() (*rest.Config, error)) {
+	fake.inClusterConfigMutex.Lock()
+	defer fake.inClusterConfigMutex.Unlock()
+	fake.InClusterConfigStub = stub
+}
+
+func (fake *FakeImpl) InClusterConfigReturns(result1 *rest.Config, result2 error) {
+	fake.inClusterConfigMutex.Lock()
+	defer fake.inClusterConfigMutex.Unlock()
+	fake.InClusterConfigStub = nil
+	fake.inClusterConfigReturns = struct {
+		result1 *rest.Config
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) InClusterConfigReturnsOnCall(i int, result1 *rest.Config, result2 error) {
+	fake.inClusterConfigMutex.Lock()
+	defer fake.inClusterConfigMutex.Unlock()
+	fake.InClusterConfigStub = nil
+	if fake.inClusterConfigReturnsOnCall == nil {
+		fake.inClusterConfigReturnsOnCall = make(map[int]struct {
+			result1 *rest.Config
+			result2 error
+		})
+	}
+	fake.inClusterConfigReturnsOnCall[i] = struct {
+		result1 *rest.Config
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) Lines(arg1 *tail.Tail) chan *tail.Line {
+	fake.linesMutex.Lock()
+	ret, specificReturn := fake.linesReturnsOnCall[len(fake.linesArgsForCall)]
+	fake.linesArgsForCall = append(fake.linesArgsForCall, struct {
+		arg1 *tail.Tail
+	}{arg1})
+	stub := fake.LinesStub
+	fakeReturns := fake.linesReturns
+	fake.recordInvocation("Lines", []interface{}{arg1})
+	fake.linesMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeImpl) LinesCallCount() int {
+	fake.linesMutex.RLock()
+	defer fake.linesMutex.RUnlock()
+	return len(fake.linesArgsForCall)
+}
+
+func (fake *FakeImpl) LinesCalls(stub func(*tail.Tail) chan *tail.Line) {
+	fake.linesMutex.Lock()
+	defer fake.linesMutex.Unlock()
+	fake.LinesStub = stub
+}
+
+func (fake *FakeImpl) LinesArgsForCall(i int) *tail.Tail {
+	fake.linesMutex.RLock()
+	defer fake.linesMutex.RUnlock()
+	argsForCall := fake.linesArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeImpl) LinesReturns(result1 chan *tail.Line) {
+	fake.linesMutex.Lock()
+	defer fake.linesMutex.Unlock()
+	fake.LinesStub = nil
+	fake.linesReturns = struct {
+		result1 chan *tail.Line
+	}{result1}
+}
+
+func (fake *FakeImpl) LinesReturnsOnCall(i int, result1 chan *tail.Line) {
+	fake.linesMutex.Lock()
+	defer fake.linesMutex.Unlock()
+	fake.LinesStub = nil
+	if fake.linesReturnsOnCall == nil {
+		fake.linesReturnsOnCall = make(map[int]struct {
+			result1 chan *tail.Line
+		})
+	}
+	fake.linesReturnsOnCall[i] = struct {
+		result1 chan *tail.Line
+	}{result1}
+}
+
+func (fake *FakeImpl) ListPods(arg1 *kubernetes.Clientset, arg2 string) (*v1.PodList, error) {
+	fake.listPodsMutex.Lock()
+	ret, specificReturn := fake.listPodsReturnsOnCall[len(fake.listPodsArgsForCall)]
+	fake.listPodsArgsForCall = append(fake.listPodsArgsForCall, struct {
+		arg1 *kubernetes.Clientset
+		arg2 string
+	}{arg1, arg2})
+	stub := fake.ListPodsStub
+	fakeReturns := fake.listPodsReturns
+	fake.recordInvocation("ListPods", []interface{}{arg1, arg2})
+	fake.listPodsMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) ListPodsCallCount() int {
+	fake.listPodsMutex.RLock()
+	defer fake.listPodsMutex.RUnlock()
+	return len(fake.listPodsArgsForCall)
+}
+
+func (fake *FakeImpl) ListPodsCalls(stub func(*kubernetes.Clientset, string) (*v1.PodList, error)) {
+	fake.listPodsMutex.Lock()
+	defer fake.listPodsMutex.Unlock()
+	fake.ListPodsStub = stub
+}
+
+func (fake *FakeImpl) ListPodsArgsForCall(i int) (*kubernetes.Clientset, string) {
+	fake.listPodsMutex.RLock()
+	defer fake.listPodsMutex.RUnlock()
+	argsForCall := fake.listPodsArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeImpl) ListPodsReturns(result1 *v1.PodList, result2 error) {
+	fake.listPodsMutex.Lock()
+	defer fake.listPodsMutex.Unlock()
+	fake.ListPodsStub = nil
+	fake.listPodsReturns = struct {
+		result1 *v1.PodList
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) ListPodsReturnsOnCall(i int, result1 *v1.PodList, result2 error) {
+	fake.listPodsMutex.Lock()
+	defer fake.listPodsMutex.Unlock()
+	fake.ListPodsStub = nil
+	if fake.listPodsReturnsOnCall == nil {
+		fake.listPodsReturnsOnCall = make(map[int]struct {
+			result1 *v1.PodList
+			result2 error
+		})
+	}
+	fake.listPodsReturnsOnCall[i] = struct {
+		result1 *v1.PodList
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) MetricsAuditInc(arg1 api.SecurityProfilesOperatorClient, arg2 *api.MetricsAuditRequest, arg3 ...grpc.CallOption) (*api.EmptyResponse, error) {
+	fake.metricsAuditIncMutex.Lock()
+	ret, specificReturn := fake.metricsAuditIncReturnsOnCall[len(fake.metricsAuditIncArgsForCall)]
+	fake.metricsAuditIncArgsForCall = append(fake.metricsAuditIncArgsForCall, struct {
+		arg1 api.SecurityProfilesOperatorClient
+		arg2 *api.MetricsAuditRequest
+		arg3 []grpc.CallOption
+	}{arg1, arg2, arg3})
+	stub := fake.MetricsAuditIncStub
+	fakeReturns := fake.metricsAuditIncReturns
+	fake.recordInvocation("MetricsAuditInc", []interface{}{arg1, arg2, arg3})
+	fake.metricsAuditIncMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2, arg3...)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) MetricsAuditIncCallCount() int {
+	fake.metricsAuditIncMutex.RLock()
+	defer fake.metricsAuditIncMutex.RUnlock()
+	return len(fake.metricsAuditIncArgsForCall)
+}
+
+func (fake *FakeImpl) MetricsAuditIncCalls(stub func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, ...grpc.CallOption) (*api.EmptyResponse, error)) {
+	fake.metricsAuditIncMutex.Lock()
+	defer fake.metricsAuditIncMutex.Unlock()
+	fake.MetricsAuditIncStub = stub
+}
+
+func (fake *FakeImpl) MetricsAuditIncArgsForCall(i int) (api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, []grpc.CallOption) {
+	fake.metricsAuditIncMutex.RLock()
+	defer fake.metricsAuditIncMutex.RUnlock()
+	argsForCall := fake.metricsAuditIncArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
+}
+
+func (fake *FakeImpl) MetricsAuditIncReturns(result1 *api.EmptyResponse, result2 error) {
+	fake.metricsAuditIncMutex.Lock()
+	defer fake.metricsAuditIncMutex.Unlock()
+	fake.MetricsAuditIncStub = nil
+	fake.metricsAuditIncReturns = struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) MetricsAuditIncReturnsOnCall(i int, result1 *api.EmptyResponse, result2 error) {
+	fake.metricsAuditIncMutex.Lock()
+	defer fake.metricsAuditIncMutex.Unlock()
+	fake.MetricsAuditIncStub = nil
+	if fake.metricsAuditIncReturnsOnCall == nil {
+		fake.metricsAuditIncReturnsOnCall = make(map[int]struct {
+			result1 *api.EmptyResponse
+			result2 error
+		})
+	}
+	fake.metricsAuditIncReturnsOnCall[i] = struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) NewForConfig(arg1 *rest.Config) (*kubernetes.Clientset, error) {
+	fake.newForConfigMutex.Lock()
+	ret, specificReturn := fake.newForConfigReturnsOnCall[len(fake.newForConfigArgsForCall)]
+	fake.newForConfigArgsForCall = append(fake.newForConfigArgsForCall, struct {
+		arg1 *rest.Config
+	}{arg1})
+	stub := fake.NewForConfigStub
+	fakeReturns := fake.newForConfigReturns
+	fake.recordInvocation("NewForConfig", []interface{}{arg1})
+	fake.newForConfigMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) NewForConfigCallCount() int {
+	fake.newForConfigMutex.RLock()
+	defer fake.newForConfigMutex.RUnlock()
+	return len(fake.newForConfigArgsForCall)
+}
+
+func (fake *FakeImpl) NewForConfigCalls(stub func(*rest.Config) (*kubernetes.Clientset, error)) {
+	fake.newForConfigMutex.Lock()
+	defer fake.newForConfigMutex.Unlock()
+	fake.NewForConfigStub = stub
+}
+
+func (fake *FakeImpl) NewForConfigArgsForCall(i int) *rest.Config {
+	fake.newForConfigMutex.RLock()
+	defer fake.newForConfigMutex.RUnlock()
+	argsForCall := fake.newForConfigArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeImpl) NewForConfigReturns(result1 *kubernetes.Clientset, result2 error) {
+	fake.newForConfigMutex.Lock()
+	defer fake.newForConfigMutex.Unlock()
+	fake.NewForConfigStub = nil
+	fake.newForConfigReturns = struct {
+		result1 *kubernetes.Clientset
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) NewForConfigReturnsOnCall(i int, result1 *kubernetes.Clientset, result2 error) {
+	fake.newForConfigMutex.Lock()
+	defer fake.newForConfigMutex.Unlock()
+	fake.NewForConfigStub = nil
+	if fake.newForConfigReturnsOnCall == nil {
+		fake.newForConfigReturnsOnCall = make(map[int]struct {
+			result1 *kubernetes.Clientset
+			result2 error
+		})
+	}
+	fake.newForConfigReturnsOnCall[i] = struct {
+		result1 *kubernetes.Clientset
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) Open(arg1 string) (*os.File, error) {
+	fake.openMutex.Lock()
+	ret, specificReturn := fake.openReturnsOnCall[len(fake.openArgsForCall)]
+	fake.openArgsForCall = append(fake.openArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.OpenStub
+	fakeReturns := fake.openReturns
+	fake.recordInvocation("Open", []interface{}{arg1})
+	fake.openMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) OpenCallCount() int {
+	fake.openMutex.RLock()
+	defer fake.openMutex.RUnlock()
+	return len(fake.openArgsForCall)
+}
+
+func (fake *FakeImpl) OpenCalls(stub func(string) (*os.File, error)) {
+	fake.openMutex.Lock()
+	defer fake.openMutex.Unlock()
+	fake.OpenStub = stub
+}
+
+func (fake *FakeImpl) OpenArgsForCall(i int) string {
+	fake.openMutex.RLock()
+	defer fake.openMutex.RUnlock()
+	argsForCall := fake.openArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeImpl) OpenReturns(result1 *os.File, result2 error) {
+	fake.openMutex.Lock()
+	defer fake.openMutex.Unlock()
+	fake.OpenStub = nil
+	fake.openReturns = struct {
+		result1 *os.File
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) OpenReturnsOnCall(i int, result1 *os.File, result2 error) {
+	fake.openMutex.Lock()
+	defer fake.openMutex.Unlock()
+	fake.OpenStub = nil
+	if fake.openReturnsOnCall == nil {
+		fake.openReturnsOnCall = make(map[int]struct {
+			result1 *os.File
+			result2 error
+		})
+	}
+	fake.openReturnsOnCall[i] = struct {
+		result1 *os.File
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) TailFile(arg1 string, arg2 tail.Config) (*tail.Tail, error) {
+	fake.tailFileMutex.Lock()
+	ret, specificReturn := fake.tailFileReturnsOnCall[len(fake.tailFileArgsForCall)]
+	fake.tailFileArgsForCall = append(fake.tailFileArgsForCall, struct {
+		arg1 string
+		arg2 tail.Config
+	}{arg1, arg2})
+	stub := fake.TailFileStub
+	fakeReturns := fake.tailFileReturns
+	fake.recordInvocation("TailFile", []interface{}{arg1, arg2})
+	fake.tailFileMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) TailFileCallCount() int {
+	fake.tailFileMutex.RLock()
+	defer fake.tailFileMutex.RUnlock()
+	return len(fake.tailFileArgsForCall)
+}
+
+func (fake *FakeImpl) TailFileCalls(stub func(string, tail.Config) (*tail.Tail, error)) {
+	fake.tailFileMutex.Lock()
+	defer fake.tailFileMutex.Unlock()
+	fake.TailFileStub = stub
+}
+
+func (fake *FakeImpl) TailFileArgsForCall(i int) (string, tail.Config) {
+	fake.tailFileMutex.RLock()
+	defer fake.tailFileMutex.RUnlock()
+	argsForCall := fake.tailFileArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeImpl) TailFileReturns(result1 *tail.Tail, result2 error) {
+	fake.tailFileMutex.Lock()
+	defer fake.tailFileMutex.Unlock()
+	fake.TailFileStub = nil
+	fake.tailFileReturns = struct {
+		result1 *tail.Tail
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) TailFileReturnsOnCall(i int, result1 *tail.Tail, result2 error) {
+	fake.tailFileMutex.Lock()
+	defer fake.tailFileMutex.Unlock()
+	fake.TailFileStub = nil
+	if fake.tailFileReturnsOnCall == nil {
+		fake.tailFileReturnsOnCall = make(map[int]struct {
+			result1 *tail.Tail
+			result2 error
+		})
+	}
+	fake.tailFileReturnsOnCall[i] = struct {
+		result1 *tail.Tail
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.closeMutex.RLock()
+	defer fake.closeMutex.RUnlock()
+	fake.dialMutex.RLock()
+	defer fake.dialMutex.RUnlock()
+	fake.getenvMutex.RLock()
+	defer fake.getenvMutex.RUnlock()
+	fake.inClusterConfigMutex.RLock()
+	defer fake.inClusterConfigMutex.RUnlock()
+	fake.linesMutex.RLock()
+	defer fake.linesMutex.RUnlock()
+	fake.listPodsMutex.RLock()
+	defer fake.listPodsMutex.RUnlock()
+	fake.metricsAuditIncMutex.RLock()
+	defer fake.metricsAuditIncMutex.RUnlock()
+	fake.newForConfigMutex.RLock()
+	defer fake.newForConfigMutex.RUnlock()
+	fake.openMutex.RLock()
+	defer fake.openMutex.RUnlock()
+	fake.tailFileMutex.RLock()
+	defer fake.tailFileMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeImpl) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
diff --git a/internal/pkg/daemon/enricher/impl.go b/internal/pkg/daemon/enricher/impl.go
new file mode 100644
index 000000000..5b0ae2382
--- /dev/null
+++ b/internal/pkg/daemon/enricher/impl.go
@@ -0,0 +1,105 @@
+/*
+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 enricher
+
+import (
+	"context"
+	"os"
+
+	"github.com/hpcloud/tail"
+	"google.golang.org/grpc"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+
+	api "sigs.k8s.io/security-profiles-operator/api/server"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/server"
+)
+
+type defaultImpl struct{}
+
+//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
+//counterfeiter:generate . impl
+type impl interface {
+	Getenv(key string) string
+	Dial() (*grpc.ClientConn, error)
+	Close(*grpc.ClientConn) error
+	TailFile(filename string, config tail.Config) (*tail.Tail, error)
+	Lines(tailFile *tail.Tail) chan *tail.Line
+	Open(name string) (*os.File, error)
+	InClusterConfig() (*rest.Config, error)
+	NewForConfig(c *rest.Config) (*kubernetes.Clientset, error)
+	ListPods(c *kubernetes.Clientset, nodeName string) (*v1.PodList, error)
+	MetricsAuditInc(
+		client api.SecurityProfilesOperatorClient,
+		in *api.MetricsAuditRequest,
+		opts ...grpc.CallOption,
+	) (*api.EmptyResponse, error)
+}
+
+func (d *defaultImpl) Getenv(key string) string {
+	return os.Getenv(key)
+}
+
+func (d *defaultImpl) Dial() (*grpc.ClientConn, error) {
+	return grpc.Dial(server.Addr(), grpc.WithInsecure())
+}
+
+func (d *defaultImpl) Close(conn *grpc.ClientConn) error {
+	return conn.Close()
+}
+
+func (d *defaultImpl) TailFile(
+	filename string, config tail.Config,
+) (*tail.Tail, error) {
+	return tail.TailFile(filename, config)
+}
+
+func (d *defaultImpl) Lines(tailFile *tail.Tail) chan *tail.Line {
+	return tailFile.Lines
+}
+
+func (d *defaultImpl) Open(name string) (*os.File, error) {
+	return os.Open(name)
+}
+
+func (d *defaultImpl) InClusterConfig() (*rest.Config, error) {
+	return rest.InClusterConfig()
+}
+
+func (d *defaultImpl) NewForConfig(
+	c *rest.Config,
+) (*kubernetes.Clientset, error) {
+	return kubernetes.NewForConfig(c)
+}
+
+func (d *defaultImpl) ListPods(
+	c *kubernetes.Clientset, nodeName string,
+) (*v1.PodList, error) {
+	return c.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{
+		FieldSelector: "spec.nodeName=" + nodeName,
+	})
+}
+
+func (d *defaultImpl) MetricsAuditInc(
+	client api.SecurityProfilesOperatorClient,
+	in *api.MetricsAuditRequest,
+	opts ...grpc.CallOption,
+) (*api.EmptyResponse, error) {
+	return client.MetricsAuditInc(context.Background(), in, opts...)
+}


Test patch:
diff --git a/internal/pkg/daemon/enricher/enricher_test.go b/internal/pkg/daemon/enricher/enricher_test.go
new file mode 100644
index 000000000..c8e745ca2
--- /dev/null
+++ b/internal/pkg/daemon/enricher/enricher_test.go
@@ -0,0 +1,121 @@
+/*
+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 enricher
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/hpcloud/tail"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/enricher/enricherfakes"
+)
+
+const (
+	node        = "test-node"
+	namespace   = "test-namespace"
+	pod         = "test-pod"
+	executable  = "/bin/busybox"
+	syscall     = "mprotect"
+	crioPrefix  = "cri-o://"
+	seccompLine = `type=SECCOMP msg=audit(1624537480.360:8477): auid=1000 ` +
+		`uid=0 gid=0 ses=1 subj=kernel pid=2060394 comm="sleep" ` +
+		`exe="` + executable + `" sig=0 arch=c000003e syscall=10 compat=0 ` +
+		`ip=0x7f4ce626349b code=0x7ffc0000 AUID="user" UID="root" ` +
+		`GID="root" ARCH=x86_64 SYSCALL=` + syscall
+	containerID = "218ce99dd8b33f6f9b6565863d7cd47dc880963ddd2cd987bcb2d330c65144bf"
+	cgroupLine  = "0::/kubepods/burstable/" +
+		"pod4baee654-1da5-4d8c-a110-864a22f9ae39/" +
+		"crio-" + containerID + "\n"
+)
+
+func TestRun(t *testing.T) {
+	t.Parallel()
+
+	lineChan := make(chan *tail.Line)
+	f, err := os.CreateTemp("", "cgroup-")
+	require.Nil(t, err)
+	_, err = f.WriteString(cgroupLine)
+	require.Nil(t, err)
+	require.Nil(t, f.Close())
+	cgroupFilePath := f.Name()
+	defer os.Remove(cgroupFilePath)
+
+	for _, tc := range []struct {
+		prepare func(*enricherfakes.FakeImpl)
+		assert  func(*enricherfakes.FakeImpl, error)
+	}{
+		{ // success
+			prepare: func(mock *enricherfakes.FakeImpl) {
+				mock.GetenvReturns(node)
+				mock.LinesReturns(lineChan)
+				mock.OpenCalls(func(string) (*os.File, error) {
+					return os.Open(cgroupFilePath)
+				})
+				mock.ListPodsReturns(&v1.PodList{Items: []v1.Pod{{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      pod,
+						Namespace: namespace,
+					},
+					Status: v1.PodStatus{
+						ContainerStatuses: []v1.ContainerStatus{{
+							ContainerID: crioPrefix + containerID,
+						}},
+					},
+				}}}, nil)
+			},
+			assert: func(mock *enricherfakes.FakeImpl, err error) {
+				for mock.LinesCallCount() != 1 {
+					// Wait for Lines() to be called
+				}
+
+				lineChan <- &tail.Line{
+					Text: seccompLine,
+					Time: time.Now(),
+				}
+
+				for mock.MetricsAuditIncCallCount() != 1 {
+					// Wait for MetricsAuditIncCallCount to be called
+				}
+
+				_, res, _ := mock.MetricsAuditIncArgsForCall(0)
+				require.Equal(t, node, res.Node)
+				require.Equal(t, namespace, res.Namespace)
+				require.Equal(t, pod, res.Pod)
+				require.Equal(t, executable, res.Executable)
+				require.Equal(t, syscall, res.Syscall)
+
+				require.Nil(t, err)
+			},
+		},
+	} {
+		mock := &enricherfakes.FakeImpl{}
+		tc.prepare(mock)
+
+		sut := New(logr.DiscardLogger{})
+		sut.impl = mock
+
+		var err error
+		go func() { err = sut.Run() }()
+		tc.assert(mock, err)
+	}
+}