Skip to content

Repository files navigation

vigie

License

Status: template, validate, and the envtest + kubeconfig apply tiers shipped. vigie lint, vigie test (render + assertions, --cluster envtest to install each test against a real API server, and --cluster kubeconfig to run integration tests — dependencies, lifecycle hooks, and live-cluster matchers — against a cluster you already have), vigie validate (kubeconform), and vigie schema work end to end. Self-provisioned cluster backends (kind/k3d, simulated) are on the roadmap.

A single CLI and declarative YAML DSL for testing Helm charts across progressively higher-fidelity tiers — from fast in-process template rendering to full end-to-end cluster tests. Helm is used as a library, never shelled out.


Commands and tiers

Three chart commands, plus schema/version utilities:

Command What it proves Status
vigie lint Chart hygiene: Chart.yaml, best-practices, deprecations ✅ shipped
vigie validate Rendered YAML is structurally valid Kubernetes (kubeconform) ✅ shipped
vigie test Assertions over rendered manifests, optionally against a live cluster ✅ shipped

lint and validate are chart-level (no test files). test is the fidelity dial: it renders in-process by default, and --cluster raises it to install each test against a real control plane and assert on the live objects.

vigie test --cluster … Backend What it adds Status
none (default) helm template in-process Template logic produces the expected YAML ✅ shipped
envtest real kube-apiserver + etcd, no controllers The API server accepts the resources ✅ shipped
kubeconfig a cluster you already have Integration tests (deps, hooks, live matchers) against a real cluster you provide ✅ shipped
simulated envtest + controllers + kwok Controllers reconcile, Pods start TODO
kind / k3d provisioned throwaway node cluster Workloads run, real network and probes TODO

Installation

Build from source with Nix (Go 1.26):

git clone https://github.com/fregateops/vigie
cd vigie
nix develop --command make build   # -> dist/vigie
./dist/vigie version

Or with a local Go 1.26+ toolchain:

go build -o vigie ./cmd/vigie

Quick start

Drop test files under tests/unit/ in your chart:

# mychart/tests/unit/deployment_test.yaml
suite: deployment
templates:
  - templates/deployment.yaml

tests:
  - it: renders a Deployment with the configured replica count
    inputs:
      set:
        replicaCount: 3
    asserts:
      - isKind: Deployment
      - isAPIVersion: apps/v1
      - equal:
          path: spec.replicas
          value: 3

  - it: image tag is overridable
    inputs:
      set:
        image.tag: v2.0
    asserts:
      - matchRegex:
          path: spec.template.spec.containers[0].image
          pattern: ':v2\.0$'

Run them:

vigie test ./mychart
deployment  (9ms)
────────────────────────────────────────────────────────────
  PASS  renders a Deployment with the configured replica count  (1ms)
  PASS  image tag is overridable  (1ms)

────────────────────────────────────────────────────────────
Tests: 2 total, 2 passed (2ms total test time)

A complete, realistic example chart lives in testdata/charts/basic — its tests/unit/ suite exercises the full matcher library, matrix/cases, helper (call:) tests, and snapshots.


Validate

vigie validate is a chart-level smoke check that needs no test files. It renders the chart with the baseline values.yaml plus any overlays you pass (helm -f semantics), then runs kubeconform over the rendered manifests to prove they are structurally valid Kubernetes for a given API version. Each (overlay × kubeVersion) pair runs as an independent scenario.

# Baseline render against the default Kubernetes version (1.36.1)
vigie validate ./mychart

# Layer production values and validate across two Kubernetes versions
vigie validate ./mychart --values values-prod.yaml --kube-version 1.33.0,1.36.1
validate: values.yaml  (140ms)
────────────────────────────────────────────────────────────
  PASS  k8s 1.36.1  (140ms)

────────────────────────────────────────────────────────────
Tests: 1 total, 1 passed (140ms total test time)

Overlays, kube versions, --set* overrides, and per-finding ignore rules can also be set under a validate: block in .vigie.yaml so CI and local runs stay consistent.


Test file structure

suite: <name>            # human-readable suite name
templates:               # limit rendering to these templates (optional)
  - templates/deployment.yaml

tests:
  - it: <description>
    skip: false          # true, or a non-empty string reason, skips the test
    inputs:              # helm rendering inputs (required nesting level)
      set:               # --set overrides (dot notation supported)
        image.tag: v2.0
      release:
        name: my-release
        namespace: my-ns
      capabilities:
        kubeVersion: "1.30"
    target:              # which rendered document to assert on (optional)
      kind: Deployment
      name: my-app
    asserts:
      - <matcher>: ...

