Repository: kubernetes-sigs/security-profiles-operator
Pull request #2160 (2024-03-11 13:17:59)

Update golangci-lint to v1.56.2 and fix lints


#### What type of PR is this?


/kind cleanup


#### What this PR does / why we need it:
Update the linter to the latest release and adapt the config / lints.
#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->
None
#### Does this PR have test?
None
<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:

#### 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/2160#" 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/main/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":[]} -->
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/2160?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) Report
> Merging [#2160](https://app.codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/2160?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (8fea1d4) into [main](https://app.codecov.io/gh/kubernetes-sigs/security-profiles-operator/commit/11d77f49d652b2b077eaeb9a0ef9eff5f24b6a8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubernetes-sigs) (11d77f4) will **decrease** coverage by `4.55%`.
> Report is 90 commits behind head on main.
> The diff coverage is `0.00%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2160      +/-   ##
==========================================
- Coverage   45.50%   40.95%   -4.55%     
==========================================
  Files          79      102      +23     
  Lines        7782    15106    +7324     
==========================================
+ Hits         3541     6187    +2646     
- Misses       4099     8482    +4383     
- Partials      142      437     +295     
```

</details>
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/2160#pullrequestreview-1927882488" title="Approved">ccojocar</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/2160#pullrequestreview-1927817370" title="Approved">JAORMX</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/2160#" 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/main/OWNERS)~~ [JAORMX,ccojocar,saschagrunert]

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

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index e3421eb38..78c6f13ee 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -6,18 +6,11 @@ issues:
   exclude-rules:
     - path: internal/pkg/daemon/bpfrecorder/generated.go
       linters:
-        - dupl
         - gofumpt
         - lll
     - linters:
         - goerr113
       text: do not define dynamic errors, use wrapped static errors instead
-    - linters:
-        - lll
-      source: "^//go:generate " # exclude lll issues for long lines with go:generate
-    - linters:
-        - paralleltest
-      path: internal/pkg/config/config_test.go
 linters:
   disable-all: true
   enable:
@@ -87,6 +80,7 @@ linters:
     - revive
     - rowserrcheck
     - sloglint
+    - spancheck
     - sqlclosecheck
     - staticcheck
     - stylecheck
@@ -125,6 +119,10 @@ linters:
     # - wrapcheck
     # - wsl
 linters-settings:
+  revive:
+    rules:
+      - name: unused-parameter
+        disabled: true
   goconst:
     min-occurrences: 6
   gomoddirectives:
@@ -158,6 +156,7 @@ linters-settings:
       - badLock
       - badRegexp
       - badSorting
+      - badSyncOnceFunc
       - boolExprSimplify
       - builtinShadow
       - builtinShadowDecl
@@ -223,7 +222,6 @@ linters-settings:
       - sliceClear
       - sloppyLen
       - sloppyReassign
-      - sloppyTestFuncName
       - sloppyTypeAssert
       - sortSlice
       - sprintfQuotedString
@@ -233,7 +231,6 @@ linters-settings:
       - stringsCompare
       - switchTrue
       - syncMapLoadAndDelete
-      - timeCmpSimplify
       - timeExprSimplify
       - todoCommentWithoutDetail
       - tooManyResultsChecker
diff --git a/Makefile b/Makefile
index f9171cddc..eedcce88f 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 
 GO ?= go
 
-GOLANGCI_LINT_VERSION = v1.55.2
+GOLANGCI_LINT_VERSION = v1.56.2
 REPO_INFRA_VERSION = v0.2.5
 KUSTOMIZE_VERSION = 5.2.1
 OPERATOR_SDK_VERSION ?= v1.25.0
diff --git a/dependencies.yaml b/dependencies.yaml
index 86f8ea48b..eb6febdaa 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -12,7 +12,7 @@ dependencies:
       match: CI_IMAGE
 
   - name: golangci-lint
-    version: 1.55.2
+    version: 1.56.2
     refPaths:
     - path: Makefile
       match: GOLANGCI_LINT_VERSION
diff --git a/internal/pkg/cli/merger/merger.go b/internal/pkg/cli/merger/merger.go
index 9c9279b0f..b4f4c4c58 100644
--- a/internal/pkg/cli/merger/merger.go
+++ b/internal/pkg/cli/merger/merger.go
@@ -25,6 +25,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/yaml"
 
+	apparmorprofileapi "sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1"
 	seccompprofileapi "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1"
 	selinuxprofileapi "sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha2"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/manager/recordingmerger"
@@ -82,6 +83,12 @@ func (p *Merger) Run() error {
 				return fmt.Errorf("unmarshal to selinux profile: %w", err)
 			}
 			contents[i] = &profile
+		case "AppArmorProfile":
+			var profile apparmorprofileapi.AppArmorProfile
+			if err := yaml.Unmarshal(content, &profile); err != nil {
+				return fmt.Errorf("unmarshal to apparmor profile: %w", err)
+			}
+			contents[i] = &profile
 		default:
 			return fmt.Errorf("unexpected YAML kind: %s", kind)
 		}
