diff --git a/Readme.md b/Readme.md index 348ee3d..a22ed41 100644 --- a/Readme.md +++ b/Readme.md @@ -101,6 +101,12 @@ $ echo $? 1 ``` +* Passing manifests via Stdin +``` +cat fixtures/valid.yaml | ./bin/kubeconform -summary - +Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0 +``` + * Validating a folder, increasing the number of parallel workers ``` $ ./bin/kubeconform -summary -n 16 fixtures diff --git a/acceptance.bats b/acceptance.bats index a206bbc..081788f 100755 --- a/acceptance.bats +++ b/acceptance.bats @@ -124,4 +124,21 @@ @test "Fail with an invalid input when validating against openshift manifests" { run bin/kubeconform -kubernetes-version 3.8.0 -schema-location 'https://raw.githubusercontent.com/garethr/openshift-json-schema/master/{{ .NormalizedVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}.json' -summary fixtures/invalid.yaml [ "$status" -eq 1 ] -} \ No newline at end of file +} + +@test "Pass when parsing a valid Kubernetes config YAML file on stdin" { + run bash -c "cat fixtures/valid.yaml | bin/kubeconform -summary" + [ "$status" -eq 0 ] + [ "$output" = "Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0" ] +} + +@test "Pass when parsing a valid Kubernetes config YAML file explicitly on stdin" { + run bash -c "cat fixtures/valid.yaml | bin/kubeconform -summary" + [ "$status" -eq 0 ] + [ "$output" = "Summary: 1 resource found parsing stdin - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0" ] +} + +@test "Fail when parsing an invalid Kubernetes config file on stdin" { + run bash -c "cat fixtures/invalid.yaml | bin/kubeconform -" + [ "$status" -eq 1 ] + } diff --git a/cmd/kubeconform/main.go b/cmd/kubeconform/main.go index a694bbe..782034d 100644 --- a/cmd/kubeconform/main.go +++ b/cmd/kubeconform/main.go @@ -4,14 +4,13 @@ import ( "bytes" "flag" "fmt" + "github.com/xeipuuv/gojsonschema" "io" "io/ioutil" "os" "strings" "sync" - "github.com/xeipuuv/gojsonschema" - "github.com/yannh/kubeconform/pkg/cache" "github.com/yannh/kubeconform/pkg/fsutils" "github.com/yannh/kubeconform/pkg/output" @@ -136,14 +135,14 @@ func (ap *arrayParam) Set(value string) error { return nil } -func getLogger(outputFormat string, printSummary, verbose bool) (output.Output, error) { +func getLogger(outputFormat string, printSummary, isStdin, verbose bool) (output.Output, error) { w := os.Stdout switch { case outputFormat == "text": - return output.Text(w, printSummary, verbose), nil + return output.Text(w, printSummary, isStdin, verbose), nil case outputFormat == "json": - return output.JSON(w, printSummary, verbose), nil + return output.JSON(w, printSummary, isStdin, verbose), nil default: return nil, fmt.Errorf("-output must be text or json") } @@ -244,10 +243,18 @@ func realMain() int { return 1 } + // Detect whether we have data being piped through stdin + stat, _ := os.Stdin.Stat() + isStdin := (stat.Mode() & os.ModeCharDevice) == 0 + skipKinds := skipKindsMap(skipKindsCSV) - for _, file := range flag.Args() { - files = append(files, file) + if len(flag.Args()) == 1 && flag.Args()[0] == "-" { + isStdin = true + } else { + for _, file := range flag.Args() { + files = append(files, file) + } } filter := func(signature resource.Signature) bool { @@ -273,6 +280,7 @@ func realMain() int { } validationResults := make(chan []validationResult) + c := cache.New() fileBatches := make(chan []string) go func() { @@ -281,7 +289,7 @@ func realMain() int { }() var o output.Output - if o, err = getLogger(outputFormat, summary, verbose); err != nil { + if o, err = getLogger(outputFormat, summary, isStdin, verbose); err != nil { fmt.Fprintln(os.Stderr, err) return 1 } @@ -289,38 +297,46 @@ func realMain() int { res := make(chan bool) go processResults(o, validationResults, res) - c := cache.New() - var wg sync.WaitGroup - for i := 0; i < nWorkers; i++ { - wg.Add(1) - go func() { - defer wg.Done() + if isStdin { + res := ValidateStream(os.Stdin, registries, k8sVersion, c, filter, ignoreMissingSchemas) + for i := range res { + res[i].filename = "stdin" + } + validationResults <- res + } else { + var wg sync.WaitGroup + for i := 0; i < nWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() - for fileBatch := range fileBatches { - for _, filename := range fileBatch { - f, err := os.Open(filename) - if err != nil { - validationResults <- []validationResult{{ - filename: filename, - err: err, - skipped: true, - }} - continue + for fileBatch := range fileBatches { + for _, filename := range fileBatch { + f, err := os.Open(filename) + if err != nil { + validationResults <- []validationResult{{ + filename: filename, + err: err, + skipped: true, + }} + continue + } + + res := ValidateStream(f, registries, k8sVersion, c, filter, ignoreMissingSchemas) + f.Close() + + for i := range res { + res[i].filename = filename + } + validationResults <- res } - - res := ValidateStream(f, registries, k8sVersion, c, filter, ignoreMissingSchemas) - f.Close() - - for i := range res { - res[i].filename = filename - } - validationResults <- res } - } - }() + }() + } + + wg.Wait() } - wg.Wait() close(validationResults) success := <-res o.Flush() diff --git a/pkg/output/json.go b/pkg/output/json.go index 682bbe3..cbf89fc 100644 --- a/pkg/output/json.go +++ b/pkg/output/json.go @@ -24,7 +24,7 @@ type jsono struct { } // JSON will output the results of the validation as a JSON -func JSON(w io.Writer, withSummary bool, verbose bool) Output { +func JSON(w io.Writer, withSummary bool, isStdin, verbose bool) Output { return &jsono{ w: w, withSummary: withSummary, diff --git a/pkg/output/json_test.go b/pkg/output/json_test.go index b0f0cf2..4d0006f 100644 --- a/pkg/output/json_test.go +++ b/pkg/output/json_test.go @@ -100,7 +100,7 @@ func TestJSONWrite(t *testing.T) { }, } { w := new(bytes.Buffer) - o := JSON(w, testCase.withSummary, testCase.verbose) + o := JSON(w, testCase.withSummary, false, testCase.verbose) for _, res := range testCase.res { o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped) diff --git a/pkg/output/text.go b/pkg/output/text.go index a2706b8..b232821 100644 --- a/pkg/output/text.go +++ b/pkg/output/text.go @@ -10,16 +10,18 @@ type text 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 text -func Text(w io.Writer, withSummary, verbose bool) Output { +func Text(w io.Writer, withSummary, isStdin, verbose bool) Output { return &text{ w: w, withSummary: withSummary, + isStdin: isStdin, verbose: verbose, files: map[string]bool{}, nValid: 0, @@ -76,7 +78,11 @@ func (o *text) Flush() error { if nFiles > 1 { filesPlural = "s" } - _, 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) + 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/text_test.go b/pkg/output/text_test.go index 82628b7..14f35ce 100644 --- a/pkg/output/text_test.go +++ b/pkg/output/text_test.go @@ -72,7 +72,7 @@ Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0 Skipped: 0 }, } { w := new(bytes.Buffer) - o := Text(w, testCase.withSummary, testCase.verbose) + o := Text(w, testCase.withSummary, false, testCase.verbose) for _, res := range testCase.res { o.Write(res.fileName, res.kind, res.name, res.version, res.err, res.skipped)