Repository: plexsystems/konstraint (go)

Issue:
Unexpected match.kinds generated when matching multiple apiGroups
I expect `@kinds` matchers to produce `spec.match.kinds` combined by apiGroups, instead of producing single `kinds` matcher for all apiGroups and all kinds.

For example, I expect this annotation:
```rego
# @kinds core/Pod apps/Deployment
```

To produce following `spec.match.kinds`:
```yaml
kinds:
- apiGroups:
  - ""
  kinds:
  - Pod
- apiGroups:
  - apps
  kinds:
  - Deployment
```

instead of currently generated:
```yaml
- apiGroups:
  - ""
  - apps
  kinds:
  - Pod
  - Deployment
```


Pull request description:
Group kind matchers by apiGroup
Fixes #269

I've also added sorting into kinds matcher generation so it always produces consistent result for given set of `@kinds` matchers.

If that's undesirable/unneeded, I can remove sorting in each apiGroup or even remove sorting completely, so results are produced in order of given annotations – please ping me if that's the case.

Patch:
diff --git a/examples/container-deny-added-caps/constraint.yaml b/examples/container-deny-added-caps/constraint.yaml
index f652a0e..22c2e00 100755
--- a/examples/container-deny-added-caps/constraint.yaml
+++ b/examples/container-deny-added-caps/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/container-deny-escalation/constraint.yaml b/examples/container-deny-escalation/constraint.yaml
index 40d3195..32d7d8c 100755
--- a/examples/container-deny-escalation/constraint.yaml
+++ b/examples/container-deny-escalation/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/container-deny-privileged-if-tenant/constraint.yaml b/examples/container-deny-privileged-if-tenant/constraint.yaml
index 22f946d..5ae9c97 100755
--- a/examples/container-deny-privileged-if-tenant/constraint.yaml
+++ b/examples/container-deny-privileged-if-tenant/constraint.yaml
@@ -6,13 +6,15 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
     labelSelector:
       matchLabels:
         is-tenant: "true"
diff --git a/examples/container-deny-privileged/constraint.yaml b/examples/container-deny-privileged/constraint.yaml
index f9fc21d..283d3eb 100755
--- a/examples/container-deny-privileged/constraint.yaml
+++ b/examples/container-deny-privileged/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/container-deny-without-resource-constraints/constraint.yaml b/examples/container-deny-without-resource-constraints/constraint.yaml
index 17d09c0..2317367 100644
--- a/examples/container-deny-without-resource-constraints/constraint.yaml
+++ b/examples/container-deny-without-resource-constraints/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/pod-deny-host-alias/constraint.yaml b/examples/pod-deny-host-alias/constraint.yaml
index d8d6f43..0bb0d5a 100755
--- a/examples/pod-deny-host-alias/constraint.yaml
+++ b/examples/pod-deny-host-alias/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/pod-deny-host-ipc/constraint.yaml b/examples/pod-deny-host-ipc/constraint.yaml
index 789a4dd..d8ab8f6 100755
--- a/examples/pod-deny-host-ipc/constraint.yaml
+++ b/examples/pod-deny-host-ipc/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/pod-deny-host-network/constraint.yaml b/examples/pod-deny-host-network/constraint.yaml
index d86eba0..ef603f9 100755
--- a/examples/pod-deny-host-network/constraint.yaml
+++ b/examples/pod-deny-host-network/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/pod-deny-host-pid/constraint.yaml b/examples/pod-deny-host-pid/constraint.yaml
index 5f4411f..3c0604c 100755
--- a/examples/pod-deny-host-pid/constraint.yaml
+++ b/examples/pod-deny-host-pid/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/pod-deny-without-runasnonroot/constraint.yaml b/examples/pod-deny-without-runasnonroot/constraint.yaml
index 0631e0a..6d00f8e 100755
--- a/examples/pod-deny-without-runasnonroot/constraint.yaml
+++ b/examples/pod-deny-without-runasnonroot/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/examples/policies-no-rego.md b/examples/policies-no-rego.md
index c64afb3..2b6926d 100644
--- a/examples/policies-no-rego.md
+++ b/examples/policies-no-rego.md
@@ -50,7 +50,7 @@ _source: [required-labels](required-labels)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Granting containers privileged capabilities on the node makes it easier
 for containers to escalate their privileges. As such, this is not allowed
@@ -63,7 +63,7 @@ _source: [container-deny-added-caps](container-deny-added-caps)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Privileged containers can much more easily obtain root on the node.
 As such, they are not allowed.
@@ -75,7 +75,7 @@ _source: [container-deny-escalation](container-deny-escalation)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Privileged containers can easily escalate to root privileges on the node. As
 such containers running as privileged or with sufficient capabilities granted
