From 8bc9f42f39e99681e88baea78a4564ddb83a78de Mon Sep 17 00:00:00 2001 From: Yann Hamon Date: Sat, 22 Apr 2023 18:41:51 +0200 Subject: [PATCH] Add support for "pretty" output (#195) * feat: add pretty output format --------- Co-authored-by: William Yardley --- pkg/config/config.go | 2 +- pkg/output/output.go | 2 + pkg/output/pretty.go | 109 ++++++++++++++++++++++++++++++++++++++ pkg/output/pretty_test.go | 84 +++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 pkg/output/pretty.go create mode 100644 pkg/output/pretty_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index b64a3c12..17ec67f0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -74,7 +74,7 @@ func FromFlags(progName string, args []string) (Config, string, error) { flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end (ignored for junit output)") flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently") flags.BoolVar(&c.Strict, "strict", false, "disallow additional properties not in schema or duplicated keys") - flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, tap, text") + flags.StringVar(&c.OutputFormat, "output", "text", "output format - json, junit, pretty, tap, text") flags.BoolVar(&c.Verbose, "verbose", false, "print results for all resources (ignored for tap and junit output)") flags.BoolVar(&c.SkipTLS, "insecure-skip-tls-verify", false, "disable verification of the server's SSL certificate. This will make your HTTPS connections insecure") flags.StringVar(&c.Cache, "cache", "", "cache schemas downloaded via HTTP to this folder") diff --git a/pkg/output/output.go b/pkg/output/output.go index 76128730..aaefc74e 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -20,6 +20,8 @@ func New(outputFormat string, printSummary, isStdin, verbose bool) (Output, erro return jsonOutput(w, printSummary, isStdin, verbose), nil case outputFormat == "junit": return junitOutput(w, printSummary, isStdin, verbose), nil + case outputFormat == "pretty": + return prettyOutput(w, printSummary, isStdin, verbose), nil case outputFormat == "tap": return tapOutput(w, printSummary, isStdin, verbose), nil case outputFormat == "text": diff --git a/pkg/output/pretty.go b/pkg/output/pretty.go new file mode 100644 index 00000000..f9e49653 --- /dev/null +++ b/pkg/output/pretty.go @@ -0,0 +1,109 @@ +package output + +import ( + "fmt" + "io" + "sync" + + "github.com/yannh/kubeconform/pkg/validator" +) + +type prettyo struct { + sync.Mutex + w io.Writer + withSummary bool + isStdin bool + verbose bool + files map[string]bool + nValid, nInvalid, nErrors, nSkipped int +} + +// Text will output the results of the validation as a texto +func prettyOutput(w io.Writer, withSummary, isStdin, verbose bool) Output { + return &prettyo{ + w: w, + withSummary: withSummary, + isStdin: isStdin, + verbose: verbose, + files: map[string]bool{}, + nValid: 0, + nInvalid: 0, + nErrors: 0, + nSkipped: 0, + } +} + +func (o *prettyo) Write(result validator.Result) error { + checkmark := "\u2714" + multiplicationSign := "\u2716" + reset := "\033[0m" + cRed := "\033[31m" + cGreen := "\033[32m" + cYellow := "\033[33m" + + o.Lock() + defer o.Unlock() + + var err error + + sig, _ := result.Resource.Signature() + + o.files[result.Resource.Path] = true + switch result.Status { + case validator.Valid: + if o.verbose { + fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is valid%s\n", cGreen, checkmark, reset, result.Resource.Path, cGreen, sig.Kind, sig.Name, reset) + } + o.nValid++ + case validator.Invalid: + fmt.Fprintf(o.w, "%s%s%s %s: %s%s %s is invalid: %s%s\n", cRed, multiplicationSign, reset, result.Resource.Path, cRed, sig.Kind, sig.Name, result.Err.Error(), reset) + + o.nInvalid++ + case validator.Error: + fmt.Fprintf(o.w, "%s%s%s %s: ", cRed, multiplicationSign, reset, result.Resource.Path) + if sig.Kind != "" && sig.Name != "" { + fmt.Fprintf(o.w, "%s%s failed validation: %s %s%s\n", cRed, sig.Kind, sig.Name, result.Err.Error(), reset) + } else { + fmt.Fprintf(o.w, "%sfailed validation: %s %s%s\n", cRed, sig.Name, result.Err.Error(), reset) + } + o.nErrors++ + case validator.Skipped: + if o.verbose { + fmt.Fprintf(o.w, "%s-%s %s: ", cYellow, reset, result.Resource.Path) + if sig.Kind != "" && sig.Name != "" { + fmt.Fprintf(o.w, "%s%s %s skipped%s\n", cYellow, sig.Kind, sig.Name, reset) + } else if sig.Kind != "" { + fmt.Fprintf(o.w, "%s%s skipped%s\n", cYellow, sig.Kind, reset) + } else { + fmt.Fprintf(o.w, "%sskipped%s\n", cYellow, reset) + } + } + o.nSkipped++ + case validator.Empty: // sent to ensure we count the filename as parsed + } + + return err +} + +func (o *prettyo) Flush() error { + var err error + if o.withSummary { + nFiles := len(o.files) + nResources := o.nValid + o.nInvalid + o.nErrors + o.nSkipped + resourcesPlural := "" + if nResources > 1 { + resourcesPlural = "s" + } + filesPlural := "" + if nFiles > 1 { + filesPlural = "s" + } + if o.isStdin { + _, err = fmt.Fprintf(o.w, "Summary: %d resource%s found parsing stdin - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped) + } else { + _, err = fmt.Fprintf(o.w, "Summary: %d resource%s found in %d file%s - Valid: %d, Invalid: %d, Errors: %d, Skipped: %d\n", nResources, resourcesPlural, nFiles, filesPlural, o.nValid, o.nInvalid, o.nErrors, o.nSkipped) + } + } + + return err +} diff --git a/pkg/output/pretty_test.go b/pkg/output/pretty_test.go new file mode 100644 index 00000000..1131884a --- /dev/null +++ b/pkg/output/pretty_test.go @@ -0,0 +1,84 @@ +package output + +import ( + "bytes" + "testing" + + "github.com/yannh/kubeconform/pkg/resource" + "github.com/yannh/kubeconform/pkg/validator" +) + +func TestPrettyTextWrite(t *testing.T) { + for _, testCase := range []struct { + name string + withSummary bool + isStdin bool + verbose bool + results []validator.Result + expect string + }{ + { + "a single deployment, no summary, no verbose", + false, + false, + false, + []validator.Result{}, + "", + }, + { + "a single deployment, summary, no verbose", + true, + false, + false, + []validator.Result{ + { + Resource: resource.Resource{ + Path: "deployment.yml", + Bytes: []byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: "my-app" +`), + }, + Status: validator.Valid, + Err: nil, + }, + }, + "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n", + }, + { + "a single deployment, verbose, with summary", + true, + false, + true, + []validator.Result{ + { + Resource: resource.Resource{ + Path: "deployment.yml", + Bytes: []byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: "my-app" +`), + }, + Status: validator.Valid, + Err: nil, + }, + }, + "\033[32m✔\033[0m deployment.yml: \033[32mDeployment my-app is valid\033[0m\n" + + "Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0\n", + }, + } { + w := new(bytes.Buffer) + o := prettyOutput(w, testCase.withSummary, testCase.isStdin, testCase.verbose) + + for _, res := range testCase.results { + o.Write(res) + } + o.Flush() + + if w.String() != testCase.expect { + t.Errorf("%s - expected, but got:\n%s\n%s\n", testCase.name, testCase.expect, w) + } + } +}