diff --git a/internal/pkg/daemon/bpfrecorder/bpfrecorder.go b/internal/pkg/daemon/bpfrecorder/bpfrecorder.go
index ea1ddba13..e2dbc3d40 100644
--- a/internal/pkg/daemon/bpfrecorder/bpfrecorder.go
+++ b/internal/pkg/daemon/bpfrecorder/bpfrecorder.go
@@ -554,7 +554,6 @@ func (b *BpfRecorder) Load(startEventProcessor bool) (err error) {
 		BPFObjName: "recorder.bpf.o",
 		BTFObjPath: b.btfPath,
 	})
-
 	if err != nil {
 		return fmt.Errorf("load bpf module: %w", err)
 	}
diff --git a/internal/pkg/manager/recordingmerger/merge_utils.go b/internal/pkg/manager/recordingmerger/merge_utils.go
index 400632fef..84117c7f2 100644
--- a/internal/pkg/manager/recordingmerger/merge_utils.go
+++ b/internal/pkg/manager/recordingmerger/merge_utils.go
@@ -19,12 +19,14 @@ package recordingmerger
 import (
 	"context"
 	"fmt"
+	"sort"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	apparmorprofileapi "sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1"
 	profilebase "sigs.k8s.io/security-profiles-operator/api/profilebase/v1alpha1"
 	profilerecording1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilerecording/v1alpha1"
 	seccompprofile "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1"
@@ -164,6 +166,8 @@ func newMergeableProfile(obj client.Object) (mergeableProfile, error) {
 		return &mergeableSeccompProfile{SeccompProfile: *obj}, nil
 	case *selinuxprofileapi.SelinuxProfile:
 		return &MergeableSelinuxProfile{SelinuxProfile: *obj}, nil
+	case *apparmorprofileapi.AppArmorProfile:
+		return &mergeableAppArmorProfile{AppArmorProfile: *obj}, nil
 	default:
 		return nil, fmt.Errorf("cannot convert %T to mergeableProfile", obj)
 	}
@@ -240,3 +244,173 @@ func addAllow(union, additional selinuxprofileapi.Allow) selinuxprofileapi.Allow
 
 	return union
 }
