From 8292f611662fa1f409b370e3837dc40c9ff2ca41 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Mon, 12 Feb 2024 16:55:01 +0100 Subject: [PATCH 1/5] feat: allow validating a single chart directory --- main.go | 74 ++++++++++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/main.go b/main.go index edaebea..fbd1156 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,7 @@ package main import ( "bytes" - "errors" - "fmt" + "io/fs" "os" "os/exec" "path/filepath" @@ -84,23 +83,38 @@ func main() { } func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) error { - return foreachChart(cfg.ChartsDirectory.path, func(base string) error { - logger := log.With().Str("chart", filepath.Base(base)).Logger() - valuesFiles, err := os.ReadDir(filepath.Join(base, TestsPath)) + err := filepath.WalkDir(cfg.ChartsDirectory.path, func(path string, dirent fs.DirEntry, err error) error { + logger := log.With().Str("path", path).Logger() if err != nil { - logger.Error().Stack().Err(err).Msgf("Could not open directory %s", base) - return err + logger.Warn().Err(err).Msg("skipping path") + return nil + } + + if dirent.IsDir() { + return nil } - for _, file := range valuesFiles { - name := file.Name() - fileLogger := logger.With().Str("file", name).Logger() - fileLogger.Printf("Validating chart %s with values file %s...\n", filepath.Base(base), name) - manifests, err := runHelm(cfg.Helm.path, base, name, updateDependencies) + if dirent.Name() != "Chart.yaml" && dirent.Name() != "Chart.yml" { + return nil + } + + chart_dir := filepath.Dir(path) + logger = log.With().Str("chart", filepath.Base(chart_dir)).Logger() + err = filepath.WalkDir(filepath.Join(chart_dir, TestsPath), func(values_file string, dirent fs.DirEntry, err error) error { + logger := logger.With().Str("values", dirent.Name()).Logger() + if err != nil { + logger.Err(err).Stack().Msg("Could not open directory") + return err + } + if dirent.Name() == TestsPath { + return nil + } + logger.Info().Msg("Validating chart...") + manifests, err := runHelm(cfg.Helm.path, chart_dir, dirent.Name(), updateDependencies) if err != nil { - fileLogger.Printf("Could not run Helm: %s\nStdout: %s\n", err, manifests.String()) + logger.Err(err).Msgf("Could not run Helm: stdout: %s\n", manifests.String()) return err } @@ -109,38 +123,18 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er // , // so instead we shell out to it output, err := runKubeconform(manifests, cfg.Kubeconform.path, cfg.Strict, additionalSchemaPaths, cfg.KubernetesVersion) + logger.Info().Msgf("Output: %s", output) - fileLogger.Info().Msgf("Output: %s", output) - - if err != nil { - return err - } + return err + }) + if err != nil { + return err } - return nil + return fs.SkipDir // We processed the chart, so skip its directory }) -} - -func foreachChart(path string, fn func(path string) error) error { - files, err := os.ReadDir(path) - - if err != nil { - return err - } - - for _, file := range files { - if !file.IsDir() { - return errors.New(fmt.Sprintf("Non-directory file in charts directory: %s", file.Name())) - } - - p := filepath.Join(path, file.Name()) - - if err := fn(p); err != nil { - return err - } - } - return nil + return err } func runHelm(path string, directory string, valuesFile string, updateDependencies bool) (bytes.Buffer, error) { From e45a5c8a6dce87e8bb79785a37879a659b532b71 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Mon, 12 Feb 2024 22:03:30 +0100 Subject: [PATCH 2/5] feat: validate all charts and report all failures --- main.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index fbd1156..baef79e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "io/fs" "os" "os/exec" @@ -83,6 +84,7 @@ func main() { } func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) error { + var validationsErrors []error err := filepath.WalkDir(cfg.ChartsDirectory.path, func(path string, dirent fs.DirEntry, err error) error { logger := log.With().Str("path", path).Logger() @@ -101,7 +103,7 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er chart_dir := filepath.Dir(path) logger = log.With().Str("chart", filepath.Base(chart_dir)).Logger() - err = filepath.WalkDir(filepath.Join(chart_dir, TestsPath), func(values_file string, dirent fs.DirEntry, err error) error { + filepath.WalkDir(filepath.Join(chart_dir, TestsPath), func(values_file string, dirent fs.DirEntry, err error) error { logger := logger.With().Str("values", dirent.Name()).Logger() if err != nil { logger.Err(err).Stack().Msg("Could not open directory") @@ -124,16 +126,20 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er // so instead we shell out to it output, err := runKubeconform(manifests, cfg.Kubeconform.path, cfg.Strict, additionalSchemaPaths, cfg.KubernetesVersion) logger.Info().Msgf("Output: %s", output) + if err != nil { + validationsErrors = append(validationsErrors, err) + } + + return nil - return err }) - if err != nil { - return err - } return fs.SkipDir // We processed the chart, so skip its directory }) + if validationsErrors != nil { + return errors.New("") + } return err } From d231947060daf79af952e5756b95eddda2b43c50 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Mon, 12 Feb 2024 22:03:51 +0100 Subject: [PATCH 3/5] feat: allow skipping directories Add a new parameters to skip searching charts under some directories. We default to `\.git` as this is the default useful thing when running on the root of a git reposository. --- action.yml | 4 ++++ main.go | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 0455bcf..4684511 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,10 @@ inputs: description: "Directory to search for chart directories under" default: "charts" required: true + regexSkipDir: + description: "Skip search in directories matching this regex" + default: "\.git" + required: false kubernetesVersion: description: "Version of Kubernetes to validate manifests against" default: "master" diff --git a/main.go b/main.go index baef79e..ddfbdc2 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "reflect" + "regexp" "strings" "github.com/caarlos0/env/v6" @@ -30,6 +31,7 @@ type Config struct { Strict bool `env:"KUBECONFORM_STRICT" envDefault:"true"` AdditionalSchemaPaths []Path `env:"ADDITIONAL_SCHEMA_PATHS" envSeparator:"\n"` ChartsDirectory Path `env:"CHARTS_DIRECTORY"` + RegexSkipDir string `env:"REGEX_SKIP_DIR" envDefault:"\.git"` KubernetesVersion string `env:"KUBERNETES_VERSION" envDefault:"master"` Kubeconform Path `env:"KUBECONFORM"` Helm Path `env:"HELM"` @@ -85,6 +87,7 @@ func main() { func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) error { var validationsErrors []error + skipRegex := regexp.MustCompile("^" + cfg.RegexSkipDir + "$") err := filepath.WalkDir(cfg.ChartsDirectory.path, func(path string, dirent fs.DirEntry, err error) error { logger := log.With().Str("path", path).Logger() @@ -93,8 +96,9 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er return nil } - if dirent.IsDir() { - return nil + if dirent.IsDir() && skipRegex.MatchString(dirent.Name()) { + logger.Info().Msg("matching skip regex, skipping") + return fs.SkipDir } if dirent.Name() != "Chart.yaml" && dirent.Name() != "Chart.yml" { From c89dac8d99f1fca42ce1971062df1c3412af4b90 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Mon, 12 Feb 2024 23:16:11 +0100 Subject: [PATCH 4/5] fix: simplify logging --- main.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index ddfbdc2..51cc62f 100644 --- a/main.go +++ b/main.go @@ -80,7 +80,7 @@ func main() { feErr := run(cfg, additionalSchemaPaths, cfg.UpdateDependencies) if feErr != nil { - log.Fatal().Stack().Err(feErr).Msgf("Validation failed: %s", feErr) + log.Fatal().Stack().Err(feErr).Msg("") return } } @@ -89,7 +89,7 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er var validationsErrors []error skipRegex := regexp.MustCompile("^" + cfg.RegexSkipDir + "$") - err := filepath.WalkDir(cfg.ChartsDirectory.path, func(path string, dirent fs.DirEntry, err error) error { + filepath.WalkDir(cfg.ChartsDirectory.path, func(path string, dirent fs.DirEntry, err error) error { logger := log.With().Str("path", path).Logger() if err != nil { logger.Warn().Err(err).Msg("skipping path") @@ -116,11 +116,10 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er if dirent.Name() == TestsPath { return nil } - logger.Info().Msg("Validating chart...") manifests, err := runHelm(cfg.Helm.path, chart_dir, dirent.Name(), updateDependencies) if err != nil { - logger.Err(err).Msgf("Could not run Helm: stdout: %s\n", manifests.String()) + logger.Err(err).Str("stdout", manifests.String()).Msg("Could not run Helm") return err } @@ -129,7 +128,7 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er // , // so instead we shell out to it output, err := runKubeconform(manifests, cfg.Kubeconform.path, cfg.Strict, additionalSchemaPaths, cfg.KubernetesVersion) - logger.Info().Msgf("Output: %s", output) + logger.Err(err).Str("output", output).Msg("") if err != nil { validationsErrors = append(validationsErrors, err) } @@ -142,9 +141,9 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er }) if validationsErrors != nil { - return errors.New("") + return errors.New("Validation failed") } - return err + return nil } func runHelm(path string, directory string, valuesFile string, updateDependencies bool) (bytes.Buffer, error) { @@ -197,12 +196,6 @@ func runKubeconform(manifests bytes.Buffer, path string, strict bool, additional output, err := cmd.CombinedOutput() - // whatever the output is, we want to display it, and we want to return the error if there is one - if err != nil { - log.Printf("Failed to run kubeconform command %s: %s\n", cmd, string(output[:])) - return "", err - } - return string(output[:]), err } From bb9fc5cbd80c2d9882c260ed0b30c3a4f91f98ef Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Mon, 12 Feb 2024 23:49:58 +0100 Subject: [PATCH 5/5] feat: propagate JSON output from kubeconform upon validation error --- main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 51cc62f..e6e7274 100644 --- a/main.go +++ b/main.go @@ -128,7 +128,7 @@ func run(cfg Config, additionalSchemaPaths []string, updateDependencies bool) er // , // so instead we shell out to it output, err := runKubeconform(manifests, cfg.Kubeconform.path, cfg.Strict, additionalSchemaPaths, cfg.KubernetesVersion) - logger.Err(err).Str("output", output).Msg("") + logger.Err(err).RawJSON("kubeconform", output).Msg("") if err != nil { validationsErrors = append(validationsErrors, err) } @@ -180,13 +180,13 @@ func runHelmUpdateDependencies(path string, directory string) error { return cmd.Run() } -func runKubeconform(manifests bytes.Buffer, path string, strict bool, additionalSchemaPaths []string, kubernetesVersion string) (string, error) { +func runKubeconform(manifests bytes.Buffer, path string, strict bool, additionalSchemaPaths []string, kubernetesVersion string) ([]byte, error) { cmd := kubeconformCommand(path, strict, additionalSchemaPaths, kubernetesVersion) stdin, err := cmd.StdinPipe() if err != nil { - return "", err + return nil, err } go func() { @@ -196,7 +196,7 @@ func runKubeconform(manifests bytes.Buffer, path string, strict bool, additional output, err := cmd.CombinedOutput() - return string(output[:]), err + return output, err } func kubeconformCommand(path string, strict bool, additionalSchemaPaths []string, kubernetesVersion string) *exec.Cmd { @@ -210,6 +210,8 @@ func kubeconformArgs(strict bool, additionalSchemaPaths []string, kubernetesVers "-summary", "-kubernetes-version", kubernetesVersion, + "-output", + "json", } if strict {