Skip to content

Commit 16d2f9c

Browse files
committed
feat(test): run kubeconform per test by default
1 parent 5e63012 commit 16d2f9c

3 files changed

Lines changed: 66 additions & 16 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ vigie test <chart> template tier: render + assert (tests/unit/*_test.
293293
--file <path> run a single test file instead of discovering all
294294
--tests <dir> discovery root (default: <chart>/tests)
295295
--snapshot-dir <dir> snapshot directory (default: <chart>/tests/snapshots)
296+
--no-schema skip the per-test kubeconform pass (on by default)
297+
--kube-version <ver> Kubernetes version for the kubeconform pass (default: 1.36.1)
296298
--pass-on-warning exit 0 on run warnings, e.g. no tests executed (default: exit 5)
297299

298300
vigie validate <chart> chart tier: render values.yaml + overlays, validate with kubeconform
@@ -336,6 +338,8 @@ validate:
336338
337339
test:
338340
testsDir: tests/unit
341+
skipSchema: false # kubeconform runs per test by default; true opts out
342+
kubeVersions: [1.36.1] # first entry pins the kubeconform version
339343
```
340344

341345
### Lint rule sets

cmd/vigie/test.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var (
1717
flagTestTestsDir string
1818
flagTestSnapshotDir string
1919
flagTestPassOnWarning bool
20+
flagTestNoSchema bool
21+
flagTestKubeVersion string
2022
)
2123

2224
// exitWarnings is the exit code for a run that produced only warnings (e.g. no
@@ -42,6 +44,8 @@ func init() {
4244
testCmd.Flags().StringVar(&flagTestTestsDir, "tests", "", "Directory to scan recursively for *_test.yaml (overrides test.testsDir; default: <chart>/tests)")
4345
testCmd.Flags().StringVar(&flagTestSnapshotDir, "snapshot-dir", "", "Directory for snapshot files (default: <chart>/tests/snapshots)")
4446
testCmd.Flags().BoolVar(&flagTestPassOnWarning, "pass-on-warning", false, "Exit 0 on run warnings such as no tests executed (default: exit 5)")
47+
testCmd.Flags().BoolVar(&flagTestNoSchema, "no-schema", false, "Skip the per-test kubeconform pass (on by default)")
48+
testCmd.Flags().StringVar(&flagTestKubeVersion, "kube-version", "", "Kubernetes version for the per-test kubeconform pass (default: 1.36.1)")
4549
rootCmd.AddCommand(testCmd)
4650
}
4751

@@ -50,6 +54,10 @@ func runTestCmd(cmd *cobra.Command, args []string) error {
5054

5155
slog.Debug("invoked", "command", "test", "chart", chartPath, "parallelism", flagParallelism)
5256

57+
if err := config.ValidateKubeVersion("--kube-version", flagTestKubeVersion); err != nil {
58+
exitErr(3, "%v", err)
59+
}
60+
5361
cfg, err := config.Load(chartPath)
5462
if err != nil {
5563
exitErr(3, "loading config: %v", err)
@@ -76,12 +84,21 @@ func runTestCmd(cmd *cobra.Command, args []string) error {
7684
if len(files) == 0 {
7785
warnings = append(warnings, fmt.Sprintf("no unit test files found under %s", displayTestsRoot(chartPath, testsDir)))
7886
} else {
87+
// Schema validation is on by default; --no-schema or test.skipSchema disables it.
88+
skipSchema := flagTestNoSchema || cfg.Test.SkipSchema
89+
kubeVersion := flagTestKubeVersion
90+
if kubeVersion == "" && len(cfg.Test.KubeVersions) > 0 {
91+
kubeVersion = cfg.Test.KubeVersions[0]
92+
}
93+
7994
opts := runner.Options{
80-
ChartPath: chartPath,
81-
TestFiles: files,
82-
Parallelism: flagParallelism,
83-
Cfg: cfg,
84-
SnapshotDir: flagTestSnapshotDir,
95+
ChartPath: chartPath,
96+
TestFiles: files,
97+
Parallelism: flagParallelism,
98+
Cfg: cfg,
99+
SnapshotDir: flagTestSnapshotDir,
100+
ValidateSchemas: !skipSchema,
101+
KubeVersion: kubeVersion,
85102
}
86103

87104
results, err := runner.Run(opts)

internal/runner/runner.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ type SuiteResult struct {
4343

4444
// Options controls runner behavior.
4545
type Options struct {
46-
ChartPath string
47-
TestFiles []string
48-
Parallelism int
49-
Cfg *config.Config
50-
SnapshotDir string // default: "<chartPath>/tests/snapshots"
51-
SnapshotUpdate bool
46+
ChartPath string
47+
TestFiles []string
48+
Parallelism int
49+
Cfg *config.Config
50+
SnapshotDir string // default: "<chartPath>/tests/snapshots"
51+
SnapshotUpdate bool
52+
ValidateSchemas bool // when true, kubeconform-validate each test's rendered docs
53+
KubeVersion string // kubernetes version for schema validation; empty = config.DefaultKubeVersion
5254
}
5355

5456
// resolveSnapshotDir returns the snapshot directory: the explicit override
@@ -70,14 +72,30 @@ type expandedTest struct {
7072

7173
// Run executes all test files and returns suite-level results.
7274
func Run(opts Options) ([]SuiteResult, error) {
73-
slog.Debug("starting runner", "files", len(opts.TestFiles), "parallelism", opts.Parallelism)
75+
slog.Debug("starting runner", "files", len(opts.TestFiles), "parallelism", opts.Parallelism, "validateSchemas", opts.ValidateSchemas)
76+
77+
// Build one schema validator shared across all files/tests; its in-memory
78+
// schema cache then amortises across every rendered document.
79+
var schemaValidator *render.SchemaValidator
80+
if opts.ValidateSchemas {
81+
kubeVer := opts.KubeVersion
82+
if kubeVer == "" {
83+
kubeVer = config.DefaultKubeVersion
84+
}
85+
sv, err := render.NewSchemaValidator(kubeVer)
86+
if err != nil {
87+
return nil, fmt.Errorf("setup error: %w", err)
88+
}
89+
schemaValidator = sv
90+
slog.Debug("schema validator ready", "kubeVersion", kubeVer)
91+
}
7492

7593
return runParallel(opts.TestFiles, opts.Parallelism, func(_ int, path string) (SuiteResult, error) {
76-
return runFile(path, opts)
94+
return runFile(path, opts, schemaValidator)
7795
})
7896
}
7997

80-
func runFile(filePath string, opts Options) (SuiteResult, error) {
98+
func runFile(filePath string, opts Options, sv *render.SchemaValidator) (SuiteResult, error) {
8199
slog.Debug("loading test file", "file", filePath)
82100
start := time.Now()
83101

@@ -100,7 +118,7 @@ func runFile(filePath string, opts Options) (SuiteResult, error) {
100118
store := &snapshot.Store{Dir: resolveSnapshotDir(opts.SnapshotDir, opts.ChartPath), Update: opts.SnapshotUpdate}
101119

102120
for _, et := range expanded {
103-
sr.Results = append(sr.Results, runTest(et, suite, opts, store))
121+
sr.Results = append(sr.Results, runTest(et, suite, opts, store, sv))
104122
}
105123
sr.Duration = time.Since(start)
106124
slog.Debug("suite finished", "suite", suite.SuiteName, "tests", len(sr.Results), "duration", sr.Duration)
@@ -226,7 +244,7 @@ func formatEntry(entry map[string]any) string {
226244
return strings.Join(parts, ", ")
227245
}
228246

229-
func runTest(et expandedTest, suite *dsl.Suite, opts Options, store *snapshot.Store) (tr TestResult) {
247+
func runTest(et expandedTest, suite *dsl.Suite, opts Options, store *snapshot.Store, sv *render.SchemaValidator) (tr TestResult) {
230248
test := et.Test
231249
tr = TestResult{SuiteName: suite.SuiteName, TestName: et.DisplayName}
232250
start := time.Now()
@@ -265,6 +283,17 @@ func runTest(et expandedTest, suite *dsl.Suite, opts Options, store *snapshot.St
265283
allDocs = renderResult.Docs
266284
}
267285

286+
// Schema validation (validate tier), on by default unless --no-schema.
287+
if sv != nil && renderErr == nil && len(allDocs) > 0 {
288+
schemaErrs, err := sv.Validate(allDocs)
289+
if err != nil {
290+
tr.Failures = append(tr.Failures, fmt.Sprintf(" schema validation error: %v", err))
291+
}
292+
for _, se := range schemaErrs {
293+
tr.Failures = append(tr.Failures, fmt.Sprintf(" schema: %s/%s: %s", se.Kind, se.Name, se.Message))
294+
}
295+
}
296+
268297
evaluateAssertions(&tr, et, suite, allDocs, renderErr, store)
269298

270299
tr.Pass = len(tr.Failures) == 0

0 commit comments

Comments
 (0)