diff --git a/docs/constraint_creation.md b/docs/constraint_creation.md index 02032569..6a3aab15 100644 --- a/docs/constraint_creation.md +++ b/docs/constraint_creation.md @@ -150,3 +150,17 @@ missing_labels = missing { missing := required - provided } ``` +## Setting constraint metadata.annotations and metadata.labels + +You can optionally specify annotations and labels for the generated Constraint. This can be useful if you use Argo CD for deployment (see [here](https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#skip-dry-run-for-new-custom-resources-types)). + +``` +# METADATA +# title: Required Labels +# description: >- +# This policy allows you to require certain labels are set on a resource. +# custom: +# annotations: +# "argocd.argoproj.io/sync-options": "SkipDryRunOnMissingResource=true" +... +``` diff --git a/examples/container-deny-added-caps/constraint.yaml b/examples/container-deny-added-caps/constraint.yaml index 22c2e008..c523518a 100755 --- a/examples/container-deny-added-caps/constraint.yaml +++ b/examples/container-deny-added-caps/constraint.yaml @@ -1,6 +1,8 @@ apiVersion: constraints.gatekeeper.sh/v1beta1 kind: ContainerDenyAddedCaps metadata: + annotations: + argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true name: containerdenyaddedcaps spec: match: diff --git a/examples/container-deny-added-caps/src.rego b/examples/container-deny-added-caps/src.rego index 4c97f8af..21206499 100644 --- a/examples/container-deny-added-caps/src.rego +++ b/examples/container-deny-added-caps/src.rego @@ -5,6 +5,8 @@ # for containers to escalate their privileges. As such, this is not allowed # outside of Kubernetes controller namespaces. # custom: +# annotations: +# "argocd.argoproj.io/sync-options": "SkipDryRunOnMissingResource=true" # matchers: # kinds: # - apiGroups: diff --git a/internal/commands/create.go b/internal/commands/create.go index eaa3f619..4fff56ad 100644 --- a/internal/commands/create.go +++ b/internal/commands/create.go @@ -269,6 +269,14 @@ func getConstraint(violation rego.Rego, logger *log.Entry) (*unstructured.Unstru var constraint unstructured.Unstructured constraint.SetGroupVersionKind(gvk) constraint.SetName(violation.Name()) + annotations := violation.Annotations() + if annotations != nil { + constraint.SetAnnotations(annotations) + } + labels := violation.Labels() + if labels != nil { + constraint.SetLabels(labels) + } if violation.Enforcement() != "deny" { if err := unstructured.SetNestedField(constraint.Object, violation.Enforcement(), "spec", "enforcementAction"); err != nil { diff --git a/internal/rego/rego.go b/internal/rego/rego.go index f1f40164..000999f8 100644 --- a/internal/rego/rego.go +++ b/internal/rego/rego.go @@ -38,8 +38,15 @@ const ( annoParameters = "parameters" annoSkipTemplate = "skipTemplate" annoSkipConstraint = "skipConstraint" + annoAnnotations = "annotations" + annoLabels = "labels" ) +type MetaData struct { + Annotations map[string]string + Labels map[string]string +} + // Rego represents a parsed rego file. type Rego struct { id string @@ -53,7 +60,7 @@ type Rego struct { enforcement string skipTemplate bool skipConstraint bool - + metaData *MetaData // Duplicate data from OPA Metadata annotations. annotations *ast.Annotations annoTitle string @@ -241,9 +248,54 @@ func (r *Rego) parseAnnotations(annotations *ast.Annotations) error { r.enforcement = e } + metaAnnotations, ok := annotations.Custom[annoAnnotations] + if ok { + a, ok := metaAnnotations.(map[string]interface{}) + if !ok { + return fmt.Errorf("supplied annotations value is not a map[string]interface{}: %T", metaAnnotations) + } + if r.metaData == nil { + r.metaData = &MetaData{} + } + ans, err := switchToMap(a) + if err != nil { + return err + } + r.metaData.Annotations = ans + } + + metaLabels, ok := annotations.Custom[annoLabels] + if ok { + l, ok := metaLabels.(map[string]interface{}) + if !ok { + return fmt.Errorf("supplied labels value is not a map[string]interface{}: %T", metaLabels) + } + if r.metaData == nil { + r.metaData = &MetaData{} + } + labels, err := switchToMap(l) + if err != nil { + return err + } + r.metaData.Labels = labels + } + return nil } +func switchToMap(in map[string]interface{}) (map[string]string, error) { + out := map[string]string{} + for k, v := range in { + switch c := v.(type) { + case string: + out[k] = v.(string) + default: + return nil, fmt.Errorf("supplied value is not a string: %v", c) + } + } + return out, nil +} + func (r *Rego) parseAnnotationsMatchers(matchers map[string]any) error { kindMatchers, ok := matchers["kinds"] if ok { @@ -342,6 +394,22 @@ func (r Rego) Name() string { return strings.ToLower(r.Kind()) } +// Labels returns the labels found in the header comment of the rego file. +func (r Rego) Labels() map[string]string { + if r.metaData == nil { + return nil + } + return r.metaData.Labels +} + +// Annotations returns the annotations found in the header comment of the rego file. +func (r Rego) Annotations() map[string]string { + if r.metaData == nil { + return nil + } + return r.metaData.Annotations +} + // Title returns the title found in the header comment of the rego file. func (r Rego) Title() string { if r.annoTitle != "" { @@ -441,7 +509,7 @@ func (r Rego) Description() string { return description } -// HasMetadataAnnotations checks whenether rego file has +// HasMetadataAnnotations checks whether rego file has // OPA Metadata Annotations func (r Rego) HasMetadataAnnotations() bool { return r.annotations != nil