Two ways to parameterize a test:

  - it: works across replica counts
    matrix:
      replicaCount: [1, 3]
    inputs:
      set:
        replicaCount: ${{ matrix.replicaCount }}
    asserts:
      - equal:
          path: spec.replicas
          value: ${{ matrix.replicaCount }}

  - it: builds the image reference per tag
    cases:
      - name: pinned
        set:
          image.tag: "2.0.0"
        want: "myrepo/app:2.0.0"
    asserts:
      - equal:
          path: spec.template.spec.containers[0].image
          value: ${{ case.want }}

Use block style (as above) rather than YAML flow mappings for assertions: an unquoted index like containers[0] inside a { … } flow mapping is parsed by YAML as a nested sequence and fails to load.

${{ ... }} interpolates a CEL expression against the current matrix/case bindings.

Matchers

Matcher Description
equal / notEqual: { path, value } Deep (in)equality at path
greaterThan / lessThan / gte / lte: { path, value } Numeric comparison
contains / notContains: { path, content } Substring or list membership
startsWith / endsWith: { path, value } String prefix/suffix
matchRegex / notMatchRegex: { path, pattern } RE2 regex
matchTemplate: { path, pattern } ${VAR}-placeholder template match
exists / notExists: { path } Path existence
isNull / isNotNull: { path } Null check
isEmpty / isNotEmpty: { path } Empty string/list/map
isKind: <string> kind field equals value
isAPIVersion: <string> apiVersion field equals value
isType: { path, of } Type check: string, int, float, bool, list, map
lengthEqual: { path, value } Collection length equals N
isSubset: { path, content } Object contains all keys/values from content
matchSchema: { path, schema } Value validates against an inline JSON Schema fragment
hasDocuments: <int> Number of rendered documents equals N
failedTemplate: { errorPattern } Render failed, optionally matching a regex
matchSnapshot: { path } Value matches on-disk snapshot (auto-created on first run)
allOf / anyOf: [...] All / at least one nested assertion must pass
expr: <CEL> CEL expression over doc/resources/matrix/case evaluates to true

Any matcher accepts not: true to invert the result.

Path syntax is dotted with integer bracket indexing, e.g. spec.template.spec.containers[0].image. Map keys that contain dots or slashes (such as the app.kubernetes.io/name label) use a quoted bracket segment: metadata.labels["app.kubernetes.io/name"] (single or double quotes). The same keys are also reachable from expr: via CEL, e.g. doc.metadata.labels["app.kubernetes.io/name"].

Selecting a document

When a test renders multiple documents, pin one with target::

target:
  kind: Deployment       # by kind
  name: my-app           # by metadata.name
  documentIndex: 0       # by position in render output (0-based)
  expr: 'doc.kind == "Service"'   # or a CEL predicate

Override per assertion with on: when a single test checks multiple documents:

asserts:
  - isKind: Deployment
    on: { kind: Deployment }
  - isKind: Service
    on: { kind: Service }

Set forEach: true on a test to run every assertion against all documents matching target.

Helper (call:) tests

Test a named template directly, without rendering a whole chart:

suite: helpers
helpers:
  - templates/_helpers.tpl
tests:
  - it: builds an image reference
    call: mychart.image
    args:
      repository: myrepo/app
      tag: "1.2.3"
    outputAs: string        # string | yaml | json | bool
    asserts:
      - equal: { value: "myrepo/app:1.2.3" }

Editor autocomplete

vigie schema prints the test-file JSON Schema. Reference it from a test file with a yaml-language-server modeline for completion and validation as you type — either the hosted schema:

# yaml-language-server: $schema=https://raw.githubusercontent.com/fregateops/vigie/refs/heads/main/pkg/api/schema/v1/testfile.json

or a local copy for offline/pinned use:

vigie schema > .vigie.schema.json
# then:  # yaml-language-server: $schema=./.vigie.schema.json

CLI reference

vigie version                 print version information

vigie lint [chart]            static analysis: chart-yaml, best-practices, deprecations
  --rule-sets <a,b>           run only these rule sets (default: all)
  --disable-rules <a,b>       skip specific rule IDs (added to config)
  --kube-version <ver>        target Kubernetes API version for deprecation checks

