Status: template, validate, and the envtest + kubeconfig apply tiers shipped.
vigie lint,vigie test(render + assertions,--cluster envtestto install each test against a real API server, and--cluster kubeconfigto run integration tests — dependencies, lifecycle hooks, and live-cluster matchers — against a cluster you already have),vigie validate(kubeconform), andvigie schemawork 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.
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 |
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 versionOr with a local Go 1.26+ toolchain:
go build -o vigie ./cmd/vigieDrop 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 ./mychartdeployment (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.
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.1validate: 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.
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.
| 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"].
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 predicateOverride 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.
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" }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.jsonor a local copy for offline/pinned use:
vigie schema > .vigie.schema.json
# then: # yaml-language-server: $schema=./.vigie.schema.jsonvigie 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).
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)| 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).
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.
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 |
Apache 2.0