Repository: plexsystems/konstraint
Pull request #28 (2020-07-23 17:20:03)

Enhance documentation output
Notable comments have been left in the PR itself to assist with the review.


jalseth: LGTM! 🎉 

Patch:
diff --git a/.gitignore b/.gitignore
index df96a4a..ee9cd9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
 build
+konstraint*
+.DS_Store
 
 ### JetBrains template
 # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
diff --git a/acceptance.bats b/acceptance.bats
index 3f3bb3d..dd1222c 100644
--- a/acceptance.bats
+++ b/acceptance.bats
@@ -10,8 +10,17 @@
   git diff --quiet -- test/doc/expected.md
 }
 
-@test "[CREATE] Created constraints and templates matches examples" {
+@test "[CREATE] Creating constraints and templates matches examples" {
   run ./build/konstraint create examples
   git diff --quiet -- examples/container-images/constraint.yaml
   git diff --quiet -- examples/container-images/template.yaml
 }
+
+@test "[CREATE] Creating constraints using --output matches expected output" {
+  run ./build/konstraint create examples --output test/create
+  git diff --quiet -- test/create/constraint_ContainersLatestTag.yaml
+  git diff --quiet -- test/create/template_ContainersLatestTag.yaml
+
+  git diff --quiet -- test/create/constraint_ContainersResourceConstraints.yaml
+  git diff --quiet -- test/create/template_ContainersResourceConstraints.yaml
+}
diff --git a/docs/cli/konstraint.md b/docs/cli/konstraint.md
index 927909d..67f7ea2 100644
--- a/docs/cli/konstraint.md
+++ b/docs/cli/konstraint.md
@@ -9,9 +9,7 @@ A tool to create and manage Gatekeeper CRDs from Rego
 ### Options
 
 ```
-  -h, --help            help for konstraint
-      --ignore string   A regex pattern which can be used for ignoring directories and files (default "^$")
-      --lib string      The name of the folder where the Rego librarie(s) are (default "lib")
+  -h, --help   help for konstraint
 ```
 
 ### SEE ALSO
@@ -19,4 +17,4 @@ A tool to create and manage Gatekeeper CRDs from Rego
 * [konstraint create](konstraint_create.md)	 - Create Gatekeeper constraints from Rego policies
 * [konstraint doc](konstraint_doc.md)	 - Generate documentation from Rego policies
 
-###### Auto generated by spf13/cobra on 22-Jul-2020
+###### Auto generated by spf13/cobra on 24-Jul-2020
diff --git a/docs/cli/konstraint_create.md b/docs/cli/konstraint_create.md
index 2123ff4..6cbfc7a 100644
--- a/docs/cli/konstraint_create.md
+++ b/docs/cli/konstraint_create.md
@@ -31,15 +31,8 @@ Create constraints with the Gatekeeper enforcement action set to dryrun
   -o, --output string   Specify an output directory for the Gatekeeper resources
 ```
 
-### Options inherited from parent commands
-
-```
-      --ignore string   A regex pattern which can be used for ignoring directories and files (default "^$")
-      --lib string      The name of the folder where the Rego librarie(s) are (default "lib")
-```
-
 ### SEE ALSO
 
 * [konstraint](konstraint.md)	 - Konstraint
 
-###### Auto generated by spf13/cobra on 22-Jul-2020
+###### Auto generated by spf13/cobra on 24-Jul-2020
diff --git a/docs/cli/konstraint_doc.md b/docs/cli/konstraint_doc.md
index d41bf00..da86980 100644
--- a/docs/cli/konstraint_doc.md
+++ b/docs/cli/konstraint_doc.md
@@ -27,15 +27,8 @@ Save the documentation to a specific directory
   -o, --output string   Output location (including filename) for the policy documentation (default "policies.md")
 ```
 
-### Options inherited from parent commands
-
-```
-      --ignore string   A regex pattern which can be used for ignoring directories and files (default "^$")
-      --lib string      The name of the folder where the Rego librarie(s) are (default "lib")
-```
-
 ### SEE ALSO
 
 * [konstraint](konstraint.md)	 - Konstraint
 
-###### Auto generated by spf13/cobra on 22-Jul-2020
+###### Auto generated by spf13/cobra on 24-Jul-2020
diff --git a/examples/container-images/src.rego b/examples/container-images/src.rego
deleted file mode 100644
index 2ecfc80..0000000
--- a/examples/container-images/src.rego
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import data.lib.k8s
-
-# Container images can not use the latest tag.
-# @Kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
-violation[msg] {
-  has_latest_tag
-
-  msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
-}
-
-has_latest_tag {
-  endswith(k8s.container_images[_], ":latest")
-}
-
-has_latest_tag {
-  contains(k8s.container_images[_], ":") == false
-}
diff --git a/examples/container-images/constraint.yaml b/examples/containers-latest-tag/constraint.yaml
similarity index 79%
rename from examples/container-images/constraint.yaml
rename to examples/containers-latest-tag/constraint.yaml
index 3e281bd..c8b04c7 100755
--- a/examples/container-images/constraint.yaml
+++ b/examples/containers-latest-tag/constraint.yaml
@@ -1,7 +1,7 @@
 apiVersion: constraints.gatekeeper.sh/v1beta1
-kind: ContainerImages
+kind: ContainersLatestTag
 metadata:
-  name: containerimages
+  name: containerslatesttag
 spec:
   match:
     kinds:
diff --git a/examples/container-images/src_test.rego b/examples/containers-latest-tag/src_test.rego
similarity index 100%
rename from examples/container-images/src_test.rego
rename to examples/containers-latest-tag/src_test.rego
diff --git a/examples/container-images/template.yaml b/examples/containers-latest-tag/template.yaml
similarity index 91%
rename from examples/container-images/template.yaml
rename to examples/containers-latest-tag/template.yaml
index 56591ea..d34e682 100644
--- a/examples/container-images/template.yaml
+++ b/examples/containers-latest-tag/template.yaml
@@ -2,12 +2,12 @@ apiVersion: templates.gatekeeper.sh/v1beta1
 kind: ConstraintTemplate
 metadata:
   creationTimestamp: null
-  name: containerimages
+  name: containerslatesttag
 spec:
   crd:
     spec:
       names:
-        kind: ContainerImages
+        kind: ContainersLatestTag
   targets:
   - libs:
     - |
@@ -139,48 +139,32 @@ spec:
         volume = pod.spec.volumes[_]
       }
 
-      # 10 ** 21
       mem_multiple("E") = 1000000000000000000000 { true }
 
-      # 10 ** 18
       mem_multiple("P") = 1000000000000000000 { true }
 
-      # 10 ** 15
       mem_multiple("T") = 1000000000000000 { true }
 
-      # 10 ** 12
       mem_multiple("G") = 1000000000000 { true }
 
-      # 10 ** 9
       mem_multiple("M") = 1000000000 { true }
 
-      # 10 ** 6
       mem_multiple("k") = 1000000 { true }
 
-      # 10 ** 3
       mem_multiple("") = 1000 { true }
 
-      # Kubernetes accepts millibyte precision when it probably shouldn't.
-      # https://github.com/kubernetes/kubernetes/issues/28741
-      # 10 ** 0
       mem_multiple("m") = 1 { true }
 
-      # 1000 * 2 ** 10
       mem_multiple("Ki") = 1024000 { true }
 
-      # 1000 * 2 ** 20
       mem_multiple("Mi") = 1048576000 { true }
 
-      # 1000 * 2 ** 30
       mem_multiple("Gi") = 1073741824000 { true }
 
-      # 1000 * 2 ** 40
       mem_multiple("Ti") = 1099511627776000 { true }
 
-      # 1000 * 2 ** 50
       mem_multiple("Pi") = 1125899906842624000 { true }
 
