diff --git a/cmd/kubeconform/main.go b/cmd/kubeconform/main.go index b0e5f28..61b7d9e 100644 --- a/cmd/kubeconform/main.go +++ b/cmd/kubeconform/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/xeipuuv/gojsonschema" "os" @@ -85,7 +86,7 @@ func ValidateResources(resources <-chan resource.Resource, validationResults cha } } -func processResults(o output.Output, validationResults <-chan validator.Result) <-chan bool { +func processResults(ctx context.Context, o output.Output, validationResults <-chan validator.Result, exitOnError bool) <-chan bool { success := true result := make(chan bool) @@ -99,7 +100,15 @@ func processResults(o output.Output, validationResults <-chan validator.Result) fmt.Fprint(os.Stderr, "failed writing log\n") } } + if success == false && exitOnError { + ctx.Done() // early exit - signal to stop searching for resources + break + } } + + for range validationResults { // allow resource finders to exit + } + result <- success close(result) }() @@ -144,12 +153,13 @@ func realMain() int { var errors <-chan error validationResults := make(chan validator.Result) - successChan := processResults(o, validationResults) + ctx := context.Background() + successChan := processResults(ctx, o, validationResults, cfg.ExitOnError) if isStdin { - resourcesChan, errors = resource.FromStream("stdin", os.Stdin) + resourcesChan, errors = resource.FromStream(ctx, "stdin", os.Stdin) } else { - resourcesChan, errors = resource.FromFiles(cfg.Files...) + resourcesChan, errors = resource.FromFiles(ctx, cfg.Files...) } c := cache.New() @@ -165,10 +175,13 @@ func realMain() int { wg.Add(1) go func() { for err := range errors { - if err != nil { - if err, ok := err.(resource.DiscoveryError); ok { - validationResults <- validator.NewError(err.Path, err.Err) - } + if err == nil { + continue + } + + if err, ok := err.(resource.DiscoveryError); ok { + validationResults <- validator.NewError(err.Path, err.Err) + ctx.Done() } } wg.Done() diff --git a/pkg/config/config.go b/pkg/config/config.go index f40d1c3..5cdb70d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,6 +9,7 @@ import ( ) type Config struct { + ExitOnError bool Files []string SchemaLocations []string SkipKinds map[string]bool @@ -59,6 +60,7 @@ func FromFlags(progName string, args []string) (Config, string, error) { flags.StringVar(&c.KubernetesVersion, "kubernetes-version", "1.18.0", "version of Kubernetes to validate against") flags.Var(&schemaLocationsParam, "schema-location", "override schemas location search path (can be specified multiple times)") flags.StringVar(&skipKindsCSV, "skip", "", "comma-separated list of kinds to ignore") + flags.BoolVar(&c.ExitOnError, "exit-on-error", false, "immediately stop execution when the first error is encountered") flags.BoolVar(&c.IgnoreMissingSchemas, "ignore-missing-schemas", false, "skip files with missing schemas instead of failing") flags.BoolVar(&c.Summary, "summary", false, "print a summary at the end") flags.IntVar(&c.NumberOfWorkers, "n", 4, "number of goroutines to run concurrently") diff --git a/pkg/resource/files.go b/pkg/resource/files.go index 8c9bc1a..4719840 100644 --- a/pkg/resource/files.go +++ b/pkg/resource/files.go @@ -2,6 +2,8 @@ package resource import ( "bytes" + "context" + "io" "io/ioutil" "os" "path/filepath" @@ -25,17 +27,25 @@ func (de DiscoveryError) Error() string { return de.Err.Error() } -func FromFiles(paths ...string) (<-chan Resource, <-chan error) { +func FromFiles(ctx context.Context, paths ...string) (<-chan Resource, <-chan error) { resources := make(chan Resource) errors := make(chan error) + stop := false + + go func() { + <-ctx.Done() + stop = true + }() go func() { for _, path := range paths { // we handle errors in the walk function directly // so it should be safe to discard the outer error - _ = filepath.Walk(path, func(p string, i os.FileInfo, err error) error { + err := filepath.Walk(path, func(p string, i os.FileInfo, err error) error { + if stop == true { + return io.EOF + } if err != nil { - errors <- DiscoveryError{p, err} return err } @@ -45,13 +55,11 @@ func FromFiles(paths ...string) (<-chan Resource, <-chan error) { f, err := os.Open(p) if err != nil { - errors <- DiscoveryError{p, err} return err } b, err := ioutil.ReadAll(f) if err != nil { - errors <- DiscoveryError{p, err} return err } @@ -61,6 +69,10 @@ func FromFiles(paths ...string) (<-chan Resource, <-chan error) { return nil }) + + if err != nil && err != io.EOF { + errors <- DiscoveryError{path, err} + } } close(resources) diff --git a/pkg/resource/stream.go b/pkg/resource/stream.go index 79f703c..04382ad 100644 --- a/pkg/resource/stream.go +++ b/pkg/resource/stream.go @@ -2,13 +2,20 @@ package resource import ( "bytes" + "context" "io" "io/ioutil" ) -func FromStream(path string, r io.Reader) (<-chan Resource, <-chan error) { +func FromStream(ctx context.Context, path string, r io.Reader) (<-chan Resource, <-chan error) { resources := make(chan Resource) errors := make(chan error) + stop := false + + go func() { + <-ctx.Done() + stop = true + }() go func() { data, err := ioutil.ReadAll(r) @@ -18,6 +25,9 @@ func FromStream(path string, r io.Reader) (<-chan Resource, <-chan error) { rawResources := bytes.Split(data, []byte("---\n")) for _, rawResource := range rawResources { + if stop == true { + break + } resources <- Resource{Path: path, Bytes: rawResource} } diff --git a/pkg/resource/stream_test.go b/pkg/resource/stream_test.go index 5ff06a8..0da6ba3 100644 --- a/pkg/resource/stream_test.go +++ b/pkg/resource/stream_test.go @@ -2,6 +2,7 @@ package resource_test import ( "bytes" + "context" "github.com/yannh/kubeconform/pkg/resource" "io" "reflect" @@ -143,7 +144,8 @@ kind: Deployment } for _, testCase := range testCases { - resChan, errChan := resource.FromStream(testCase.Have.Path, testCase.Have.Reader) + ctx := context.Background() + resChan, errChan := resource.FromStream(ctx, testCase.Have.Path, testCase.Have.Reader) var wg sync.WaitGroup wg.Add(2)