diff --git a/README.md b/README.md index 9178a38..4ba0428 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,18 @@ $ echo $? 1 ``` +Alternatively kubeval can also take input via `stdin` which can make using +it as part of an automated pipeline easier. + +``` +$ cat my-invalid-rc.yaml | kubeval +The document my-invalid-rc.yaml contains an invalid ReplicationController +--> spec.replicas: Invalid type. Expected: integer, given: string +$ echo $? +1 +``` + + ## Why? * If you're writing Kubernetes configuration files by hand it is useful diff --git a/acceptance.bats b/acceptance.bats index fa9cdde..f3b970e 100755 --- a/acceptance.bats +++ b/acceptance.bats @@ -6,6 +6,12 @@ [ "$output" = "The document fixtures/valid.yaml contains a valid ReplicationController" ] } +@test "Pass when parsing a valid Kubernetes config YAML file on stdin" { + run bash -c "cat fixtures/valid.yaml | kubeval" + [ "$status" -eq 0 ] + [ "$output" = "The document stdin contains a valid ReplicationController" ] +} + @test "Pass when parsing a valid Kubernetes config JSON file" { run kubeval fixtures/valid.json [ "$status" -eq 0 ] @@ -46,6 +52,11 @@ [ "$status" -eq 1 ] } +@test "Fail when parsing an invalid Kubernetes config file on stdin" { + run bash -c "cat fixtures/invalid.yaml | kubeval" + [ "$status" -eq 1 ] +} + @test "Return relevant error for non-existent file" { run kubeval fixtures/not-here [ "$status" -eq 1 ] diff --git a/cmd/root.go b/cmd/root.go index a7853b7..b0c124c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,12 @@ package cmd import ( + "bufio" + "bytes" "io/ioutil" "os" "path/filepath" + "runtime" "github.com/spf13/cobra" @@ -21,34 +24,51 @@ var RootCmd = &cobra.Command{ printVersion() os.Exit(0) } - if len(args) < 1 { - log.Error("You must pass at least one file as an argument") - os.Exit(1) - } success := true - for _, fileName := range args { - filePath, _ := filepath.Abs(fileName) - fileContents, err := ioutil.ReadFile(filePath) - if err != nil { - log.Error("Could not open file", fileName) + windowsStdinIssue := false + stat, err := os.Stdin.Stat() + if err != nil { + // Stat() will return an error on Windows in both Powershell and + // console until go1.9 when nothing is passed on stdin. + // See https://github.com/golang/go/issues/14853. + if runtime.GOOS != "windows" { + log.Error(err) os.Exit(1) + } else { + windowsStdinIssue = true } - results, err := kubeval.Validate(fileContents, fileName) + } + // We detect whether we have anything on stdin to process + if !windowsStdinIssue && ((stat.Mode() & os.ModeCharDevice) == 0) { + var buffer bytes.Buffer + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + buffer.WriteString(scanner.Text() + "\n") + } + results, err := kubeval.Validate(buffer.Bytes(), "stdin") if err != nil { log.Error(err) os.Exit(1) } - - for _, result := range results { - if len(result.Errors) > 0 { - success = false - log.Warn("The document", result.FileName, "contains an invalid", result.Kind) - for _, desc := range result.Errors { - log.Info("--->", desc) - } - } else { - log.Success("The document", result.FileName, "contains a valid", result.Kind) + success = logResults(results, success) + } else { + if len(args) < 1 { + log.Error("You must pass at least one file as an argument") + os.Exit(1) + } + for _, fileName := range args { + filePath, _ := filepath.Abs(fileName) + fileContents, err := ioutil.ReadFile(filePath) + if err != nil { + log.Error("Could not open file", fileName) + os.Exit(1) + } + results, err := kubeval.Validate(fileContents, fileName) + if err != nil { + log.Error(err) + os.Exit(1) } + success = logResults(results, success) } } if !success { @@ -57,6 +77,21 @@ var RootCmd = &cobra.Command{ }, } +func logResults(results []kubeval.ValidationResult, success bool) bool { + for _, result := range results { + if len(result.Errors) > 0 { + success = false + log.Warn("The document", result.FileName, "contains an invalid", result.Kind) + for _, desc := range result.Errors { + log.Info("--->", desc) + } + } else { + log.Success("The document", result.FileName, "contains a valid", result.Kind) + } + } + return success +} + // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() {