@@ -88,7 +88,7 @@ _source: [container-deny-privileged](container-deny-privileged)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can change aliases in the host's /etc/hosts file can
 redirect traffic to malicious servers.
@@ -100,7 +100,7 @@ _source: [pod-deny-host-alias](pod-deny-host-alias)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that are allowed to access the host IPC can read memory of
 the other containers, breaking that security boundary.
@@ -112,7 +112,7 @@ _source: [pod-deny-host-ipc](pod-deny-host-ipc)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can access the host's network interfaces can potentially
 access and tamper with traffic the pod should not have access to.
@@ -124,7 +124,7 @@ _source: [pod-deny-host-network](pod-deny-host-network)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can access the host's process tree can view and attempt to
 modify processes outside of their namespace, breaking that security
@@ -137,7 +137,7 @@ _source: [pod-deny-host-pid](pod-deny-host-pid)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods running as root (uid of 0) can much more easily escalate privileges
 to root on the node. As such, they are not allowed.
@@ -236,7 +236,7 @@ _source: [psp-deny-privileged](psp-deny-privileged)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Using the latest tag on images can cause unexpected problems in production. By specifying a pinned version
 we can have higher confidence that our applications are immutable and do not change unexpectedly.
@@ -263,7 +263,7 @@ _source: [container-deny-latest-tag](container-deny-latest-tag)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Resource constraints on containers ensure that a given workload does not take up more resources than it requires
 and potentially starve other applications that need to run.
@@ -286,7 +286,7 @@ _source: [role-deny-use-privileged-psp](role-deny-use-privileged-psp)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 **MatchLabels:** is-tenant=true
 
@@ -317,7 +317,7 @@ _source: [any-warn-deprecated-api-versions](any-warn-deprecated-api-versions)_
 
 **Severity:** Warning
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 In order to prevent persistence in the case of a compromise, it is
 important to make the root filesystem read-only.
diff --git a/examples/policies.md b/examples/policies.md
index fa25a1b..046f012 100755
--- a/examples/policies.md
+++ b/examples/policies.md
@@ -72,7 +72,7 @@ _source: [required-labels](required-labels)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Granting containers privileged capabilities on the node makes it easier
 for containers to escalate their privileges. As such, this is not allowed
@@ -107,7 +107,7 @@ _source: [container-deny-added-caps](container-deny-added-caps)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Privileged containers can much more easily obtain root on the node.
 As such, they are not allowed.
@@ -144,7 +144,7 @@ _source: [container-deny-escalation](container-deny-escalation)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Privileged containers can easily escalate to root privileges on the node. As
 such containers running as privileged or with sufficient capabilities granted
@@ -183,7 +183,7 @@ _source: [container-deny-privileged](container-deny-privileged)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can change aliases in the host's /etc/hosts file can
 redirect traffic to malicious servers.
@@ -215,7 +215,7 @@ _source: [pod-deny-host-alias](pod-deny-host-alias)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that are allowed to access the host IPC can read memory of
 the other containers, breaking that security boundary.
@@ -247,7 +247,7 @@ _source: [pod-deny-host-ipc](pod-deny-host-ipc)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can access the host's network interfaces can potentially
 access and tamper with traffic the pod should not have access to.
@@ -279,7 +279,7 @@ _source: [pod-deny-host-network](pod-deny-host-network)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods that can access the host's process tree can view and attempt to
 modify processes outside of their namespace, breaking that security
@@ -312,7 +312,7 @@ _source: [pod-deny-host-pid](pod-deny-host-pid)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Pods running as root (uid of 0) can much more easily escalate privileges
 to root on the node. As such, they are not allowed.
@@ -579,7 +579,7 @@ _source: [psp-deny-privileged](psp-deny-privileged)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Using the latest tag on images can cause unexpected problems in production. By specifying a pinned version
 we can have higher confidence that our applications are immutable and do not change unexpectedly.
@@ -631,7 +631,7 @@ _source: [container-deny-latest-tag](container-deny-latest-tag)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 Resource constraints on containers ensure that a given workload does not take up more resources than it requires
 and potentially starve other applications that need to run.
@@ -715,7 +715,7 @@ _source: [role-deny-use-privileged-psp](role-deny-use-privileged-psp)_
 
 **Severity:** Violation
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 **MatchLabels:** is-tenant=true
 
@@ -789,7 +789,7 @@ _source: [any-warn-deprecated-api-versions](any-warn-deprecated-api-versions)_
 
 **Severity:** Warning
 
