Kyverno Policy Examples
Ready-to-use Kyverno ClusterPolicies for propagating Shoehorn annotations to workloads automatically. Instead of adding annotations to every deployment, configure them once and let Kyverno handle the rest.
Propagate Team from Namespace
Section titled “Propagate Team from Namespace”Copies the team label from the namespace to all workloads as shoehorn.dev/team:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-propagate-team annotations: policies.kyverno.io/title: Propagate team ownership to Shoehorn policies.kyverno.io/description: >- Sets shoehorn.dev/team on workloads from the namespace's team label. Only applies if the workload doesn't already have the annotation.spec: rules: - name: propagate-team match: any: - resources: kinds: - Deployment - StatefulSet - DaemonSet - CronJob preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/team\" || ''}}" operator: Equals value: "" context: - name: nsTeam apiCall: urlPath: "/api/v1/namespaces/{{request.namespace}}" jmesPath: "metadata.labels.team || 'unassigned'" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/team: "{{nsTeam}}"Set Tier by Namespace
Section titled “Set Tier by Namespace”Assigns tier based on which namespace the workload is in:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-default-tierspec: rules: - name: production-gold match: any: - resources: kinds: ["Deployment", "StatefulSet"] namespaces: ["payments", "checkout", "auth", "core"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/tier\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/tier: "gold"
- name: staging-silver match: any: - resources: kinds: ["Deployment", "StatefulSet"] namespaces: ["staging-*"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/tier\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/tier: "silver"
- name: default-bronze match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/tier\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/tier: "bronze"Set Lifecycle from Namespace Pattern
Section titled “Set Lifecycle from Namespace Pattern”Infers lifecycle from namespace naming convention:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-lifecyclespec: rules: - name: production match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] namespaces: ["prod-*", "production-*", "payments", "checkout", "auth"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/lifecycle\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/lifecycle: "production"
- name: staging match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] namespaces: ["staging-*", "stg-*"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/lifecycle\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/lifecycle: "staging"
- name: development match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] namespaces: ["dev-*", "development-*", "sandbox-*"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/lifecycle\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/lifecycle: "development"Set entityFile for All Workloads in a Namespace
Section titled “Set entityFile for All Workloads in a Namespace”When all services in a namespace follow the same repo convention:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-entity-filespec: rules: - name: set-entity-file match: any: - resources: kinds: ["Deployment", "StatefulSet"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/entityFile\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"Infer Description from app.kubernetes.io Labels
Section titled “Infer Description from app.kubernetes.io Labels”Build a description from standard Kubernetes labels so entities are not blank in the catalog:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-infer-descriptionspec: rules: - name: build-description match: any: - resources: kinds: ["Deployment", "StatefulSet"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/description\" || ''}}" operator: Equals value: "" - key: "{{request.object.metadata.labels.\"app.kubernetes.io/name\" || ''}}" operator: NotEquals value: "" context: - name: appName variable: value: "{{request.object.metadata.labels.\"app.kubernetes.io/name\"}}" - name: component variable: value: "{{request.object.metadata.labels.\"app.kubernetes.io/component\" || ''}}" - name: partOf variable: value: "{{request.object.metadata.labels.\"app.kubernetes.io/part-of\" || ''}}" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/description: "{{appName}}{{#if component}} ({{component}}){{/if}}{{#if partOf}} - part of {{partOf}}{{/if}}"Auto-Tag from Kubernetes Labels
Section titled “Auto-Tag from Kubernetes Labels”Generate tags from common labels so entities are searchable without manual tagging:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-auto-tagsspec: rules: - name: tags-from-labels match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/tags\" || ''}}" operator: Equals value: "" context: - name: appComponent variable: value: "{{request.object.metadata.labels.\"app.kubernetes.io/component\" || ''}}" - name: appPartOf variable: value: "{{request.object.metadata.labels.\"app.kubernetes.io/part-of\" || ''}}" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/tags: "{{request.namespace}}{{#if appComponent}},{{appComponent}}{{/if}}{{#if appPartOf}},{{appPartOf}}{{/if}}"Add Grafana Link from Namespace Convention
Section titled “Add Grafana Link from Namespace Convention”If your Grafana dashboards follow a naming convention (/d/<namespace>-<app>), auto-generate the link:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-grafana-linkspec: rules: - name: add-grafana-link match: any: - resources: kinds: ["Deployment", "StatefulSet"] preconditions: all: - key: "{{request.object.metadata.annotations.\"link.shoehorn.dev/grafana-url\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: link.shoehorn.dev/grafana-url: "https://grafana.internal/d/{{request.namespace}}-{{request.object.metadata.name}}" link.shoehorn.dev/grafana-name: "Service Metrics"Replace https://grafana.internal with your Grafana URL.
Combined Policy: Full Enrichment from Namespace
Section titled “Combined Policy: Full Enrichment from Namespace”A single policy that sets team, tier, lifecycle, and entityFile based on namespace labels and conventions. This is the recommended starting point — one policy covers most use cases:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: shoehorn-full-enrichment annotations: policies.kyverno.io/title: Shoehorn full entity enrichment policies.kyverno.io/description: >- Sets team, tier, lifecycle, and entityFile on all workloads from namespace context. Skips workloads that already have annotations set.spec: rules: - name: team-from-namespace match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/team\" || ''}}" operator: Equals value: "" context: - name: nsTeam apiCall: urlPath: "/api/v1/namespaces/{{request.namespace}}" jmesPath: "metadata.labels.team || metadata.labels.\"shoehorn.dev/team\" || 'unassigned'" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/team: "{{nsTeam}}"
- name: entity-file-default match: any: - resources: kinds: ["Deployment", "StatefulSet"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/entityFile\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/entityFile: ".shoehorn/catalog.yaml"
- name: production-lifecycle match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] namespaces: ["prod-*", "production-*"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/lifecycle\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/lifecycle: "production"
- name: staging-lifecycle match: any: - resources: kinds: ["Deployment", "StatefulSet", "DaemonSet", "CronJob"] namespaces: ["staging-*", "stg-*"] preconditions: all: - key: "{{request.object.metadata.annotations.\"shoehorn.dev/lifecycle\" || ''}}" operator: Equals value: "" mutate: patchStrategicMerge: metadata: annotations: shoehorn.dev/lifecycle: "staging"- All policies use
preconditionsto skip workloads that already have the annotation set. Explicit annotations always win. - Kyverno applies mutations at admission time. Existing workloads need a
kubectl rollout restartor policy apply to pick up changes. - Test policies in
Auditmode before switching toEnforce. - The “Combined Policy” above is the recommended starting point. It covers the most common case: team from namespace, entityFile for full enrichment, and lifecycle from namespace naming convention.
- For more on what each annotation does, see the Annotations Reference and Entity Enrichment Modes.