k8wiz
Back to Articles

Ready to test your Kyverno knowledge? Try our Kyverno quiz!

How to test Kyverno policies in CI with the Kyverno CLI

Write kyverno-test.yaml files for a CEL ValidatingPolicy, run them with kyverno test, and make a CI job fail when a policy change breaks expected results.

7 min read
By k8wiz Team
  • kyverno
  • testing
  • ci
  • policy-as-code

A Kyverno policy is code that runs in your admission path. A typo in an expression can block every Deployment in the cluster, or quietly let through the thing it was written to stop. Yet many teams still test policies by applying them to a staging cluster and watching what happens.

The Kyverno CLI can do better without any cluster. kyverno test applies policies to sample resources on your machine and compares the outcome with results you declare up front. This guide sets up tests for one CEL-based ValidatingPolicy, runs them, and wires them into CI. Field names and flags come from the Kyverno 1.19 CLI.

Step 1: install the CLI

The CLI is a single binary called kyverno. Each Kyverno release on GitHub publishes it as an archive per platform. Pin the same version your cluster runs, so tests see the same policy engine as admission does.

VERSION=v1.19.2
curl -sSLO "https://github.com/kyverno/kyverno/releases/download/${VERSION}/kyverno-cli_${VERSION}_linux_x86_64.tar.gz"
tar -xzf "kyverno-cli_${VERSION}_linux_x86_64.tar.gz" kyverno
sudo install kyverno /usr/local/bin/kyverno
kyverno version

On macOS the archive name ends in darwin_arm64 or darwin_x86_64 instead.

Step 2: lay out one directory per policy

kyverno test takes a directory and walks it recursively, looking for files named kyverno-test.yaml. Each test file points at policies and resources by path, relative to its own directory. Keeping each policy next to its test cases makes the paths short and the reviews easy.

policies/
  require-team-label/
    policy.yaml
    resources.yaml
    kyverno-test.yaml
  disallow-latest-tag/
    policy.yaml
    resources.yaml
    kyverno-test.yaml

Step 3: write the policy

The example policy requires a team label on every Deployment. It uses ValidatingPolicy, the CEL-based policy type Kyverno introduced alongside the older ClusterPolicy. The policy denies requests that fail the check.

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-team-label
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
      message: "Deployments must have a team label."

The has() check matters. A Deployment without any labels has no metadata.labels map, and the expression would fail with an error instead of returning false. Testing exactly that case is one of the reasons to have tests.

Step 4: write the sample resources

Put one resource per case you care about in resources.yaml. Give each a name that says what it tests, because the test file refers to resources by name and the output prints them.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: labeled
  namespace: shop
  labels:
    team: payments
spec:
  replicas: 1
  selector:
    matchLabels:
      app: labeled
  template:
    metadata:
      labels:
        app: labeled
    spec:
      containers:
        - name: app
          image: nginx:1.27
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: no-labels
  namespace: shop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: no-labels
  template:
    metadata:
      labels:
        app: no-labels
    spec:
      containers:
        - name: app
          image: nginx:1.27
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wrong-label
  namespace: shop
  labels:
    owner: payments
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wrong-label
  template:
    metadata:
      labels:
        app: wrong-label
    spec:
      containers:
        - name: app
          image: nginx:1.27

Three cases cover the policy. One passes, one has no labels at all and one has labels but not the right one. When the policy changes later, add a resource for the new edge case before you change the expression.

Step 5: declare the expected results

The test file is a Kubernetes-style manifest with its own API version. It lists the policies, the resources and what should happen to each resource.

apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
  name: require-team-label
policies:
  - policy.yaml
resources:
  - resources.yaml
results:
  - policy: require-team-label
    isValidatingPolicy: true
    kind: Deployment
    resources:
      - labeled
    result: pass
  - policy: require-team-label
    isValidatingPolicy: true
    kind: Deployment
    resources:
      - no-labels
      - wrong-label
    result: fail

Each entry under results names a policy, the kind and names of the resources it covers, and the expected result. For a policy that should block a resource, the expected result is fail. The other values you'll meet are pass, skip, warn and error.

isValidatingPolicy: true tells the CLI that the policy is a ValidatingPolicy. There are matching flags for the other new types, such as isMutatingPolicy and isImageValidatingPolicy. For a classic ClusterPolicy, leave the flag out and add rule: with the rule name instead, since a ClusterPolicy can hold several rules.

Write apiVersion, kind and metadata.name exactly as shown. The CLI rejects test files without apiVersion and kind, or with the old top-level name field, with an error that the file uses a deprecated schema. Older examples on the internet still use that format.

Step 6: run the tests

Run the command from the repository root and point it at the policies directory.

kyverno test policies/

For each test file, the CLI prints which file it loaded, applies the policies and prints a table with the policy, the resource, the result and a reason. It ends with a summary line.

Test Summary: 3 tests passed and 0 tests failed

To see the failure path, change the expected result for labeled to fail and run again. The table marks that row as failed, the summary counts it, and the command exits with a non-zero status. CI relies on that exit status.

While you work on one policy, run only its tests with the selector flag. The default selector is policy=*,rule=*,resource=*.

kyverno test policies/ --test-case-selector "policy=require-team-label"
kyverno test policies/ --detailed-results

--detailed-results adds the policy's message to each row, which helps when a resource fails for a different reason than you expected.

Step 7: check a single resource quickly

kyverno test is for cases you want to keep. When you just want to know what a policy thinks of a manifest, kyverno apply is quicker and needs no test file.

kyverno apply policies/require-team-label/policy.yaml --resource deploy/api.yaml

It prints the violations it finds. Once you know how the manifest should be treated, add it to resources.yaml with the expected result, so the behavior is locked in.

Step 8: run the tests in CI

The CI job needs the CLI and one command. Here is a GitLab CI job that installs the pinned version and runs every test.

kyverno-tests:
  stage: test
  image: debian:bookworm-slim
  variables:
    KYVERNO_VERSION: v1.19.2
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl -sSLO "https://github.com/kyverno/kyverno/releases/download/${KYVERNO_VERSION}/kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz"
    - tar -xzf "kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz" kyverno
    - install kyverno /usr/local/bin/kyverno
  script:
    - kyverno test policies/ --require-tests --remove-color
  rules:
    - changes:
        - policies/**/*

--require-tests closes a quiet gap. Without it, a run that finds no kyverno-test.yaml files prints "No test yamls available" and still succeeds. A renamed directory or a wrong path would then turn the whole job into a no-op that always passes. With the flag, finding no tests is an error.

--remove-color drops the terminal color codes, which keeps CI logs readable. The CLI also offers --warnings-as-errors, which fails the run on deprecation warnings, so policies don't drift onto fields Kyverno plans to remove.

What to test

Aim for at least one passing and one failing resource per validation, plus the edge cases that broke things before. Resources with no labels, empty lists and missing optional fields catch most expression bugs, because CEL raises an error when you read a field that isn't there.

If you use policy exceptions, test them too. A test file can list exception manifests under exceptions:, next to policies: and resources:, and the expected result for the exempted resource then becomes skip. That proves the exception matches only what it should.

Treat the test files as part of the policy. A pull request that changes a policy without changing or adding a test case deserves a question in review, the same as application code without tests.

Test yourself

Ready to test your Kyverno knowledge? Try our Kyverno quiz!