-**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+**Resources:** core/Pod apps/DaemonSet apps/Deployment apps/StatefulSet
 
 In order to prevent persistence in the case of a compromise, it is
 important to make the root filesystem read-only.
diff --git a/internal/commands/create.go b/internal/commands/create.go
index 7af15d2..f396d55 100644
--- a/internal/commands/create.go
+++ b/internal/commands/create.go
@@ -6,7 +6,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"strings"
 
 	"github.com/plexsystems/konstraint/internal/rego"
 
@@ -327,42 +326,22 @@ func addParametersToConstraint(constraint *unstructured.Unstructured, parameters
 	return nil
 }
 
-func setKindMatcher(constraint *unstructured.Unstructured, kindMatchers []rego.KindMatcher) error {
-	var kinds []string
-	var apiGroups []string
-	for _, kindMatcher := range kindMatchers {
-		kinds = appendIfNotExists(kinds, kindMatcher.Kind)
-	}
+func setKindMatcher(constraint *unstructured.Unstructured, kindMatchers rego.KindMatchers) error {
+	constraintMatchers := make([]interface{}, len(kindMatchers))
 
-	for _, kindMatcher := range kindMatchers {
-		apiGroup := kindMatcher.APIGroup
-		if kindMatcher.APIGroup == "core" {
-			apiGroup = ""
+	for i, matcher := range kindMatchers {
+		constraintMatchers[i] = map[string]interface{}{
+			"apiGroups": toInterfaceSlice([]string{matcher.APIGroup}),
+			"kinds":     toInterfaceSlice(matcher.Kinds),
 		}
-
-		apiGroups = appendIfNotExists(apiGroups, apiGroup)
-	}
-
-	constraintMatcher := map[string]interface{}{
-		"apiGroups": toInterfaceSlice(apiGroups),
-		"kinds":     toInterfaceSlice(kinds),
 	}
 
-	if err := unstructured.SetNestedSlice(constraint.Object, []interface{}{constraintMatcher}, "spec", "match", "kinds"); err != nil {
+	if err := unstructured.SetNestedSlice(constraint.Object, constraintMatchers, "spec", "match", "kinds"); err != nil {
 		return fmt.Errorf("set constraint kinds matchers: %w", err)
 	}
 	return nil
 }
 
-func appendIfNotExists(currentItems []string, newItem string) []string {
-	for _, item := range currentItems {
-		if strings.EqualFold(newItem, item) {
-			return currentItems
-		}
-	}
-	return append(currentItems, newItem)
-}
-
 func toInterfaceSlice(input []string) []interface{} {
 	res := make([]interface{}, len(input))
 	for i, v := range input {
diff --git a/internal/rego/matchers.go b/internal/rego/matchers.go
index 7fbe0b1..97118e5 100644
--- a/internal/rego/matchers.go
+++ b/internal/rego/matchers.go
@@ -2,6 +2,7 @@ package rego
 
 import (
 	"fmt"
+	"sort"
 	"strings"
 )
 
@@ -14,23 +15,69 @@ type Matchers struct {
 	ExcludedNamespaceMatcher []string
 }
 
-// KindMatchers is the slice of KindMatcher
+// kindMatchersMap is internal representation of KindMatchers
+// it maps apiGroup to a slice of kinds
+type kindMatchersMap map[string][]string
+
+// KindMatcher is the matcher to generate `constraints.spec.match.kinds`
+type KindMatcher struct {
+	APIGroup string
+	Kinds    []string
+}
+
+// KindMatchers is a slice of KindMatcher
 type KindMatchers []KindMatcher
 
 func (k KindMatchers) String() string {
 	var result string
 	for _, kindMatcher := range k {
-		result += kindMatcher.APIGroup + "/" + kindMatcher.Kind + " "
+		apiGroup := kindMatcher.APIGroup
+		if apiGroup == "" {
+			apiGroup = "core"
+		}
+		for _, kind := range kindMatcher.Kinds {
+			result += apiGroup + "/" + kind + " "
+		}
 	}
 
 	result = strings.TrimSpace(result)
 	return result
 }
 
-// KindMatcher are the matchers that are applied to constraints.
-type KindMatcher struct {
-	APIGroup string
-	Kind     string
+// addIfNotPresent adds an apiGroup/kind matcher to the map
+// unless it's already present
+//
+// it also transforms apiGroup `"core"` to `""`
+func (k kindMatchersMap) addIfNotPreset(apiGroup, kind string) {
+	if apiGroup == "core" {
+		apiGroup = ""
+	}
+	for _, item := range k[apiGroup] {
+		if strings.EqualFold(kind, item) {
+			return
+		}
+	}
+	k[apiGroup] = append(k[apiGroup], kind)
+}
+
+// convert converts kindMatchersMap to KindMatchers,
+// sorted by apiGroup, with kinds in each apiGroupKinds sorted
+func (k kindMatchersMap) convert() KindMatchers {
+	apiGroups := make([]string, 0, len(k))
+	for apiGroup := range k {
+		apiGroups = append(apiGroups, apiGroup)
+	}
+	sort.Strings(apiGroups)
+
+	result := make(KindMatchers, len(apiGroups))
+	for i, apiGroup := range apiGroups {
+		result[i].APIGroup = apiGroup
+		kinds := k[apiGroup]
+		sort.Strings(kinds)
+		result[i].Kinds = kinds
+	}
+
+	return result
 }
 
 // MatchLabelsMatcher is the matcher to generate `constraints.spec.match.labelSelector.matchLabels`.
@@ -54,14 +101,18 @@ func (m MatchLabelsMatcher) String() string {
 
 // Matchers returns all of the matchers found in the rego file.
 func (r Rego) Matchers() (Matchers, error) {
-	var matchers Matchers
+	matchers := Matchers{
+		MatchLabelsMatcher: make(MatchLabelsMatcher),
+	}
+
+	kindMatchers := make(kindMatchersMap)
+
 	for _, comment := range r.headerComments {
 		if commentStartsWith(comment, "@kinds") {
-			m, err := getKindMatchers(comment)
+			err := appendKindMatchers(kindMatchers, comment)
 			if err != nil {
 				return Matchers{}, fmt.Errorf("get kind matchers: %w", err)
 			}
-			matchers.KindMatchers = append(matchers.KindMatchers, m...)
 		}
 
 		if commentStartsWith(comment, "@matchlabels") {
@@ -69,10 +120,6 @@ func (r Rego) Matchers() (Matchers, error) {
 			if err != nil {
 				return Matchers{}, fmt.Errorf("get match labels matcher: %w", err)
 			}
-			if matchers.MatchLabelsMatcher == nil {
-				matchers.MatchLabelsMatcher = m
-				m = nil
-			}
 			for k, v := range m {
 				matchers.MatchLabelsMatcher[k] = v
 			}
@@ -103,29 +150,26 @@ func (r Rego) Matchers() (Matchers, error) {
 		}
 	}
 
+	if len(kindMatchers) > 0 {
+		matchers.KindMatchers = kindMatchers.convert()
+	}
+
 	return matchers, nil
 }
 
-func getKindMatchers(comment string) ([]KindMatcher, error) {
+func appendKindMatchers(kindMatchersMap kindMatchersMap, comment string) error {
 	kindMatcherText := strings.TrimSpace(strings.SplitAfter(comment, "@kinds")[1])
 	kindMatcherGroups := strings.Split(kindMatcherText, " ")
 
-	var kindMatchers []KindMatcher
 	for _, kindMatcherGroup := range kindMatcherGroups {
 		kindMatcherSegments := strings.Split(kindMatcherGroup, "/")
 		if len(kindMatcherSegments) != 2 {
-			return nil, fmt.Errorf("invalid @kinds: %s", kindMatcherGroup)
+			return fmt.Errorf("invalid @kinds: %s", kindMatcherGroup)
 		}
-
-		kindMatcher := KindMatcher{
-			APIGroup: kindMatcherSegments[0],
-			Kind:     kindMatcherSegments[1],
-		}
-
-		kindMatchers = append(kindMatchers, kindMatcher)
+		kindMatchersMap.addIfNotPreset(kindMatcherSegments[0], kindMatcherSegments[1])
 	}
 
-	return kindMatchers, nil
+	return nil
 }
 
 func getMatchLabelsMatcher(comment string) (MatchLabelsMatcher, error) {


Test patch:
diff --git a/examples/container-deny-latest-tag/constraint.yaml b/examples/container-deny-latest-tag/constraint.yaml
index fe716be..8f3f756 100755
--- a/examples/container-deny-latest-tag/constraint.yaml
+++ b/examples/container-deny-latest-tag/constraint.yaml
@@ -6,10 +6,12 @@ spec:
   match:
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
diff --git a/internal/commands/create_test.go b/internal/commands/create_test.go
index a41ddc7..55da37f 100644
--- a/internal/commands/create_test.go
+++ b/internal/commands/create_test.go
@@ -5,36 +5,6 @@ import (
 	"testing"
 )
 
-func TestAppendIfNotExists(t *testing.T) {
-	type args struct {
-		currentItems []string
-		newItem      string
-	}
-	tests := []struct {
-		name string
-		args args
-		want []string
-	}{
-		{
-			"append",
-			args{currentItems: []string{"a"}, newItem: "b"},
-			[]string{"a", "b"},
-		},
-		{
-			"skip",
-			args{currentItems: []string{"a"}, newItem: "a"},
-			[]string{"a"},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := appendIfNotExists(tt.args.currentItems, tt.args.newItem); !reflect.DeepEqual(got, tt.want) {
-				t.Errorf("appendIfNotExists() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
 func TestToInterfaceSlice(t *testing.T) {
 	type args struct {
 		input []string
diff --git a/internal/rego/matchers_test.go b/internal/rego/matchers_test.go
index d4dea2d..5b1646a 100644
--- a/internal/rego/matchers_test.go
+++ b/internal/rego/matchers_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 )
 
-func TestGetKindMatchers(t *testing.T) {
+func TestMultilineKindMatchers(t *testing.T) {
 	comments := []string{
 		"@kinds core/Pod apps/Deployment",
 		"@kinds apps/StatefulSet",
@@ -15,9 +15,8 @@ func TestGetKindMatchers(t *testing.T) {
 	}
 
 	expected := KindMatchers{
-		{APIGroup: "core", Kind: "Pod"},
-		{APIGroup: "apps", Kind: "Deployment"},
-		{APIGroup: "apps", Kind: "StatefulSet"},
+		{APIGroup: "", Kinds: []string{"Pod"}},
+		{APIGroup: "apps", Kinds: []string{"Deployment", "StatefulSet"}},
 	}
 
 	matchers, err := rego.Matchers()
@@ -31,6 +30,63 @@ func TestGetKindMatchers(t *testing.T) {
 	}
 }
 
+func TestKindMatchers(t *testing.T) {
+	tests := []struct {
+		name    string
+		comment string
+		want    KindMatchers
+	}{
+		{
+			"core_Pod",
+			"@kinds core/Pod",
+			KindMatchers{{APIGroup: "", Kinds: []string{"Pod"}}},
+		},
+		{
+			"core_Pod,core_Pod",
+			"@kinds core/Pod core/Pod",
+			KindMatchers{{APIGroup: "", Kinds: []string{"Pod"}}},
+		},
+		{
+			"apps_Deployment,apps_StatefulSet",
+			"@kinds apps/Deployment apps/StatefulSet",
+			KindMatchers{{APIGroup: "apps", Kinds: []string{"Deployment", "StatefulSet"}}},
+		},
+		{
+			"apps_StatefulSet,apps_Deployment",
+			"@kinds apps/StatefulSet apps/Deployment",
+			KindMatchers{{APIGroup: "apps", Kinds: []string{"Deployment", "StatefulSet"}}},
+		},
+		{
+			"apps_Deployment,apps_StatefulSet,core_Pod",
+			"@kinds apps/Deployment apps/StatefulSet core/Pod",
+			KindMatchers{
+				{APIGroup: "", Kinds: []string{"Pod"}},
+				{APIGroup: "apps", Kinds: []string{"Deployment", "StatefulSet"}},
+			},
+		},
+		{
+			"apps_Deployment,core_Pod,apps_StatefulSet",
+			"@kinds apps/Deployment core/Pod apps/StatefulSet",
+			KindMatchers{
+				{APIGroup: "", Kinds: []string{"Pod"}},
+				{APIGroup: "apps", Kinds: []string{"Deployment", "StatefulSet"}},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			matchers, err := Rego{headerComments: []string{tt.comment}}.Matchers()
+			if err != nil {
+				t.Fatal(err)
+			}
+			actual := matchers.KindMatchers
+			if !reflect.DeepEqual(tt.want, actual) {
+				t.Errorf("Unexpected KindMatchers. expected %v, actual %v.", tt.want, actual)
+			}
+		})
+	}
+}
+
 func TestGetMatchLabelsMatcher(t *testing.T) {
 	comments := []string{
 		"@matchlabels team=a app.kubernetes.io/name=test",
diff --git a/test/constraint_Test.yaml b/test/constraint_Test.yaml
old mode 100755
new mode 100644
index 12007ba..8f78b1f
--- a/test/constraint_Test.yaml
+++ b/test/constraint_Test.yaml
@@ -9,13 +9,15 @@ spec:
     - gatekeeper-system
     kinds:
     - apiGroups:
-      - apps
       - ""
       kinds:
+      - Pod
+    - apiGroups:
+      - apps
+      kinds:
       - DaemonSet
       - Deployment
       - StatefulSet
-      - Pod
     labelSelector:
       matchExpressions:
       - key: foo