+
+type mergeableAppArmorProfile struct {
+	apparmorprofileapi.AppArmorProfile
+}
+
+func (sp *mergeableAppArmorProfile) getProfile() client.Object {
+	return &sp.AppArmorProfile
+}
+
+func (sp *mergeableAppArmorProfile) merge(other mergeableProfile) error {
+	otherSP, ok := other.(*mergeableAppArmorProfile)
+	if !ok {
+		return fmt.Errorf("cannot merge AppArmorProfile with %T", other)
+	}
+
+	a1 := &sp.Spec.Abstract
+	a2 := &otherSP.Spec.Abstract
+
+	if a1.Executable != nil && a2.Executable != nil {
+		a1.Executable.AllowedExecutables = mergeDedupSortStrings(
+			a1.Executable.AllowedExecutables,
+			a2.Executable.AllowedExecutables,
+		)
+		a1.Executable.AllowedLibraries = mergeDedupSortStrings(
+			a1.Executable.AllowedLibraries,
+			a2.Executable.AllowedLibraries,
+		)
+	} else if a2.Executable != nil {
+		a1.Executable = a2.Executable
+	}
+
+	mergeFilesystem(a1, a2)
+
+	if a1.Network != nil && a2.Network != nil {
+		a1.Network.AllowRaw = mergeBools(a1.Network.AllowRaw, a2.Network.AllowRaw)
+
+		if a1.Network.Protocols != nil && a2.Network.Protocols != nil {
+			a1.Network.Protocols.AllowTCP = mergeBools(a1.Network.Protocols.AllowTCP, a2.Network.Protocols.AllowTCP)
+			a1.Network.Protocols.AllowUDP = mergeBools(a1.Network.Protocols.AllowUDP, a2.Network.Protocols.AllowUDP)
+		} else if a2.Network.Protocols != nil {
+			a1.Network.Protocols = a2.Network.Protocols
+		}
+	} else if a2.Network != nil {
+		a1.Network = a2.Network
+	}
+
+	if a1.Capability != nil && a2.Capability != nil {
+		a1.Capability.AllowedCapabilities = *mergeDedupSortStrings(
+			&a1.Capability.AllowedCapabilities,
+			&a2.Capability.AllowedCapabilities,
+		)
+	} else if a2.Capability != nil {
+		a1.Capability = a2.Capability
+	}
+
+	return nil
+}
+
+func mergeFilesystem(a1, a2 *apparmorprofileapi.AppArmorAbstract) {
+	if a1.Filesystem != nil && a2.Filesystem != nil {
+		read := make(map[string]bool)
+		write := make(map[string]bool)
+		for _, fs := range []apparmorprofileapi.AppArmorFsRules{*a1.Filesystem, *a2.Filesystem} {
+			if fs.ReadOnlyPaths != nil {
+				for _, p := range *fs.ReadOnlyPaths {
+					read[p] = true
+				}
+			}
+			if fs.WriteOnlyPaths != nil {
+				for _, p := range *fs.WriteOnlyPaths {
+					write[p] = true
+				}
+			}
+			if fs.ReadWritePaths != nil {
+				for _, p := range *fs.ReadWritePaths {
+					read[p] = true
+					write[p] = true
+				}
+			}
+		}
+
+		r := make([]string, 0, len(read))
+		w := make([]string, 0, len(write))
+		rw := make([]string, 0, min(len(read), len(write)))
+
+		for path := range read {
+			if _, hasWrite := write[path]; hasWrite {
+				rw = append(rw, path)
+				delete(write, path)
+			} else {
+				r = append(r, path)
+			}
+		}
+		for path := range write {
+			w = append(w, path)
+		}
+
+		sort.Strings(r)
+		sort.Strings(w)
+		sort.Strings(rw)
+
+		a1.Filesystem = &apparmorprofileapi.AppArmorFsRules{
+			ReadOnlyPaths:  &r,
+			WriteOnlyPaths: &w,
+			ReadWritePaths: &rw,
+		}
+		// omitempty is not sufficient here.
+		if len(*a1.Filesystem.ReadOnlyPaths) == 0 {
+			a1.Filesystem.ReadOnlyPaths = nil
+		}
+		if len(*a1.Filesystem.WriteOnlyPaths) == 0 {
+			a1.Filesystem.WriteOnlyPaths = nil
+		}
+		if len(*a1.Filesystem.ReadWritePaths) == 0 {
+			a1.Filesystem.ReadWritePaths = nil
+		}
+	} else if a2.Filesystem != nil {
+		a1.Filesystem = a2.Filesystem
+	}
+}
+
+func mergeBools(a, b *bool) *bool {
+	if a == nil {
+		return b
+	}
+	if b == nil {
+		return a
+	}
+	merged := (*a || *b)
+	return &merged
+}
+
+func mergeDedupSortStrings(a, b *[]string) *[]string {
+	if a == nil {
+		return b
+	}
+	if b == nil {
+		return a
+	}
+	merged := append(*a, *b...)
+	sort.Strings(merged)
+	merged = compact(merged)
+	return &merged
+}
+
+// TODO: replace this with slices.Compact once all platform support Go 1.21.
+func compact(s []string) []string {
+	//nolint:all
+	if len(s) < 2 {
+		return s
+	}
+	i := 1
+	for k := 1; k < len(s); k++ {
+		if s[k] != s[k-1] {
+			if i != k {
+				s[i] = s[k]
+			}
+			i++
+		}
+	}
+	return s[:i]
+}
+
+// TODO: remove once all platform support Go 1.21.
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
diff --git a/internal/pkg/nodestatus/nodestatus.go b/internal/pkg/nodestatus/nodestatus.go
index 864703eaf..913c8f7b8 100644
--- a/internal/pkg/nodestatus/nodestatus.go
+++ b/internal/pkg/nodestatus/nodestatus.go
@@ -285,7 +285,7 @@ func handleRecordingFinalizer(ctx context.Context, c client.Client, pol profileb
 			continue
 		}
 
