From fb7cd1fbce063e1ab8f79569436f69c5713dc9b2 Mon Sep 17 00:00:00 2001 From: Nic Waller Date: Wed, 8 Jul 2026 17:24:06 -0700 Subject: [PATCH] feat: add schema caching and local schema directory support - Add --cache-dir flag (defaults to XDG_CACHE_HOME/testchart or ~/.cache/testchart) to cache downloaded schemas on disk across runs. This avoids re-downloading the same schemas on every invocation. - Add --schema-dir flag to point at a local checkout of yannh/kubernetes-json-schema, eliminating all HTTP traffic for standard Kubernetes schemas. When set, --cache-dir has no effect (local file reads need no cache). Co-Authored-By: Claude Sonnet 4.6 --- src/builder.go | 8 +++++--- src/main.go | 45 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/builder.go b/src/builder.go index 1bec85a..587c07b 100644 --- a/src/builder.go +++ b/src/builder.go @@ -41,7 +41,7 @@ type Test struct { ignoredLines []string } -func (test *Test) Run(theChart *chart.Chart, installAction *action.Install, rootPath string, ignorePatterns []string, schema *cue.Value) error { +func (test *Test) Run(theChart *chart.Chart, installAction *action.Install, rootPath string, ignorePatterns []string, schema *cue.Value, cacheDir, schemaDir string) error { testValuesPath := filepath.Join(rootPath, test.name, "values.yaml") testValues, err := loadValuesFile(testValuesPath) if err != nil { @@ -157,7 +157,7 @@ func (test *Test) Run(theChart *chart.Chart, installAction *action.Install, root } // Validate - err = validateManifest(test, release.Manifest) + err = validateManifest(test, release.Manifest, cacheDir, schemaDir) if err != nil { return fmt.Errorf("validating manifest: %w", err) } @@ -503,6 +503,8 @@ type RunOptions struct { IgnorePatterns []string Schema *cue.Value Concurrency int + CacheDir string + SchemaDir string HelmOptions } @@ -573,7 +575,7 @@ func (suite TestSuite) Run(opts RunOptions) error { theChart.Metadata.AppVersion = appVersion } - if err := test.Run(theChart, installAction, opts.RootFS, opts.IgnorePatterns, opts.Schema); err != nil { + if err := test.Run(theChart, installAction, opts.RootFS, opts.IgnorePatterns, opts.Schema, opts.CacheDir, opts.SchemaDir); err != nil { e <- fmt.Errorf("running test %s: %w", test.name, err) return } diff --git a/src/main.go b/src/main.go index 781a11c..1f1623e 100644 --- a/src/main.go +++ b/src/main.go @@ -6,6 +6,7 @@ import ( "io" "log" "os" + "path/filepath" "regexp" "runtime" "strings" @@ -27,6 +28,17 @@ var ( debugOutput = "" ) +func defaultCacheDir() string { + if dir := os.Getenv("XDG_CACHE_HOME"); dir != "" { + return filepath.Join(dir, "testchart") + } + home, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(home, ".cache", "testchart") +} + func main() { var testPath string var namespace string @@ -34,6 +46,8 @@ func main() { var chartVersion string var appVersion string var concurrency int + var cacheDir string + var schemaDir string isUpdate := false var ignorePatterns []string @@ -53,13 +67,15 @@ func main() { rootCmd.PersistentFlags().StringSliceVarP(&ignorePatterns, "ignore", "i", []string{}, "Regex specifying lines to ignore (can be specified multiple times)") rootCmd.PersistentFlags().StringVar(&debugOutput, "debug", "", "location to render failed install output manifests for debugging") rootCmd.PersistentFlags().IntVarP(&concurrency, "concurrency", "c", runtime.GOMAXPROCS(0), "test run concurrency") + rootCmd.PersistentFlags().StringVar(&cacheDir, "cache-dir", defaultCacheDir(), "directory for caching downloaded schemas (set to empty string to disable)") + rootCmd.PersistentFlags().StringVar(&schemaDir, "schema-dir", "", "local checkout of yannh/kubernetes-json-schema to use instead of downloading schemas over HTTP") runCmd := &cobra.Command{ Use: "run [test1 test2 ...]", Short: "Run unit tests", Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runTests(args, testPath, namespace, release, chartVersion, appVersion, isUpdate, ignorePatterns, concurrency) + return runTests(args, testPath, namespace, release, chartVersion, appVersion, isUpdate, ignorePatterns, concurrency, cacheDir, schemaDir) }, } @@ -69,7 +85,7 @@ func main() { Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { isUpdate = true - return runTests(args, testPath, namespace, release, chartVersion, appVersion, isUpdate, ignorePatterns, concurrency) + return runTests(args, testPath, namespace, release, chartVersion, appVersion, isUpdate, ignorePatterns, concurrency, cacheDir, schemaDir) }, } @@ -91,7 +107,7 @@ func main() { } } -func runTests(args []string, testPath, namespace, releaseName, chartVersion, appVersion string, isUpdate bool, ignorePatterns []string, concurrency int) error { +func runTests(args []string, testPath, namespace, releaseName, chartVersion, appVersion string, isUpdate bool, ignorePatterns []string, concurrency int, cacheDir, schemaDir string) error { if _, err := os.Stat(testPath); os.IsNotExist(err) { fmt.Println("No tests found") return nil @@ -125,6 +141,8 @@ func runTests(args []string, testPath, namespace, releaseName, chartVersion, app IgnorePatterns: ignorePatterns, Schema: schema, Concurrency: concurrency, + CacheDir: cacheDir, + SchemaDir: schemaDir, HelmOptions: HelmOptions{ Namespace: namespace, Release: releaseName, @@ -194,8 +212,25 @@ func loadValuesFile(filePath string) (map[string]any, error) { return data, nil } -func validateManifest(test *Test, manifest string) error { - v, err := validator.New(nil, validator.Opts{Strict: true, IgnoreMissingSchemas: true}) +func buildSchemaLocations(schemaDir string) []string { + if schemaDir != "" { + local := "file://" + filepath.Join(schemaDir, "{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}", "{{ .ResourceKind }}{{ .KindSuffix }}.json") + return []string{local} + } + return nil // default: kubeconform uses yannh/kubernetes-json-schema +} + +func validateManifest(test *Test, manifest string, cacheDir, schemaDir string) error { + if cacheDir != "" && schemaDir == "" { + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + return fmt.Errorf("creating cache directory: %w", err) + } + } + effectiveCacheDir := cacheDir + if schemaDir != "" { + effectiveCacheDir = "" + } + v, err := validator.New(buildSchemaLocations(schemaDir), validator.Opts{Strict: true, IgnoreMissingSchemas: true, Cache: effectiveCacheDir}) if err != nil { return fmt.Errorf("initializing validator: %w", err) }