Skip to content

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.

Copies the team label from the namespace to all workloads as shoehorn.dev/team:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
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}}"

Assigns tier based on which namespace the workload is in:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: shoehorn-default-tier
spec:
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"

Infers lifecycle from namespace naming convention:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: shoehorn-lifecycle
spec:
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/v1
kind: ClusterPolicy
metadata:
name: shoehorn-entity-file
spec:
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/v1
kind: ClusterPolicy
metadata:
name: shoehorn-infer-description
spec:
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}}"

Generate tags from common labels so entities are searchable without manual tagging:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: shoehorn-auto-tags
spec:
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}}"
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/v1
kind: ClusterPolicy
metadata:
name: shoehorn-grafana-link
spec:
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/v1
kind: ClusterPolicy
metadata:
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 preconditions to skip workloads that already have the annotation set. Explicit annotations always win.
  • Kyverno applies mutations at admission time. Existing workloads need a kubectl rollout restart or policy apply to pick up changes.
  • Test policies in Audit mode before switching to Enforce.
  • 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.