diff --git a/src/builder.go b/src/builder.go index 32fe007..f7364f8 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 { @@ -164,7 +164,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) } @@ -510,6 +510,8 @@ type RunOptions struct { IgnorePatterns []string Schema *cue.Value Concurrency int + CacheDir string + SchemaDir string HelmOptions } @@ -580,7 +582,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 6df7705..f89e279 100644 --- a/src/main.go +++ b/src/main.go @@ -6,6 +6,7 @@ import ( "io" "log" "os" + "path/filepath" "regexp" "runtime" "strings" @@ -28,6 +29,17 @@ var ( normalize bool ) +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 @@ -35,6 +47,8 @@ func main() { var chartVersion string var appVersion string var concurrency int + var cacheDir string + var schemaDir string isUpdate := false var ignorePatterns []string @@ -55,13 +69,15 @@ func main() { 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().BoolVar(&normalize, "normalize", true, "normalize output. Disabling allows you to view raw helm templating.") + 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) }, } @@ -71,7 +87,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) }, } @@ -93,7 +109,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 @@ -127,6 +143,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, @@ -196,8 +214,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) }