-      # 1000 * 2 ** 60
       mem_multiple("Ei") = 1152921504606846976000 { true }
 
       get_suffix(mem) = suffix {
@@ -297,8 +281,6 @@ spec:
 
       import data.lib.k8s
 
-      # Container images can not use the latest tag.
-      # @Kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
       violation[msg] {
         has_latest_tag
 
diff --git a/examples/containers-resource-constraints-required/constraint.yaml b/examples/containers-resource-constraints/constraint.yaml
old mode 100755
new mode 100644
similarity index 69%
rename from examples/containers-resource-constraints-required/constraint.yaml
rename to examples/containers-resource-constraints/constraint.yaml
index 6f364f0..5f1d5fa
--- a/examples/containers-resource-constraints-required/constraint.yaml
+++ b/examples/containers-resource-constraints/constraint.yaml
@@ -1,7 +1,7 @@
 apiVersion: constraints.gatekeeper.sh/v1beta1
-kind: ContainersResourceConstraintsRequired
+kind: ContainersResourceConstraints
 metadata:
-  name: containersresourceconstraintsrequired
+  name: containersresourceconstraints
 spec:
   match:
     kinds:
diff --git a/examples/containers-resource-constraints-required/kustomization.yaml b/examples/containers-resource-constraints/kustomization.yaml
similarity index 97%
rename from examples/containers-resource-constraints-required/kustomization.yaml
rename to examples/containers-resource-constraints/kustomization.yaml
index b59b300..a73a066 100644
--- a/examples/containers-resource-constraints-required/kustomization.yaml
+++ b/examples/containers-resource-constraints/kustomization.yaml
@@ -1,3 +1,4 @@
+  
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 
diff --git a/examples/containers-resource-constraints-required/src.rego b/examples/containers-resource-constraints/src.rego
similarity index 63%
rename from examples/containers-resource-constraints-required/src.rego
rename to examples/containers-resource-constraints/src.rego
index 3e3b59f..27b6546 100644
--- a/examples/containers-resource-constraints-required/src.rego
+++ b/examples/containers-resource-constraints/src.rego
@@ -2,8 +2,12 @@ package policy
 
 import data.lib.k8s
 
-# Containers must have resource constraints specified.
-# @Kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+# @title Containers must define resource constraints
+#
+# Resource constraints on containers ensure that a given workload does not take up more resources than it required
+# and potentially starve other applications that need to run.
+#
+# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
 violation[msg] {
   containers_resource_constraints_required
 
@@ -20,4 +24,4 @@ container_resources_provided {
   k8s.containers[_].resources.requests.memory
   k8s.containers[_].resources.limits.cpu
   k8s.containers[_].resources.limits.memory
-}
+}
\ No newline at end of file
diff --git a/examples/containers-resource-constraints-required/template.yaml b/examples/containers-resource-constraints/template.yaml
old mode 100755
new mode 100644
similarity index 91%
rename from examples/containers-resource-constraints-required/template.yaml
rename to examples/containers-resource-constraints/template.yaml
index 5b55f6f..3052d37
--- a/examples/containers-resource-constraints-required/template.yaml
+++ b/examples/containers-resource-constraints/template.yaml
@@ -2,12 +2,12 @@ apiVersion: templates.gatekeeper.sh/v1beta1
 kind: ConstraintTemplate
 metadata:
   creationTimestamp: null
-  name: containersresourceconstraintsrequired
+  name: containersresourceconstraints
 spec:
   crd:
     spec:
       names:
-        kind: ContainersResourceConstraintsRequired
+        kind: ContainersResourceConstraints
   targets:
   - libs:
     - |
@@ -139,48 +139,32 @@ spec:
         volume = pod.spec.volumes[_]
       }
 
-      # 10 ** 21
       mem_multiple("E") = 1000000000000000000000 { true }
 
-      # 10 ** 18
       mem_multiple("P") = 1000000000000000000 { true }
 
-      # 10 ** 15
       mem_multiple("T") = 1000000000000000 { true }
 
-      # 10 ** 12
       mem_multiple("G") = 1000000000000 { true }
 
-      # 10 ** 9
       mem_multiple("M") = 1000000000 { true }
 
-      # 10 ** 6
       mem_multiple("k") = 1000000 { true }
 
-      # 10 ** 3
       mem_multiple("") = 1000 { true }
 
-      # Kubernetes accepts millibyte precision when it probably shouldn't.
-      # https://github.com/kubernetes/kubernetes/issues/28741
-      # 10 ** 0
       mem_multiple("m") = 1 { true }
 
-      # 1000 * 2 ** 10
       mem_multiple("Ki") = 1024000 { true }
 
-      # 1000 * 2 ** 20
       mem_multiple("Mi") = 1048576000 { true }
 
-      # 1000 * 2 ** 30
       mem_multiple("Gi") = 1073741824000 { true }
 
-      # 1000 * 2 ** 40
       mem_multiple("Ti") = 1099511627776000 { true }
 
-      # 1000 * 2 ** 50
       mem_multiple("Pi") = 1125899906842624000 { true }
 
-      # 1000 * 2 ** 60
       mem_multiple("Ei") = 1152921504606846976000 { true }
 
       get_suffix(mem) = suffix {
@@ -292,13 +276,11 @@ spec:
         has_field(c, "securityContext")
         has_field(c.securityContext, "allowPrivilegeEscalation")
       }
-    rego: |
+    rego: |-
       package policy
 
       import data.lib.k8s
 
-      # Containers must have resource constraints specified.
-      # @Kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
       violation[msg] {
         containers_resource_constraints_required
 
diff --git a/examples/policies.md b/examples/policies.md
index c5d82ae..8927719 100755
--- a/examples/policies.md
+++ b/examples/policies.md
@@ -1,6 +1,110 @@
 # Policies
 
-|Name|Rule Types|API Groups|Kinds|Description|
-|---|---|---|---|---|
-|[Container Images](container-images)|violation|apps, core|DaemonSet, Deployment, StatefulSet, Pod|Container images can not use the latest tag.|
-|[Containers Resource Constraints Required](containers-resource-constraints-required)|violation|apps, core|DaemonSet, Deployment, StatefulSet, Pod|Containers must have resource constraints specified.|
+## Violations
+
+* [Images must not use the latest tag](#Images-must-not-use-the-latest-tag)
+* [Containers must define resource constraints](#Containers-must-define-resource-constraints)
+
+## Warnings
+
+* [Deprecated Deployment and DaemonSet API](#Deprecated-Deployment-and-DaemonSet-API)
+
+## Images must not use the latest tag
+
+**Severity:** violation
+
+**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+
+
+Using the latest tag on images can cause unexpected problems in production. By specifing a pinned version
+we can have higher confidence that our applications are immutable and do not change unexpectedly.
+
+### Rego
+
+```rego
+package main
+
+import data.lib.k8s
+
+violation[msg] {
+  has_latest_tag
+
+  msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
+}
+
+has_latest_tag {
+  endswith(k8s.container_images[_], ":latest")
+}
+
+has_latest_tag {
+  contains(k8s.container_images[_], ":") == false
+}
+
+```
+_source: [containers-latest-tag](containers-latest-tag)_
+
+## Containers must define resource constraints
+
+**Severity:** violation
+
+**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+
+
+Resource constraints on containers ensure that a given workload does not take up more resources than it required
+and potentially starve other applications that need to run.
+
+### Rego
+
+```rego
+package policy
+
+import data.lib.k8s
+
+violation[msg] {
+  containers_resource_constraints_required
+
+  msg := k8s.format(sprintf("(%s) %s: Container resource constraints must be specified", [k8s.kind, k8s.name]))
+}
+
+containers_resource_constraints_required {
+  k8s.is_workload
+  not container_resources_provided
+}
+
+container_resources_provided {
+  k8s.containers[_].resources.requests.cpu
+  k8s.containers[_].resources.requests.memory
+  k8s.containers[_].resources.limits.cpu
+  k8s.containers[_].resources.limits.memory
+}
+```
+_source: [containers-resource-constraints](containers-resource-constraints)_
+
+## Deprecated Deployment and DaemonSet API
+
+**Severity:** warn
+
+**Resources:** apps/DaemonSet apps/Deployment
+
+
+The `extensions/v1beta1 API` has been deprecated in favor of `apps/v1`. Later versions of Kubernetes
+remove this API so to ensure that the Deployment or DaemonSet can be successfully deployed to the cluster,
+the version for both of these resources must be `apps/v1`.
+
+### Rego
+
+```rego
+package main
+
+import data.lib.k8s
+
+warn[msg] {
+  resources := ["DaemonSet", "Deployment"]
+  input.apiVersion == "extensions/v1beta1"
+  input.kind == resources[_]
+
+  msg := k8s.format(sprintf("%s/%s: API extensions/v1beta1 for %s has been deprecated, use apps/v1 instead.", [input.kind, input.metadata.name, input.kind]))
+}
+
+```
+_source: [warnings](warnings)_
diff --git a/examples/warnings/src.rego b/examples/warnings/src.rego
new file mode 100644
index 0000000..e4e2dc7
--- /dev/null
+++ b/examples/warnings/src.rego
@@ -0,0 +1,18 @@
+package main
+
+import data.lib.k8s
+
+# @title Deprecated Deployment and DaemonSet API
+#
+# The `extensions/v1beta1 API` has been deprecated in favor of `apps/v1`. Later versions of Kubernetes
+# remove this API so to ensure that the Deployment or DaemonSet can be successfully deployed to the cluster,
+# the version for both of these resources must be `apps/v1`.
+# 
+# @kinds apps/DaemonSet apps/Deployment
+warn[msg] {
+  resources := ["DaemonSet", "Deployment"]
+  input.apiVersion == "extensions/v1beta1"
+  input.kind == resources[_]
+
+  msg := k8s.format(sprintf("%s/%s: API extensions/v1beta1 for %s has been deprecated, use apps/v1 instead.", [input.kind, input.metadata.name, input.kind]))
+}
diff --git a/internal/commands/create.go b/internal/commands/create.go
index 69d05f5..838bd23 100644
--- a/internal/commands/create.go
+++ b/internal/commands/create.go
@@ -5,12 +5,12 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 	"strings"
 
+	"github.com/plexsystems/konstraint/internal/rego"
+
 	"github.com/ghodss/yaml"
 	"github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1beta1"
-	"github.com/plexsystems/konstraint/internal/rego"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,14 +32,6 @@ Create constraints with the Gatekeeper enforcement action set to dryrun
 	konstraint create examples --dryrun`,
 
 		RunE: func(cmd *cobra.Command, args []string) error {
-			if err := viper.BindPFlag("ignore", cmd.Flags().Lookup("ignore")); err != nil {
-				return fmt.Errorf("bind ignore flag: %w", err)
-			}
-
-			if err := viper.BindPFlag("lib", cmd.Flags().Lookup("lib")); err != nil {
-				return fmt.Errorf("bind lib flag: %w", err)
-			}
-
 			if err := viper.BindPFlag("dryrun", cmd.PersistentFlags().Lookup("dryrun")); err != nil {
 				return fmt.Errorf("bind dryrun flag: %w", err)
 			}
@@ -64,29 +56,23 @@ Create constraints with the Gatekeeper enforcement action set to dryrun
 }
 
 func runCreateCommand(path string) error {
-	regoFilePaths, err := getRegoFilePaths(path)
+	policies, err := rego.GetFilesWithRule(path, "violation")
 	if err != nil {
-		return fmt.Errorf("get rego files: %w", err)
+		return fmt.Errorf("get policies: %w", err)
 	}
 
-	var libraryFilePaths []string
-	var policyFilePaths []string
-	for _, regoFilePath := range regoFilePaths {
-		if filepath.Base(filepath.Dir(regoFilePath)) != viper.GetString("lib") {
-			policyFilePaths = append(policyFilePaths, regoFilePath)
-		} else {
-			libraryFilePaths = append(libraryFilePaths, regoFilePath)
-		}
+	libraryPath, err := getLibraryPath(path)
+	if err != nil {
+		return fmt.Errorf("get library path: %w", err)
 	}
 
-	policies, err := rego.LoadPoliciesWithAction(policyFilePaths, "violation")
+	libraries, err := rego.GetFiles(libraryPath)
 	if err != nil {
-		return fmt.Errorf("load policies: %w", err)
+		return fmt.Errorf("get libraries: %w", err)
 	}
 
-	libraries, err := rego.LoadLibraries(libraryFilePaths)
-	if err != nil {
-		return fmt.Errorf("load libraries: %w", err)
+	for l := range libraries {
+		libraries[l].Contents = getRegoWithoutComments(libraries[l].Contents)
 	}
 
 	var templateFileName, constraintFileName, outputDir string
@@ -104,8 +90,8 @@ func runCreateCommand(path string) error {
 		if outputFlag == "" {
 			outputDir = policyDir
 		} else {
-			templateFileName = fmt.Sprintf("template_%s.yaml", getKindFromPath(policy.FilePath))
-			constraintFileName = fmt.Sprintf("constraint_%s.yaml", getKindFromPath(policy.FilePath))
+			templateFileName = fmt.Sprintf("template_%s.yaml", GetKindFromPath(policy.FilePath))
+			constraintFileName = fmt.Sprintf("constraint_%s.yaml", GetKindFromPath(policy.FilePath))
 		}
 
 		if _, err := os.Stat(outputDir); os.IsNotExist(err) {
@@ -155,7 +141,7 @@ func getConstraintTemplate(policy rego.File, libraries []rego.File) v1beta1.Cons
 		}
 	}
 
-	kind := getKindFromPath(policy.FilePath)
+	kind := GetKindFromPath(policy.FilePath)
 
 	constraintTemplate := v1beta1.ConstraintTemplate{
 		TypeMeta: metav1.TypeMeta{
@@ -177,7 +163,7 @@ func getConstraintTemplate(policy rego.File, libraries []rego.File) v1beta1.Cons
 				{
 					Target: "admission.k8s.gatekeeper.sh",
 					Libs:   libs,
-					Rego:   policy.Contents,
+					Rego:   getRegoWithoutComments(policy.Contents),
 				},
 			},
 		},
@@ -187,7 +173,7 @@ func getConstraintTemplate(policy rego.File, libraries []rego.File) v1beta1.Cons
 }
 
 func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
-	kind := getKindFromPath(policy.FilePath)
+	kind := GetKindFromPath(policy.FilePath)
 	constraint := unstructured.Unstructured{}
 	constraint.SetName(strings.ToLower(kind))
 	constraint.SetGroupVersionKind(schema.GroupVersionKind{Group: "constraints.gatekeeper.sh", Version: "v1beta1", Kind: kind})
@@ -199,28 +185,31 @@ func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
 		}
 	}
 
-	policyCommentBlocks, err := getPolicyCommentBlocks(policy.Comments)
-	if err != nil {
-		return unstructured.Unstructured{}, fmt.Errorf("get policy comment blocks: %w", err)
-	}
-
-	if len(policyCommentBlocks) == 0 {
+	matchers := GetMatchersFromComments(policy.Comments)
+	if len(matchers.KindMatchers) == 0 {
 		return constraint, nil
 	}
 
 	var kinds []interface{}
 	var apiGroups []interface{}
-	for _, policyCommentBlock := range policyCommentBlocks {
-		for _, policyKind := range policyCommentBlock.Kinds {
-			kinds = append(kinds, policyKind)
+	for _, kindMatcher := range matchers.KindMatchers {
+		kinds = append(kinds, kindMatcher.Kind)
+	}
+
+	for _, kindMatcher := range matchers.KindMatchers {
+		apiGroup := kindMatcher.APIGroup
+		if kindMatcher.APIGroup == "core" {
+			apiGroup = ""
 		}
 
-		for _, policyAPIGroup := range policyCommentBlock.APIGroups {
-			if policyAPIGroup == "core" {
-				policyAPIGroup = ""
+		var exists bool
+		for _, addedGroup := range apiGroups {
+			if apiGroup == addedGroup {
+				exists = true
 			}
-
-			apiGroups = append(apiGroups, policyAPIGroup)
+		}
+		if !exists {
+			apiGroups = append(apiGroups, apiGroup)
 		}
 	}
 
@@ -236,14 +225,11 @@ func getConstraint(policy rego.File) (unstructured.Unstructured, error) {
 	return constraint, nil
 }
 
-func getRegoFilePaths(path string) ([]string, error) {
-	ignoreRegex, err := regexp.Compile(viper.GetString("ignore"))
-	if err != nil {
-		return nil, fmt.Errorf("compile ignore regex: %w", err)
-	}
+func getLibraryPath(path string) (string, error) {
+	libraryFolderNames := []string{"lib", "libs", "util", "utils"}
 
-	var regoFilePaths []string
-	err = filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error {
+	var libraryPath string
+	err := filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error {
 		if err != nil {
 			return fmt.Errorf("walk path: %w", err)
 		}
@@ -252,32 +238,16 @@ func getRegoFilePaths(path string) ([]string, error) {
 			return filepath.SkipDir
 		}
 
-		if fileInfo.IsDir() && ignoreRegex.MatchString(currentFilePath) {
-			return filepath.SkipDir
-		}
-
-		if ignoreRegex.MatchString(currentFilePath) {
+		if fileInfo.IsDir() && contains(libraryFolderNames, fileInfo.Name()) {
+			libraryPath = currentFilePath
 			return nil
 		}
 
-		if filepath.Ext(currentFilePath) != ".rego" || strings.HasSuffix(fileInfo.Name(), "_test.rego") {
-			return nil
-		}
-
-		regoFilePaths = append(regoFilePaths, currentFilePath)
-
 		return nil
 	})
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 
-	return regoFilePaths, nil
-}
-
-func getKindFromPath(path string) string {
-	name := getNameFromPath(path)
-	kind := strings.ReplaceAll(name, " ", "")
-
-	return kind
+	return libraryPath, nil
 }
diff --git a/internal/commands/default.go b/internal/commands/default.go
index 81aee99..05546bd 100644
--- a/internal/commands/default.go
+++ b/internal/commands/default.go
@@ -5,7 +5,6 @@ import (
 	"path"
 
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 )
 
 // NewDefaultCommand creates a new default command
@@ -14,19 +13,9 @@ func NewDefaultCommand() *cobra.Command {
 		Use:     path.Base(os.Args[0]),
 		Short:   "Konstraint",
 		Long:    "A tool to create and manage Gatekeeper CRDs from Rego",
-		Version: "0.4.3",
+		Version: "0.5.0",
 	}
 
-	// ^$ is for match nothing and do not ignore anything
-	ignoreFlagDefault := "^$"
-	libFlagDefault := "lib"
-
-	cmd.PersistentFlags().String("ignore", ignoreFlagDefault, "A regex pattern which can be used for ignoring directories and files")
-	cmd.PersistentFlags().String("lib", libFlagDefault, "The name of the folder where the Rego librarie(s) are")
-
-	viper.BindPFlag("ignore", cmd.PersistentFlags().Lookup("ignore"))
-	viper.BindPFlag("lib", cmd.PersistentFlags().Lookup("lib"))
-
 	cmd.AddCommand(newCreateCommand())
 	cmd.AddCommand(newDocCommand())
 
diff --git a/internal/commands/document.go b/internal/commands/document.go
index 0d7767b..d6b583b 100644
--- a/internal/commands/document.go
+++ b/internal/commands/document.go
@@ -8,15 +8,24 @@ import (
 	"strings"
 
 	"github.com/plexsystems/konstraint/internal/rego"
+
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 )
 
-// PolicyCommentBlock represents a comment block in a rego file
-type PolicyCommentBlock struct {
-	APIGroups   []string
-	Kinds       []string
+// Header is the header comment block found on a Rego policy.
+type Header struct {
+	Title       string
 	Description string
+	Resources   string
+}
+
+// Document is a single policy document.
+type Document struct {
+	Header   Header
+	Severity string
+	URL      string
+	Rego     string
 }
 
 func newDocCommand() *cobra.Command {
@@ -50,97 +59,192 @@ Save the documentation to a specific directory
 
 func runDocCommand(path string) error {
 	outputDirectory := filepath.Dir(viper.GetString("output"))
-	policyDocumentation, err := getPolicyDocumentation(path, outputDirectory)
+	violationDocs, err := getDocumentation(path, "violation", outputDirectory)
 	if err != nil {
-		return fmt.Errorf("get policy documentation: %w", err)
+		return fmt.Errorf("get violation documentation: %w", err)
+	}
+
+	warningDocs, err := getDocumentation(path, "warn", outputDirectory)
+	if err != nil {
+		return fmt.Errorf("get warn documentation: %w", err)
+	}
+
+	documentContents := "# Policies"
+	documentContents += "\n\n"
+
+	// Table of contents (Violations)
+	if len(violationDocs) > 0 {
+		documentContents += "## Violations"
+		documentContents += "\n\n"
+		for _, document := range violationDocs {
+			documentContents += "* [" + document.Header.Title + "](#" + strings.ReplaceAll(document.Header.Title, " ", "-") + ")"
+			documentContents += "\n"
+		}
+
+		documentContents += "\n"
+	}
+
+	// Table of contents (Warnings)
+	if len(warningDocs) > 0 {
+		documentContents += "## Warnings"
+		documentContents += "\n\n"
+		for _, document := range warningDocs {
+			documentContents += "* [" + document.Header.Title + "](#" + strings.ReplaceAll(document.Header.Title, " ", "-") + ")"
+			documentContents += "\n"
+		}
+
+		documentContents += "\n"
+	}
+
+	for _, document := range violationDocs {
+		documentContents += "## " + document.Header.Title
+		documentContents += "\n\n"
+
+		documentContents += "**Severity:** " + document.Severity
+		documentContents += "\n\n"
+
+		documentContents += "**Resources:** " + document.Header.Resources
+		documentContents += "\n\n"
+
+		documentContents += document.Header.Description
+		documentContents += "\n"
+
+		documentContents += "### Rego"
+		documentContents += "\n\n"
+
+		documentContents += "```rego"
+		documentContents += "\n"
+
+		documentContents += document.Rego
+		documentContents += "\n"
+
+		documentContents += "```"
+		documentContents += "\n"
+
+		documentContents += "_source: [" + document.URL + "](" + document.URL + ")_"
+		documentContents += "\n\n"
 	}
 
-	if err := ioutil.WriteFile(viper.GetString("output"), []byte(policyDocumentation), os.ModePerm); err != nil {
+	for _, document := range warningDocs {
+		documentContents += "## " + document.Header.Title
+		documentContents += "\n\n"
+
+		documentContents += "**Severity:** " + document.Severity
+		documentContents += "\n\n"
+
+		documentContents += "**Resources:** " + document.Header.Resources
+		documentContents += "\n\n"
+
+		documentContents += document.Header.Description
+		documentContents += "\n"
+
+		documentContents += "### Rego"
+		documentContents += "\n\n"
+
+		documentContents += "```rego"
+		documentContents += "\n"
+
+		documentContents += document.Rego
+		documentContents += "\n"
+
+		documentContents += "```"
+		documentContents += "\n"
+
+		documentContents += "_source: [" + document.URL + "](" + document.URL + ")_"
+		documentContents += "\n\n"
+	}
+
+	documentContents = strings.TrimSuffix(documentContents, "\n")
+	if err := ioutil.WriteFile(viper.GetString("output"), []byte(documentContents), os.ModePerm); err != nil {
 		return fmt.Errorf("writing documentation: %w", err)
 	}
 
 	return nil
 }
 
-func getPolicyDocumentation(path string, outputDirectory string) (string, error) {
-	regoFilePaths, err := getRegoFilePaths(path)
+func getDocumentation(path string, severity string, outputDirectory string) ([]Document, error) {
+	policies, err := rego.GetFilesWithRule(path, severity)
 	if err != nil {
-		return "", fmt.Errorf("get rego files: %w", err)
+		return nil, fmt.Errorf("get files: %w", err)
 	}
 
-	policies, err := rego.LoadPolicies(regoFilePaths)
-	if err != nil {
-		return "", fmt.Errorf("load policies: %w", err)
-	}
-
-	policyDocument := "# Policies\n\n"
-	policyDocument += "|Name|Rule Types|API Groups|Kinds|Description|\n"
-	policyDocument += "|---|---|---|---|---|\n"
+	fmt.Println(policies)
 
+	var documents []Document
 	for _, policy := range policies {
-		policyCommentBlocks, err := getPolicyCommentBlocks(policy.Comments)
+		header, err := getHeader(policy.Comments)
 		if err != nil {
-			return "", fmt.Errorf("get policy comment blocks: %w", err)
+			return nil, fmt.Errorf("get header: %w", err)
 		}
 
-		for _, policyCommentBlock := range policyCommentBlocks {
-			relPath, err := filepath.Rel(outputDirectory, policy.FilePath)
-			if err != nil {
-				return "", fmt.Errorf("rel path: %w", err)
-			}
+		relPath, err := filepath.Rel(outputDirectory, policy.FilePath)
+		if err != nil {
+			return nil, fmt.Errorf("rel path: %w", err)
+		}
+		relDir := filepath.Dir(relPath)
+
+		regoWithoutComments := getRegoWithoutComments(policy.Contents)
 
-			relDir := filepath.Dir(relPath)
-			ruleTypes := strings.Join(policy.RulesActions, ", ")
-			apiGroups := strings.Join(policyCommentBlock.APIGroups, ", ")
-			kinds := strings.Join(policyCommentBlock.Kinds, ", ")
-
-			policyDocument += fmt.Sprintf("|[%s](%s)|%s|%s|%s|%s|\n",
-				getNameFromPath(policy.FilePath),
-				relDir,
-				ruleTypes,
-				apiGroups,
-				kinds,
-				policyCommentBlock.Description,
-			)
+		document := Document{
+			Header:   header,
+			Severity: severity,
+			URL:      relDir,
+			Rego:     regoWithoutComments,
 		}
+
+		documents = append(documents, document)
 	}
 
-	return policyDocument, nil
+	return documents, nil
 }
 
-func getPolicyCommentBlocks(comments []string) ([]PolicyCommentBlock, error) {
-	var policyCommentBlocks []PolicyCommentBlock
+func getHeader(comments []string) (Header, error) {
+	var title string
 	var description string
+	var resources string
 	for _, comment := range comments {
-		if !strings.Contains(comment, "@Kinds") {
-			description = comment
+		if strings.Contains(comment, "@title") {
+			title = strings.SplitAfter(comment, "@title")[1]
+			title = strings.TrimPrefix(title, " ")
 			continue
 		}
 
-		kindGroups := strings.Split(comment, " ")[2:]
+		if strings.Contains(comment, "@kinds") {
+			matchers := GetMatchersFromComments([]string{comment})
+			for _, kindMatcher := range matchers.KindMatchers {
+				resources += kindMatcher.APIGroup + "/" + kindMatcher.Kind + " "
+			}
+			break
+		}
 
-		var apiGroups []string
-		var kinds []string
-		for _, kindGroup := range kindGroups {
-			kindTokens := strings.Split(kindGroup, "/")
+		comment = strings.TrimSpace(comment)
+		description += comment + "\n"
+	}
 
-			if !contains(apiGroups, kindTokens[0]) {
-				apiGroups = append(apiGroups, kindTokens[0])
-			}
+	description = strings.TrimSuffix(description, "\n")
 
-			kinds = append(kinds, kindTokens[1])
-		}
+	header := Header{
+		Title:       strings.Trim(title, " "),
+		Description: strings.Trim(description, " "),
+		Resources:   strings.Trim(resources, " "),
+	}
 
-		policyCommentBlock := PolicyCommentBlock{
-			APIGroups:   apiGroups,
-			Kinds:       kinds,
-			Description: strings.Trim(description, " "),
+	return header, nil
+}
+
+func getRegoWithoutComments(rego string) string {
+	var regoWithoutComments string
+	lines := strings.Split(rego, "\n")
+	for _, line := range lines {
+		if strings.HasPrefix(line, "#") {
+			continue
 		}
 
-		policyCommentBlocks = append(policyCommentBlocks, policyCommentBlock)
+		regoWithoutComments += line + "\n"
 	}
 
-	return policyCommentBlocks, nil
+	regoWithoutComments = strings.TrimSuffix(regoWithoutComments, "\n")
+	return regoWithoutComments
 }
 
 func contains(collection []string, item string) bool {
@@ -152,11 +256,3 @@ func contains(collection []string, item string) bool {
 
 	return false
 }
-
-func getNameFromPath(path string) string {
-	name := filepath.Base(filepath.Dir(path))
-	name = strings.ReplaceAll(name, "-", " ")
-	name = strings.Title(name)
-
-	return name
-}
diff --git a/internal/commands/gatekeeper.go b/internal/commands/gatekeeper.go
new file mode 100644
index 0000000..4414497
--- /dev/null
+++ b/internal/commands/gatekeeper.go
@@ -0,0 +1,63 @@
+package commands
+
+import (
+	"path/filepath"
+	"strings"
+)
+
+// Matchers are all of the matchers that can be applied to constraints.
+type Matchers struct {
+	KindMatchers []KindMatcher
+}
+
+// KindMatcher are the matchers that are applied to constraints.
+type KindMatcher struct {
+	APIGroup string
+	Kind     string
+}
+
+// GetMatchersFromComments returns all of the matchers found in the collection of comments.
+func GetMatchersFromComments(comments []string) Matchers {
+	var matchers Matchers
+	for _, comment := range comments {
+		if strings.Contains(comment, "@kinds") {
+			matchers.KindMatchers = getKindMatchers(comment)
+		}
+	}
+
+	return matchers
+}
+
+// GetKindFromPath returns the kind of the resource based on its file path.
+func GetKindFromPath(path string) string {
+	name := getNameFromPath(path)
+	kind := strings.ReplaceAll(name, " ", "")
+
+	return kind
+}
+
+func getKindMatchers(comment string) []KindMatcher {
+	var kindMatchers []KindMatcher
+
+	kindMatcherGroups := strings.Split(comment, " ")[2:]
+	for _, kindMatcherGroup := range kindMatcherGroups {
+		kindMatcherSegments := strings.Split(kindMatcherGroup, "/")
+
+		kindMatcher := KindMatcher{
+			APIGroup: kindMatcherSegments[0],
+			Kind:     kindMatcherSegments[1],
+		}
+
+		kindMatchers = append(kindMatchers, kindMatcher)
+	}
+
+	return kindMatchers
+}
+
+func getNameFromPath(path string) string {
+	name := filepath.Base(filepath.Dir(path))
+	name = strings.ReplaceAll(name, "-", " ")
+	name = strings.Title(name)
+
+	return name
+}
diff --git a/internal/rego/rego.go b/internal/rego/rego.go
index 34e7c0c..f374adc 100644
--- a/internal/rego/rego.go
+++ b/internal/rego/rego.go
@@ -3,6 +3,8 @@ package rego
 import (
 	"fmt"
 	"io/ioutil"
+	"os"
+	"path/filepath"
 	"regexp"
 	"sort"
 	"strings"
@@ -10,50 +12,44 @@ import (
 	"github.com/open-policy-agent/opa/ast"
 )
 
-// File is a parsed Rego file
+// File is a parsed Rego file.
 type File struct {
 	FilePath       string
 	PackageName    string
 	ImportPackages []string
 	Contents       string
-	RulesActions   []string
+	RuleNames      []string
 	Comments       []string
 }
 
-// LoadPoliciesWithAction loads all policies from rego with rules with a given action name
-func LoadPoliciesWithAction(files []string, action string) ([]File, error) {
-	regoFiles, err := loadRegoFiles(files)
+// GetFiles gets all Rego files in the given path and its subdirectories.
+func GetFiles(path string) ([]File, error) {
+	filePaths, err := getFilePaths(path)
 	if err != nil {
-		return nil, fmt.Errorf("load rego files: %w", err)
+		return nil, fmt.Errorf("load files: %w", err)
 	}
 
-	policies := getPoliciesWithAction(regoFiles, action)
-	return policies, nil
-}
-
-// LoadPolicies loads all policies from rego with rules
-func LoadPolicies(files []string) ([]File, error) {
-	regoFiles, err := loadRegoFiles(files)
+	files, err := getFiles(filePaths)
 	if err != nil {
-		return nil, fmt.Errorf("load rego files: %w", err)
+		return nil, fmt.Errorf("load files: %w", err)
 	}
 
-	policies := getPolicies(regoFiles)
-	return policies, nil
+	return files, nil
 }
 
-// LoadLibraries loads all libraries from rego
-func LoadLibraries(files []string) ([]File, error) {
-	regoFiles, err := loadRegoFiles(files)
+// GetFilesWithRule gets all Rego files in the given path and its subdirectories that contain the specified rule.
+func GetFilesWithRule(path string, rule string) ([]File, error) {
+	allFiles, err := GetFiles(path)
 	if err != nil {
-		return nil, fmt.Errorf("load rego files: %w", err)
+		return nil, fmt.Errorf("load policies: %w", err)
 	}
 
-	return regoFiles, nil
+	filesWithRule := getFilesWithRule(allFiles, rule)
+	return filesWithRule, nil
 }
 
-// NewRegoFile parses the rego and creates a File
-func NewRegoFile(filePath string, contents string) (File, error) {
+// NewFile parses the rego and creates a File
+func NewFile(filePath string, contents string) (File, error) {
 	module, err := ast.ParseModule(filePath, contents)
 	if err != nil {
 		return File{}, fmt.Errorf("parse module: %w", err)
@@ -64,29 +60,60 @@ func NewRegoFile(filePath string, contents string) (File, error) {
 		importPackages = append(importPackages, module.Imports[i].Path.String())
 	}
 
-	rulesActions, err := getModuleRulesActions(module)
+	ruleNames, err := getRuleNamesFromModule(module)
 	if err != nil {
 		return File{}, fmt.Errorf("get module rules: %w", err)
 	}
 
+	var comments []string
+	for _, comment := range module.Comments {
+		comments = append(comments, string(comment.Text))
+	}
+
 	file := File{
 		FilePath:       filePath,
 		PackageName:    module.Package.Path.String(),
 		ImportPackages: importPackages,
 		Contents:       contents,
-		RulesActions:   rulesActions,
-		Comments:       getModuleComments(module),
+		RuleNames:      ruleNames,
+		Comments:       comments,
 	}
 
 	return file, nil
 }
 
-func getPoliciesWithAction(regoFiles []File, action string) []File {
+func getFilePaths(path string) ([]string, error) {
+	var regoFilePaths []string
+	err := filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error {
+		if err != nil {
+			return fmt.Errorf("walk path: %w", err)
+		}
+
+		if fileInfo.IsDir() && fileInfo.Name() == ".git" {
+			return filepath.SkipDir
+		}
+
+		if filepath.Ext(currentFilePath) != ".rego" || strings.HasSuffix(fileInfo.Name(), "_test.rego") {
+			return nil
+		}
+
+		regoFilePaths = append(regoFilePaths, currentFilePath)
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return regoFilePaths, nil
+}
+
+func getFilesWithRule(regoFiles []File, rule string) []File {
 	var matchingPolicies []File
 	allPolicies := getPolicies(regoFiles)
 	for _, policy := range allPolicies {
-		for _, ruleAction := range policy.RulesActions {
-			if ruleAction == action {
+		for _, ruleName := range policy.RuleNames {
+			if ruleName == rule {
 				matchingPolicies = append(matchingPolicies, policy)
 			}
 		}
@@ -98,7 +125,7 @@ func getPoliciesWithAction(regoFiles []File, action string) []File {
 func getPolicies(regoFiles []File) []File {
 	var policies []File
 	for _, regoFile := range regoFiles {
-		if len(regoFile.RulesActions) > 0 {
+		if len(regoFile.RuleNames) > 0 {
 			policies = append(policies, regoFile)
 		}
 	}
@@ -106,7 +133,7 @@ func getPolicies(regoFiles []File) []File {
 	return policies
 }
 
-func loadRegoFiles(files []string) ([]File, error) {
+func getFiles(files []string) ([]File, error) {
 	filesContents, err := readFilesContents(files)
 	if err != nil {
 		return nil, fmt.Errorf("read files: %w", err)
@@ -114,7 +141,7 @@ func loadRegoFiles(files []string) ([]File, error) {
 
 	var regoFiles []File
 	for path, contents := range filesContents {
-		regoFile, err := NewRegoFile(path, contents)
+		regoFile, err := NewFile(path, contents)
 		if err != nil {
 			return nil, fmt.Errorf("new rego file: %w", err)
 		}
@@ -129,34 +156,25 @@ func loadRegoFiles(files []string) ([]File, error) {
 	return regoFiles, nil
 }
 
-func getModuleRulesActions(module *ast.Module) ([]string, error) {
+func getRuleNamesFromModule(module *ast.Module) ([]string, error) {
 	re, err := regexp.Compile(`^\s*([a-z]+)\s*\[\s*msg`)
 	if err != nil {
 		return nil, fmt.Errorf("compile regex: %w", err)
 	}
 
-	var rulesActions []string
+	var ruleNames []string
 	for _, rule := range module.Rules {
 		match := re.FindStringSubmatch(rule.Head.String())
 		if len(match) == 0 {
 			continue
 		}
-		if contains(rulesActions, match[1]) {
+		if contains(ruleNames, match[1]) {
 			continue
 		}
-		rulesActions = append(rulesActions, match[1])
-	}
-
-	return rulesActions, nil
-}
-
-func getModuleComments(module *ast.Module) []string {
-	var comments []string
-	for _, comment := range module.Comments {
-		comments = append(comments, string(comment.Text))
+		ruleNames = append(ruleNames, match[1])
 	}
 
-	return comments
+	return ruleNames, nil
 }
 
 func readFilesContents(filePaths []string) (map[string]string, error) {


Test patch:
diff --git a/examples/containers-latest-tag/src.rego b/examples/containers-latest-tag/src.rego
new file mode 100644
index 0000000..94adb78
--- /dev/null
+++ b/examples/containers-latest-tag/src.rego
@@ -0,0 +1,23 @@
+package main
+
+import data.lib.k8s
+
+# @title Images must not use the latest tag
+#
+# Using the latest tag on images can cause unexpected problems in production. By specifing a pinned version
+# we can have higher confidence that our applications are immutable and do not change unexpectedly.
+#
+# @kinds apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+violation[msg] {
+  has_latest_tag
+
+  msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
+}
+
+has_latest_tag {
+  endswith(k8s.container_images[_], ":latest")
+}
+
+has_latest_tag {
+  contains(k8s.container_images[_], ":") == false
+}
diff --git a/examples/containers-resource-constraints-required/src_test.rego b/examples/containers-resource-constraints/src_test.rego
similarity index 99%
rename from examples/containers-resource-constraints-required/src_test.rego
rename to examples/containers-resource-constraints/src_test.rego
index 67533ff..bda2c08 100644
--- a/examples/containers-resource-constraints-required/src_test.rego
+++ b/examples/containers-resource-constraints/src_test.rego
@@ -43,4 +43,3 @@ test_input_as_container_with_all_constraints {
 
   not containers_resource_constraints_required with input as input
 }
-
diff --git a/examples/containers-resource-constraints-required/test.yaml b/examples/containers-resource-constraints/test.yaml
similarity index 79%
rename from examples/containers-resource-constraints-required/test.yaml
rename to examples/containers-resource-constraints/test.yaml
index 91b4e9d..bf3d4f6 100644
--- a/examples/containers-resource-constraints-required/test.yaml
+++ b/examples/containers-resource-constraints/test.yaml
@@ -5,4 +5,4 @@ metadata:
 spec:
   containers:
   - name: containers-resource-constraints-required-test
-    image: plexdev.azurecr.io/pcp/test:1.0
+    image: plexdev.azurecr.io/pcp/test:1.0
\ No newline at end of file
diff --git a/internal/commands/create_test.go b/internal/commands/create_test.go
index d4dc510..84b7ef0 100644
--- a/internal/commands/create_test.go
+++ b/internal/commands/create_test.go
@@ -13,7 +13,7 @@ func TestGetConstraint_NoKinds_ReturnsEmptyMatcher(t *testing.T) {
 # Description
 rule[msg] { msg = true }`
 
-	parsedPolicy, err := rego.NewRegoFile("test.rego", policy)
+	parsedPolicy, err := rego.NewFile("test.rego", policy)
 	if err != nil {
 		t.Fatal("new rego file:", err)
 	}
@@ -31,10 +31,10 @@ rule[msg] { msg = true }`
 func TestGetConstraint_KindsInComment_ReturnsKinds(t *testing.T) {
 	policy := `package test
 # Description
-# @Kinds core/Pod apps/Deployment
+# @kinds core/Pod apps/Deployment
 rule[msg] { msg = true }`
 
-	rego, err := rego.NewRegoFile("test.rego", policy)
+	rego, err := rego.NewFile("test.rego", policy)
 	if err != nil {
 		t.Fatal("load policy rego file:", err)
 	}
@@ -76,7 +76,7 @@ rule[msg] { msg = true }`
 
 	libraryRegos := []string{`package lib.foo`, `package lib.bar`}
 
-	policyFile, err := rego.NewRegoFile("/foo/test-kind/src.rego", policyImportsFoo)
+	policyFile, err := rego.NewFile("/foo/test-kind/src.rego", policyImportsFoo)
 	if err != nil {
 		t.Fatal("new rego file:", err)
 	}
@@ -85,7 +85,7 @@ rule[msg] { msg = true }`
 	for key, library := range libraryRegos {
 		libraryPath := fmt.Sprintf("/foo/lib/library-%v.rego", key)
 
-		libraryFile, err := rego.NewRegoFile(libraryPath, library)
+		libraryFile, err := rego.NewFile(libraryPath, library)
 		if err != nil {
 			t.Fatal("new rego file:", err)
 		}
@@ -112,7 +112,7 @@ func TestGetKindFromPath(t *testing.T) {
 	path := "/path/to/rego/container-resource-limits/something.rego"
 
 	expected := "ContainerResourceLimits"
-	actual := getKindFromPath(path)
+	actual := GetKindFromPath(path)
 
 	if actual != expected {
 		t.Errorf("expected Kind of %v, but got %v", expected, actual)
diff --git a/internal/commands/document_test.go b/internal/commands/document_test.go
index 502005b..bfbbe9a 100644
--- a/internal/commands/document_test.go
+++ b/internal/commands/document_test.go
@@ -6,66 +6,55 @@ import (
 	"github.com/plexsystems/konstraint/internal/rego"
 )
 
-func TestGetPolicyCommentBlocks_NoKinds(t *testing.T) {
+func TestGetMatchersFromComments_NoKinds(t *testing.T) {
 	policy := `package test
 # Description
 violation[msg] {
 	false
 }`
 
-	regoFile, err := rego.NewRegoFile("test.rego", policy)
+	regoFile, err := rego.NewFile("test.rego", policy)
 	if err != nil {
 		t.Fatal("new rego file", err)
 	}
 
-	actual, err := getPolicyCommentBlocks(regoFile.Comments)
-	if err != nil {
-		t.Fatal("get policy comment blocks:", err)
-	}
-
-	if len(actual) > 0 {
-		t.Error("expected no comment blocks, but comment blocks were returned")
+	actual := GetMatchersFromComments(regoFile.Comments)
+	if len(actual.KindMatchers) > 0 {
+		t.Error("expected no Kind matchers, but matchers were returned")
 	}
 }
 
-func TestGetPolicyCommentBlocks(t *testing.T) {
+func TestGetMatchersFromComments_Kinds(t *testing.T) {
 	policy := `package test
 # First description
-# @Kinds core/Pod apps/Deployment apps/DaemonSet
+# @kinds core/Pod apps/Deployment apps/DaemonSet
 violation[msg] {
 	false
 }`
 
-	regoFile, err := rego.NewRegoFile("test.rego", policy)
+	regoFile, err := rego.NewFile("test.rego", policy)
 	if err != nil {
 		t.Fatal("new rego file", err)
 	}
 
-	actual, err := getPolicyCommentBlocks(regoFile.Comments)
-	if err != nil {
-		t.Fatal("get policy comment blocks:", err)
-	}
-
-	if len(actual) == 0 {
-		t.Errorf("expected policy block to exist, but one did not.")
-	}
+	actual := GetMatchersFromComments(regoFile.Comments)
 
-	expectedAPIGroupCount := 2
-	if len(actual[0].APIGroups) != expectedAPIGroupCount {
-		t.Errorf("expected %v APIGroups to exists but %v were found", expectedAPIGroupCount, len(actual[0].APIGroups))
+	expectedMatcherCount := 3
+	if len(actual.KindMatchers) != expectedMatcherCount {
+		t.Errorf("expected %v matchers to exist, but %v were found", expectedMatcherCount, len(actual.KindMatchers))
 	}
 
-	expectedGroups := []string{"core", "apps"}
-	for _, expectedGroup := range expectedGroups {
-		if !contains(actual[0].APIGroups, expectedGroup) {
-			t.Errorf("expected policy block to contain '%v' APIGroup, but was not found.", expectedGroup)
+	expectedGroups := []string{"core", "apps", "apps"}
+	for g, kindMatcher := range actual.KindMatchers {
+		if kindMatcher.APIGroup != expectedGroups[g] {
+			t.Errorf("expected group to be %v, but was %v", expectedGroups[g], kindMatcher.APIGroup)
 		}
 	}
 
-	expectedKinds := []string{"Pod", "DaemonSet", "Deployment"}
-	for _, expectedKind := range expectedKinds {
-		if !contains(actual[0].Kinds, expectedKind) {
-			t.Errorf("expected policy block to contain '%v' APIGroup, but was not found.", expectedKind)
+	expectedKinds := []string{"Pod", "Deployment", "DaemonSet"}
+	for k, kindMatcher := range actual.KindMatchers {
+		if kindMatcher.Kind != expectedKinds[k] {
+			t.Errorf("expected kind to be %v, but was %v", expectedKinds[k], kindMatcher.Kind)
 		}
 	}
 }
diff --git a/internal/rego/rego_test.go b/internal/rego/rego_test.go
index 2b324fb..c6173ac 100644
--- a/internal/rego/rego_test.go
+++ b/internal/rego/rego_test.go
@@ -8,28 +8,28 @@ import (
 var (
 	testRegoFiles = []File{
 		{
-			FilePath:     "missingViolation.rego",
-			Contents:     "package test\ndefault a = true",
-			RulesActions: nil,
+			FilePath:  "missingViolation.rego",
+			Contents:  "package test\ndefault a = true",
+			RuleNames: nil,
 		},
 		{
-			FilePath:     "withViolation.rego",
-			Contents:     "package test\nviolation[msg] { msg = true }",
-			RulesActions: []string{"violation"},
+			FilePath:  "withViolation.rego",
+			Contents:  "package test\nviolation[msg] { msg = true }",
+			RuleNames: []string{"violation"},
 		},
 		{
-			FilePath:     "withWarn.rego",
-			Contents:     "package test\nwarn[msg] { msg = true }",
-			RulesActions: []string{"warn"},
+			FilePath:  "withWarn.rego",
+			Contents:  "package test\nwarn[msg] { msg = true }",
+			RuleNames: []string{"warn"},
 		},
 	}
 )
 
-func TestGetModulesRulesActions(t *testing.T) {
-	var rulesActionsTests = []struct {
-		policy      string
-		ruleCount   int
-		ruleActions []string
+func TestNewFile_RuleNames(t *testing.T) {
+	var rulesNamesTests = []struct {
+		policy    string
+		ruleCount int
+		ruleNames []string
 	}{
 		{"package test\ndefault test = true", 0, nil},
 		{"package test\nviolation[msg] { msg = true }", 1, []string{"violation"}},
@@ -38,18 +38,18 @@ func TestGetModulesRulesActions(t *testing.T) {
 		{"package test\nviolation[msg] { msg = true }\nviolation[msg] { msg = true }", 1, []string{"violation"}},
 	}
 
-	for _, test := range rulesActionsTests {
-		regoFile, err := NewRegoFile("test.rego", test.policy)
+	for _, test := range rulesNamesTests {
+		regoFile, err := NewFile("test.rego", test.policy)
 		if err != nil {
 			t.Fatal("newRegoFile")
 		}
 
-		if len(regoFile.RulesActions) != test.ruleCount {
-			t.Error("incorrect rule actions count")
+		if len(regoFile.RuleNames) != test.ruleCount {
+			t.Errorf("expected rule names count to be %v, but was %v", test.ruleCount, len(regoFile.RuleNames))
 		}
 
-		if !reflect.DeepEqual(regoFile.RulesActions, test.ruleActions) {
-			t.Errorf("incorrect rule actions\nreceived: %v\nexpected: %v", regoFile.RulesActions, test.ruleActions)
+		if !reflect.DeepEqual(regoFile.RuleNames, test.ruleNames) {
+			t.Errorf("expected rule names to be %v, but was %v", test.ruleNames, regoFile.RuleNames)
 		}
 	}
 }
@@ -57,13 +57,13 @@ func TestGetModulesRulesActions(t *testing.T) {
 func TestGetPolicies(t *testing.T) {
 	policies := getPolicies(testRegoFiles)
 	if len(policies) != 2 {
-		t.Error("incorrect number of policies loaded")
+		t.Errorf("expected %v policies, but got %v", 2, len(policies))
 	}
 }
 
-func TestGetPoliciesWithAction(t *testing.T) {
-	matchingPolicies := getPoliciesWithAction(testRegoFiles, "warn")
+func TestGetPoliciesWithRule(t *testing.T) {
+	matchingPolicies := getFilesWithRule(testRegoFiles, "warn")
 	if len(matchingPolicies) != 1 {
-		t.Error("incorrect number of policies loaded")
+		t.Errorf("expected %v policies, but got %v", 1, len(matchingPolicies))
 	}
 }
diff --git a/test/create/constraint_ContainersLatestTag.yaml b/test/create/constraint_ContainersLatestTag.yaml
new file mode 100755
index 0000000..c8b04c7
--- /dev/null
+++ b/test/create/constraint_ContainersLatestTag.yaml
@@ -0,0 +1,15 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: ContainersLatestTag
+metadata:
+  name: containerslatesttag
+spec:
+  match:
+    kinds:
+    - apiGroups:
+      - apps
+      - ""
+      kinds:
+      - DaemonSet
+      - Deployment
+      - StatefulSet
+      - Pod
diff --git a/test/create/constraint_ContainersResourceConstraints.yaml b/test/create/constraint_ContainersResourceConstraints.yaml
new file mode 100755
index 0000000..5f1d5fa
--- /dev/null
+++ b/test/create/constraint_ContainersResourceConstraints.yaml
@@ -0,0 +1,15 @@
+apiVersion: constraints.gatekeeper.sh/v1beta1
+kind: ContainersResourceConstraints
+metadata:
+  name: containersresourceconstraints
+spec:
+  match:
+    kinds:
+    - apiGroups:
+      - apps
+      - ""
+      kinds:
+      - DaemonSet
+      - Deployment
+      - StatefulSet
+      - Pod
diff --git a/test/create/template_ContainersLatestTag.yaml b/test/create/template_ContainersLatestTag.yaml
new file mode 100755
index 0000000..d34e682
--- /dev/null
+++ b/test/create/template_ContainersLatestTag.yaml
@@ -0,0 +1,298 @@
+apiVersion: templates.gatekeeper.sh/v1beta1
+kind: ConstraintTemplate
+metadata:
+  creationTimestamp: null
+  name: containerslatesttag
+spec:
+  crd:
+    spec:
+      names:
+        kind: ContainersLatestTag
+  targets:
+  - libs:
+    - |
+      package lib.k8s
+
+      default is_gatekeeper = false
+
+      is_gatekeeper {
+        has_field(input, "review")
+        has_field(input.review, "object")
+      }
+
+      object = input {
+        not is_gatekeeper
+      }
+
+      object = input.review.object {
+        is_gatekeeper
+      }
+
+      format(msg) = gatekeeper_format {
+        is_gatekeeper
+        gatekeeper_format = {"msg": msg}
+      }
+
+      format(msg) = msg {
+        not is_gatekeeper
+      }
+
+      name = object.metadata.name
+
+      kind = object.kind
+
+      has_field(obj, field) {
+        obj[field]
+      }
+
+      missing_field(obj, field) = true {
+        obj[field] == ""
+      }
+
+      missing_field(obj, field) = true {
+        not has_field(obj, field)
+      }
+
+      is_service {
+        lower(kind) == "service"
+      }
+
+      is_statefulset {
+        lower(kind) == "statefulset"
+      }
+
+      is_daemonset {
+        lower(kind) == "daemonset"
+      }
+
+      is_deployment {
+        lower(kind) == "deployment"
+      }
+
+      is_pod {
+        lower(kind) == "pod"
+      }
+
+      is_namespace {
+        lower(kind) == "namespace"
+      }
+
+      is_workload {
+        containers[_]
+      }
+
+      pod_containers(pod) = all_containers {
+        keys = {"containers", "initContainers"}
+        all_containers = [c | keys[k]; c = pod.spec[k][_]]
+      }
+
+      containers[container] {
+        pods[pod]
+        all_containers = pod_containers(pod)
+        container = all_containers[_]
+      }
+
+      containers[container] {
+        all_containers = pod_containers(object)
+        container = all_containers[_]
+      }
+
+      container_images[image] {
+        containers[container]
+        image = container.image
+      }
+
+      container_images[image] {
+        image = object.spec.image
+      }
+
+      split_image(image) = [image, "latest"] {
+        not contains(image, ":")
+      }
+
+      split_image(image) = [image_name, tag] {
+        [image_name, tag] = split(image, ":")
+      }
+
+      pods[pod] {
+        is_statefulset
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_daemonset
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_deployment
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_pod
+        pod = object
+      }
+
+      volumes[volume] {
+        pods[pod]
+        volume = pod.spec.volumes[_]
+      }
+
+      mem_multiple("E") = 1000000000000000000000 { true }
+
+      mem_multiple("P") = 1000000000000000000 { true }
+
+      mem_multiple("T") = 1000000000000000 { true }
+
+      mem_multiple("G") = 1000000000000 { true }
+
+      mem_multiple("M") = 1000000000 { true }
+
+      mem_multiple("k") = 1000000 { true }
+
+      mem_multiple("") = 1000 { true }
+
+      mem_multiple("m") = 1 { true }
+
+      mem_multiple("Ki") = 1024000 { true }
+
+      mem_multiple("Mi") = 1048576000 { true }
+
+      mem_multiple("Gi") = 1073741824000 { true }
+
+      mem_multiple("Ti") = 1099511627776000 { true }
+
+      mem_multiple("Pi") = 1125899906842624000 { true }
+
+      mem_multiple("Ei") = 1152921504606846976000 { true }
+
+      get_suffix(mem) = suffix {
+        not is_string(mem)
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 0
+        suffix := substring(mem, count(mem) - 1, -1)
+        mem_multiple(suffix)
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 1
+        suffix := substring(mem, count(mem) - 2, -1)
+        mem_multiple(suffix)
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 1
+        not mem_multiple(substring(mem, count(mem) - 1, -1))
+        not mem_multiple(substring(mem, count(mem) - 2, -1))
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) == 1
+        not mem_multiple(substring(mem, count(mem) - 1, -1))
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) == 0
+        suffix := ""
+      }
+
+      canonify_mem(orig) = new {
+        is_number(orig)
+        new := orig * 1000
+      }
+
+      canonify_mem(orig) = new {
+        not is_number(orig)
+        suffix := get_suffix(orig)
+        raw := replace(orig, suffix, "")
+        re_match("^[0-9]+$", raw)
+        new := to_number(raw) * mem_multiple(suffix)
+      }
+
+      canonify_storage(orig) = new {
+        is_number(orig)
+        new := orig
+      }
+
+      canonify_storage(orig) = new {
+        not is_number(orig)
+        suffix := get_suffix(orig)
+        raw := replace(orig, suffix, "")
+        re_match("^[0-9]+$", raw)
+        new := to_number(raw) * mem_multiple(suffix)
+      }
+
+      canonify_cpu(orig) = new {
+        is_number(orig)
+        new := orig * 1000
+      }
+
+      canonify_cpu(orig) = new {
+        not is_number(orig)
+        endswith(orig, "m")
+        new := to_number(replace(orig, "m", ""))
+      }
+
+      canonify_cpu(orig) = new {
+        not is_number(orig)
+        not endswith(orig, "m")
+        re_match("^[0-9]+$", orig)
+        new := to_number(orig) * 1000
+      }
+
+      dropped_capability(container, cap) {
+        container.securityContext.capabilities.drop[_] == cap
+      }
+
+      added_capability(container, cap) {
+        container.securityContext.capabilities.add[_] == cap
+      }
+
+      no_read_only_filesystem(c) {
+        not has_field(c, "securityContext")
+      }
+
+      no_read_only_filesystem(c) {
+        has_field(c, "securityContext")
+        not has_field(c.securityContext, "readOnlyRootFilesystem")
+      }
+
+      priviledge_escalation_allowed(c) {
+        not has_field(c, "securityContext")
+      }
+
+      priviledge_escalation_allowed(c) {
+        has_field(c, "securityContext")
+        has_field(c.securityContext, "allowPrivilegeEscalation")
+      }
+    rego: |
+      package main
+
+      import data.lib.k8s
+
+      violation[msg] {
+        has_latest_tag
+
+        msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
+      }
+
+      has_latest_tag {
+        endswith(k8s.container_images[_], ":latest")
+      }
+
+      has_latest_tag {
+        contains(k8s.container_images[_], ":") == false
+      }
+    target: admission.k8s.gatekeeper.sh
+status: {}
diff --git a/test/create/template_ContainersResourceConstraints.yaml b/test/create/template_ContainersResourceConstraints.yaml
new file mode 100755
index 0000000..3052d37
--- /dev/null
+++ b/test/create/template_ContainersResourceConstraints.yaml
@@ -0,0 +1,302 @@
+apiVersion: templates.gatekeeper.sh/v1beta1
+kind: ConstraintTemplate
+metadata:
+  creationTimestamp: null
+  name: containersresourceconstraints
+spec:
+  crd:
+    spec:
+      names:
+        kind: ContainersResourceConstraints
+  targets:
+  - libs:
+    - |
+      package lib.k8s
+
+      default is_gatekeeper = false
+
+      is_gatekeeper {
+        has_field(input, "review")
+        has_field(input.review, "object")
+      }
+
+      object = input {
+        not is_gatekeeper
+      }
+
+      object = input.review.object {
+        is_gatekeeper
+      }
+
+      format(msg) = gatekeeper_format {
+        is_gatekeeper
+        gatekeeper_format = {"msg": msg}
+      }
+
+      format(msg) = msg {
+        not is_gatekeeper
+      }
+
+      name = object.metadata.name
+
+      kind = object.kind
+
+      has_field(obj, field) {
+        obj[field]
+      }
+
+      missing_field(obj, field) = true {
+        obj[field] == ""
+      }
+
+      missing_field(obj, field) = true {
+        not has_field(obj, field)
+      }
+
+      is_service {
+        lower(kind) == "service"
+      }
+
+      is_statefulset {
+        lower(kind) == "statefulset"
+      }
+
+      is_daemonset {
+        lower(kind) == "daemonset"
+      }
+
+      is_deployment {
+        lower(kind) == "deployment"
+      }
+
+      is_pod {
+        lower(kind) == "pod"
+      }
+
+      is_namespace {
+        lower(kind) == "namespace"
+      }
+
+      is_workload {
+        containers[_]
+      }
+
+      pod_containers(pod) = all_containers {
+        keys = {"containers", "initContainers"}
+        all_containers = [c | keys[k]; c = pod.spec[k][_]]
+      }
+
+      containers[container] {
+        pods[pod]
+        all_containers = pod_containers(pod)
+        container = all_containers[_]
+      }
+
+      containers[container] {
+        all_containers = pod_containers(object)
+        container = all_containers[_]
+      }
+
+      container_images[image] {
+        containers[container]
+        image = container.image
+      }
+
+      container_images[image] {
+        image = object.spec.image
+      }
+
+      split_image(image) = [image, "latest"] {
+        not contains(image, ":")
+      }
+
+      split_image(image) = [image_name, tag] {
+        [image_name, tag] = split(image, ":")
+      }
+
+      pods[pod] {
+        is_statefulset
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_daemonset
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_deployment
+        pod = object.spec.template
+      }
+
+      pods[pod] {
+        is_pod
+        pod = object
+      }
+
+      volumes[volume] {
+        pods[pod]
+        volume = pod.spec.volumes[_]
+      }
+
+      mem_multiple("E") = 1000000000000000000000 { true }
+
+      mem_multiple("P") = 1000000000000000000 { true }
+
+      mem_multiple("T") = 1000000000000000 { true }
+
+      mem_multiple("G") = 1000000000000 { true }
+
+      mem_multiple("M") = 1000000000 { true }
+
+      mem_multiple("k") = 1000000 { true }
+
+      mem_multiple("") = 1000 { true }
+
+      mem_multiple("m") = 1 { true }
+
+      mem_multiple("Ki") = 1024000 { true }
+
+      mem_multiple("Mi") = 1048576000 { true }
+
+      mem_multiple("Gi") = 1073741824000 { true }
+
+      mem_multiple("Ti") = 1099511627776000 { true }
+
+      mem_multiple("Pi") = 1125899906842624000 { true }
+
+      mem_multiple("Ei") = 1152921504606846976000 { true }
+
+      get_suffix(mem) = suffix {
+        not is_string(mem)
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 0
+        suffix := substring(mem, count(mem) - 1, -1)
+        mem_multiple(suffix)
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 1
+        suffix := substring(mem, count(mem) - 2, -1)
+        mem_multiple(suffix)
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) > 1
+        not mem_multiple(substring(mem, count(mem) - 1, -1))
+        not mem_multiple(substring(mem, count(mem) - 2, -1))
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) == 1
+        not mem_multiple(substring(mem, count(mem) - 1, -1))
+        suffix := ""
+      }
+
+      get_suffix(mem) = suffix {
+        is_string(mem)
+        count(mem) == 0
+        suffix := ""
+      }
+
+      canonify_mem(orig) = new {
+        is_number(orig)
+        new := orig * 1000
+      }
+
+      canonify_mem(orig) = new {
+        not is_number(orig)
+        suffix := get_suffix(orig)
+        raw := replace(orig, suffix, "")
+        re_match("^[0-9]+$", raw)
+        new := to_number(raw) * mem_multiple(suffix)
+      }
+
+      canonify_storage(orig) = new {
+        is_number(orig)
+        new := orig
+      }
+
+      canonify_storage(orig) = new {
+        not is_number(orig)
+        suffix := get_suffix(orig)
+        raw := replace(orig, suffix, "")
+        re_match("^[0-9]+$", raw)
+        new := to_number(raw) * mem_multiple(suffix)
+      }
+
+      canonify_cpu(orig) = new {
+        is_number(orig)
+        new := orig * 1000
+      }
+
+      canonify_cpu(orig) = new {
+        not is_number(orig)
+        endswith(orig, "m")
+        new := to_number(replace(orig, "m", ""))
+      }
+
+      canonify_cpu(orig) = new {
+        not is_number(orig)
+        not endswith(orig, "m")
+        re_match("^[0-9]+$", orig)
+        new := to_number(orig) * 1000
+      }
+
+      dropped_capability(container, cap) {
+        container.securityContext.capabilities.drop[_] == cap
+      }
+
+      added_capability(container, cap) {
+        container.securityContext.capabilities.add[_] == cap
+      }
+
+      no_read_only_filesystem(c) {
+        not has_field(c, "securityContext")
+      }
+
+      no_read_only_filesystem(c) {
+        has_field(c, "securityContext")
+        not has_field(c.securityContext, "readOnlyRootFilesystem")
+      }
+
+      priviledge_escalation_allowed(c) {
+        not has_field(c, "securityContext")
+      }
+
+      priviledge_escalation_allowed(c) {
+        has_field(c, "securityContext")
+        has_field(c.securityContext, "allowPrivilegeEscalation")
+      }
+    rego: |-
+      package policy
+
+      import data.lib.k8s
+
+      violation[msg] {
+        containers_resource_constraints_required
+
+        msg := k8s.format(sprintf("(%s) %s: Container resource constraints must be specified", [k8s.kind, k8s.name]))
+      }
+
+      containers_resource_constraints_required {
+        k8s.is_workload
+        not container_resources_provided
+      }
+
+      container_resources_provided {
+        k8s.containers[_].resources.requests.cpu
+        k8s.containers[_].resources.requests.memory
+        k8s.containers[_].resources.limits.cpu
+        k8s.containers[_].resources.limits.memory
+      }
+    target: admission.k8s.gatekeeper.sh
+status: {}
diff --git a/test/doc/expected.md b/test/doc/expected.md
index 967df2e..8b841ea 100644
--- a/test/doc/expected.md
+++ b/test/doc/expected.md
@@ -1,6 +1,110 @@
 # Policies
 
-|Name|Rule Types|API Groups|Kinds|Description|
-|---|---|---|---|---|
-|[Container Images](../../examples/container-images)|violation|apps, core|DaemonSet, Deployment, StatefulSet, Pod|Container images can not use the latest tag.|
-|[Containers Resource Constraints Required](../../examples/containers-resource-constraints-required)|violation|apps, core|DaemonSet, Deployment, StatefulSet, Pod|Containers must have resource constraints specified.|
+## Violations
+
+* [Images must not use the latest tag](#Images-must-not-use-the-latest-tag)
+* [Containers must define resource constraints](#Containers-must-define-resource-constraints)
+
+## Warnings
+
+* [Deprecated Deployment and DaemonSet API](#Deprecated-Deployment-and-DaemonSet-API)
+
+## Images must not use the latest tag
+
+**Severity:** violation
+
+**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+
+
+Using the latest tag on images can cause unexpected problems in production. By specifing a pinned version
+we can have higher confidence that our applications are immutable and do not change unexpectedly.
+
+### Rego
+
+```rego
+package main
+
+import data.lib.k8s
+
+violation[msg] {
+  has_latest_tag
+
+  msg := k8s.format(sprintf("(%s) %s: Images must not use the latest tag", [k8s.kind, k8s.name]))
+}
+
+has_latest_tag {
+  endswith(k8s.container_images[_], ":latest")
+}
+
+has_latest_tag {
+  contains(k8s.container_images[_], ":") == false
+}
+
+```
+_source: [../../examples/containers-latest-tag](../../examples/containers-latest-tag)_
+
+## Containers must define resource constraints
+
+**Severity:** violation
+
+**Resources:** apps/DaemonSet apps/Deployment apps/StatefulSet core/Pod
+
+
+Resource constraints on containers ensure that a given workload does not take up more resources than it required
+and potentially starve other applications that need to run.
+
+### Rego
+
+```rego
+package policy
+
+import data.lib.k8s
+
+violation[msg] {
+  containers_resource_constraints_required
+
+  msg := k8s.format(sprintf("(%s) %s: Container resource constraints must be specified", [k8s.kind, k8s.name]))
+}
+
+containers_resource_constraints_required {
+  k8s.is_workload
+  not container_resources_provided
+}
+
+container_resources_provided {
+  k8s.containers[_].resources.requests.cpu
+  k8s.containers[_].resources.requests.memory
+  k8s.containers[_].resources.limits.cpu
+  k8s.containers[_].resources.limits.memory
+}
+```
+_source: [../../examples/containers-resource-constraints](../../examples/containers-resource-constraints)_
+
+## Deprecated Deployment and DaemonSet API
+
+**Severity:** warn
+
+**Resources:** apps/DaemonSet apps/Deployment
+
+
+The `extensions/v1beta1 API` has been deprecated in favor of `apps/v1`. Later versions of Kubernetes
+remove this API so to ensure that the Deployment or DaemonSet can be successfully deployed to the cluster,
+the version for both of these resources must be `apps/v1`.
+
+### Rego
+
+```rego
+package main
+
+import data.lib.k8s
+
+warn[msg] {
+  resources := ["DaemonSet", "Deployment"]
+  input.apiVersion == "extensions/v1beta1"
+  input.kind == resources[_]
+
+  msg := k8s.format(sprintf("%s/%s: API extensions/v1beta1 for %s has been deprecated, use apps/v1 instead.", [input.kind, input.metadata.name, input.kind]))
+}
+
+```
+_source: [../../examples/warnings](../../examples/warnings)_