-		if n := otherPol.GetName(); len(n) > 0 {
+		if n := otherPol.GetName(); n != "" {
 			// we have a partial profile that is not being deleted and is not the current one
 			if n != pol.GetName() {
 				hasOthers = true


Test patch:
diff --git a/internal/pkg/daemon/enricher/enricher_test.go b/internal/pkg/daemon/enricher/enricher_test.go
index 6f358f2a1..9e88e89df 100644
--- a/internal/pkg/daemon/enricher/enricher_test.go
+++ b/internal/pkg/daemon/enricher/enricher_test.go
@@ -83,7 +83,6 @@ func TestRun(t *testing.T) {
 				}}}, nil)
 			},
 			assert: func(mock *enricherfakes.FakeImpl, lineChan chan *tail.Line, err error) {
-				//nolint:revive // intentional empty block
 				for mock.LinesCallCount() != 1 {
 					// Wait for Lines() to be called
 				}
@@ -93,7 +92,6 @@ func TestRun(t *testing.T) {
 					Time: time.Now(),
 				}
 
-				//nolint:revive // intentional empty block
 				for mock.SendMetricCallCount() != 1 {
 					// Wait for MetricsAuditIncCallCount to be called
 				}
@@ -207,7 +205,6 @@ func TestRun(t *testing.T) {
 				mock.SendMetricReturns(errTest)
 			},
 			assert: func(mock *enricherfakes.FakeImpl, lineChan chan *tail.Line, err error) {
-				//nolint:revive // intentional empty block
 				for mock.LinesCallCount() != 1 {
 					// Wait for Lines() to be called
 				}
@@ -217,7 +214,6 @@ func TestRun(t *testing.T) {
 					Time: time.Now(),
 				}
 
-				//nolint:revive // intentional empty block
 				for mock.SendMetricCallCount() != 1 {
 					// Wait for MetricsAuditIncCallCount to be called
 				}
@@ -267,7 +263,6 @@ func TestRun(t *testing.T) {
 				}}}, nil)
 			},
 			assert: func(mock *enricherfakes.FakeImpl, lineChan chan *tail.Line, err error) {
-				//nolint:revive // intentional empty block
 				for mock.LinesCallCount() != 1 {
 					// Wait for Lines() to be called. We should hit continue
 					// in the loop, failing the find the container ID
@@ -278,7 +273,6 @@ func TestRun(t *testing.T) {
 					Time: time.Now(),
 				}
 
-				//nolint:revive // intentional empty block
 				for mock.AddToBacklogCallCount() != 1 {
 					// Make sure the backlog was added to, because the
 					// pod information shouldn't be available yet
@@ -312,13 +306,11 @@ func TestRun(t *testing.T) {
 					},
 				)
 
-				//nolint:revive // intentional empty block
 				for mock.GetFromBacklogCallCount() != 1 {
 					// Make sure the backlog was read from when the avcs
 					// were dispatched
 				}
 
-				//nolint:revive // intentional empty block
 				for mock.FlushBacklogCallCount() != 1 {
 					// Make sure the backlog was flushed. This ensures
 					// that it was not empty and the mock entry was
diff --git a/internal/pkg/manager/recordingmerger/merge_utils_test.go b/internal/pkg/manager/recordingmerger/merge_utils_test.go
index 30d494d83..d0de81703 100644
--- a/internal/pkg/manager/recordingmerger/merge_utils_test.go
+++ b/internal/pkg/manager/recordingmerger/merge_utils_test.go
@@ -23,13 +23,15 @@ import (
 	"github.com/containers/common/pkg/seccomp"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	apparmorprofileapi "sigs.k8s.io/security-profiles-operator/api/apparmorprofile/v1alpha1"
 	seccompprofile "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1"
 	selinuxprofileapi "sigs.k8s.io/security-profiles-operator/api/selinuxprofile/v1alpha2"
 )
 
-func ifaceAsSortedSeccompProfile(iface mergeableProfile) *seccompprofile.SeccompProfile {
-	prof, ok := iface.getProfile().(*seccompprofile.SeccompProfile)
+func ifaceAsSortedSeccompProfile(iface client.Object) *seccompprofile.SeccompProfile {
+	prof, ok := iface.(*seccompprofile.SeccompProfile)
 	if !ok {
 		return nil
 	}
@@ -42,8 +44,8 @@ func ifaceAsSortedSeccompProfile(iface mergeableProfile) *seccompprofile.Seccomp
 	return prof
 }
 
-func ifaceAsSortedSelinuxProfile(iface mergeableProfile) *selinuxprofileapi.SelinuxProfile {
-	prof, ok := iface.getProfile().(*selinuxprofileapi.SelinuxProfile)
+func ifaceAsSortedSelinuxProfile(iface client.Object) *selinuxprofileapi.SelinuxProfile {
+	prof, ok := iface.(*selinuxprofileapi.SelinuxProfile)
 	if !ok {
 		return nil
 	}
@@ -61,16 +63,16 @@ func TestMergeProfiles(t *testing.T) {
 
 	for _, tc := range []struct {
 		name    string
-		prepare func(*testing.T) []mergeableProfile
-		assert  func(profile mergeableProfile) error
+		prepare func(*testing.T) []client.Object
+		assert  func(profile client.Object) error
 	}{
 		{
 			name: "Two seccomp profiles",
-			prepare: func(t *testing.T) []mergeableProfile {
+			prepare: func(t *testing.T) []client.Object {
 				t.Helper()
 
-				parts := []seccompprofile.SeccompProfile{
-					{
+				return []client.Object{
+					&seccompprofile.SeccompProfile{
 						ObjectMeta: metav1.ObjectMeta{
 							Name: "test-abc",
 						},
@@ -85,7 +87,7 @@ func TestMergeProfiles(t *testing.T) {
 							},
 						},
 					},
-					{
+					&seccompprofile.SeccompProfile{
 						ObjectMeta: metav1.ObjectMeta{
 							Name: "test-ced",
 						},
@@ -101,16 +103,8 @@ func TestMergeProfiles(t *testing.T) {
 						},
 					},
 				}
-
-				partialSpecs := make([]mergeableProfile, len(parts))
-				for i := range parts {
-					var err error
-					partialSpecs[i], err = newMergeableProfile(&parts[i])
-					require.NoError(t, err)
-				}
-				return partialSpecs
 			},
-			assert: func(mergedProfIface mergeableProfile) error {
+			assert: func(mergedProfIface client.Object) error {
 				t.Helper()
 
 				mergedProf := ifaceAsSortedSeccompProfile(mergedProfIface)
@@ -123,11 +117,11 @@ func TestMergeProfiles(t *testing.T) {
 		},
 		{
 			name: "Two selinux profiles",
-			prepare: func(t *testing.T) []mergeableProfile {
+			prepare: func(t *testing.T) []client.Object {
 				t.Helper()
 
-				parts := []selinuxprofileapi.SelinuxProfile{
-					{
+				return []client.Object{
+					&selinuxprofileapi.SelinuxProfile{
 						ObjectMeta: metav1.ObjectMeta{
 							Name: "test-foobarbaz1",
 						},
@@ -143,7 +137,7 @@ func TestMergeProfiles(t *testing.T) {
 							},
 						},
 					},
-					{
+					&selinuxprofileapi.SelinuxProfile{
 						ObjectMeta: metav1.ObjectMeta{
 							Name: "test-foobarbaz2",
 						},
@@ -161,16 +155,8 @@ func TestMergeProfiles(t *testing.T) {
 						},
 					},
 				}
-
-				partialSpecs := make([]mergeableProfile, len(parts))
-				for i := range parts {
-					var err error
-					partialSpecs[i], err = newMergeableProfile(&parts[i])
-					require.NoError(t, err)
-				}
-				return partialSpecs
 			},
-			assert: func(profile mergeableProfile) error {
+			assert: func(profile client.Object) error {
 				t.Helper()
 
 				mergedProf := ifaceAsSortedSelinuxProfile(profile)
@@ -181,13 +167,90 @@ func TestMergeProfiles(t *testing.T) {
 				return nil
 			},
 		},
+		{
+			name: "Two apparmor profiles",
+			prepare: func(t *testing.T) []client.Object {
+				t.Helper()
+
+				return []client.Object{
+					&apparmorprofileapi.AppArmorProfile{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "test-foobarbaz1",
+						},
+						Spec: apparmorprofileapi.AppArmorProfileSpec{
+							Abstract: apparmorprofileapi.AppArmorAbstract{
+								Executable: &apparmorprofileapi.AppArmorExecutablesRules{
+									AllowedExecutables: &[]string{"execA", "execB"},
+									AllowedLibraries:   &[]string{"libA"},
+								},
+								Filesystem: &apparmorprofileapi.AppArmorFsRules{
+									ReadOnlyPaths:  &[]string{"read1", "merged-rw1"},
+									WriteOnlyPaths: &[]string{"write1", "merged-rw2"},
+									ReadWritePaths: &[]string{"readwrite1"},
+								},
+								Network: &apparmorprofileapi.AppArmorNetworkRules{
+									AllowRaw: func() *bool { b := true; return &b }(),
+								},
+							},
+						},
+					},
+					&apparmorprofileapi.AppArmorProfile{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "test-foobarbaz2",
+						},
+						Spec: apparmorprofileapi.AppArmorProfileSpec{
+							Abstract: apparmorprofileapi.AppArmorAbstract{
+								Executable: &apparmorprofileapi.AppArmorExecutablesRules{
+									AllowedExecutables: &[]string{"execA", "execC"},
+								},
+								Filesystem: &apparmorprofileapi.AppArmorFsRules{
+									WriteOnlyPaths: &[]string{"merged-rw1"},
+									ReadWritePaths: &[]string{"merged-rw2"},
+								},
+								Network: &apparmorprofileapi.AppArmorNetworkRules{
+									AllowRaw: func() *bool { b := false; return &b }(),
+									Protocols: &apparmorprofileapi.AppArmorAllowedProtocols{
+										AllowTCP: func() *bool { b := true; return &b }(),
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			assert: func(profile client.Object) error {
+				t.Helper()
+
+				prof, ok := profile.(*apparmorprofileapi.AppArmorProfile)
+				require.True(t, ok)
+
+				require.Equal(t, prof.Spec.Abstract, apparmorprofileapi.AppArmorAbstract{
+					Executable: &apparmorprofileapi.AppArmorExecutablesRules{
+						AllowedExecutables: &[]string{"execA", "execB", "execC"},
+						AllowedLibraries:   &[]string{"libA"},
+					},
+					Filesystem: &apparmorprofileapi.AppArmorFsRules{
+						ReadOnlyPaths:  &[]string{"read1"},
+						WriteOnlyPaths: &[]string{"write1"},
+						ReadWritePaths: &[]string{"merged-rw1", "merged-rw2", "readwrite1"},
+					},
+					Network: &apparmorprofileapi.AppArmorNetworkRules{
+						AllowRaw: func() *bool { b := true; return &b }(),
+						Protocols: &apparmorprofileapi.AppArmorAllowedProtocols{
+							AllowTCP: func() *bool { b := true; return &b }(),
+						},
+					},
+				})
+				return nil
+			},
+		},
 	} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
 			partialProfiles := tc.prepare(t)
-			mergedProfIface, err := mergeMergeableProfiles(partialProfiles)
+			mergedProfIface, err := MergeProfiles(partialProfiles)
 			require.Nil(t, err)
 			err = tc.assert(mergedProfIface)
 			require.Nil(t, err)
diff --git a/internal/pkg/nodestatus/nodestatus_test.go b/internal/pkg/nodestatus/nodestatus_test.go
index ee76462e7..464353aa9 100644
--- a/internal/pkg/nodestatus/nodestatus_test.go
+++ b/internal/pkg/nodestatus/nodestatus_test.go
@@ -28,8 +28,6 @@ import (
 )
 
 // Expected shorten the node name if length exceed the limit.
-//
-//nolint:paralleltest // cannot set environment variables in parallel tests
 func TestShortenNodeName(t *testing.T) {
 	cases := []struct {
 		name              string