vigie test [chart]            render + assert; --cluster raises fidelity to a live backend
  --cluster <backend>         none|envtest|kubeconfig (default: none)
  --file <path>               run a single test file instead of discovering all
  --tests <dir>               discovery root (default: <chart>/tests)
  --match <regex>             run only tests whose display name matches
  -u, --update-snapshots      update snapshots on mismatch instead of failing
  --fail-fast                 cancel queued tests after the first failure
  --schema                    run the per-test kubeconform pass (default: true; --schema=false skips)
  --kube-version <a,b,…>      Kubernetes version(s): template matrixes all, cluster uses the first
  --snapshot-dir <dir>        snapshot directory (default: <chart>/tests/snapshots)
  --kubeconfig <path>         kubeconfig for --cluster kubeconfig
  --keep-cluster              keep the cluster after the run (node-backed backends)
  --pass-on-warning           exit 0 on run warnings, e.g. no tests executed (default: exit 5)
  -p, --parallelism <n>       parallel test files (default: CPU count)

vigie validate [chart]        chart tier: render values.yaml + overlays, validate with kubeconform
  --values <a.yaml,b.yaml>    value overlays (helm -f); each runs as an independent scenario
  --kube-version <a,b>        Kubernetes versions to validate against (default: 1.36.1)
  --set / --set-json / --set-literal <k=v>   value overrides (helm semantics)
  -p, --parallelism <n>       parallel scenarios (default: CPU count)

vigie schema                  print the test-file JSON Schema

Chart commands default [chart] to the current directory, so vigie test works from inside a chart. Global flags: -o, --output pretty|json|junit|sarif|tap · -v debug / -vv trace. -p, --parallelism lives on test and validate (the commands that parallelize), not globally.

Exit codes: 0 pass · 1 test failure · 2 setup error · 3 user error · 4 infra error · 5 warnings (e.g. no tests executed; suppress with --pass-on-warning).


Configuration

Place a .vigie.yaml at the chart root to set defaults. A fully-commented reference is in examples/.vigie.yaml; a working example ships in testdata/charts/basic/.vigie.yaml.

defaults:
  release:
    name: release-name
    namespace: default

lint:
  ruleSets: [chart-yaml, template-best-practices, deprecation]
  disableRules:
    - template-best-practices_missing-resource-limits

validate:
  valuesFiles: [values-prod.yaml]   # overlays to render + validate
  kubeVersions: [1.36.1]
  ignore:
    - kind: Ingress                 # suppress a known kubeconform finding
      messageRegex: "networking.k8s.io/v1"

test:
  testsDir: tests/unit
  skipSchema: false          # kubeconform runs per test by default; true opts out
  kubeVersions: [1.36.1]     # kubeconform runs once per version (matrix)

Lint rule sets

Rule set Checks
helm-v3-lint Delegates to Helm v3's helm lint
chart-yaml Chart.yaml structure: apiVersion, name, version, description
template-best-practices Hardcoded namespaces, missing resource limits, …
deprecation Removed/deprecated Kubernetes APIs (+ operator-specific sets)

Rule IDs are namespaced as <ruleSet>_<id> (e.g. template-best-practices_hardcoded-namespace).


Development

Requires Nix.

nix develop                                  # enter the dev shell (Go, golangci-lint, pre-commit)
nix develop --command make build             # build ./dist/vigie
nix develop --command make test              # pre-commit + go test ./...
nix develop --command make run ARGS="test ./testdata/charts/basic"

make help lists every target.


Roadmap

Features land milestone by milestone; each is independently releasable.

Milestone Status Scope
M0 Toolchain bootstrap, vigie version, release pipeline
M1 vigie lint — chart-yaml, best-practices, deprecation rule sets
M2 DSL & JSON Schema foundation, vigie schema
M3 vigie test — template tier: full matcher library, matrix/cases, snapshots, helper tests
M4 vigie validate (kubeconform), JSON/SARIF/TAP reporters, CI annotations
M5 Distribution: Helm plugin, install scripts, pre-commit hook manifest
M6 vigie test --cluster envtest — apply tier against a real API server
M7 🔜 vigie test --cluster kubeconfig + integration format (deps, hooks, live matchers)
M8 🔜 vigie test --cluster kind|k3d — self-provisioned e2e clusters
M9 🔜 vigie test --cluster simulated — envtest + controllers + kwok
M10 🔜 vigie doctor, vigie run, docs site, watch/--changed

License

Apache 2.0

About

Vigie a DSL Helm Charts testing framework

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages