From bf6ad37e9d82d1cd570815d8aedc2aecf409b4eb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 09:09:14 -0400 Subject: [PATCH 1/7] add list-providers command Signed-off-by: Alex Goodman --- .github/workflows/daily-data-sync.yaml | 9 ++ Makefile | 2 +- cmd/grype-db/cli/cli.go | 1 + cmd/grype-db/cli/commands/list_providers.go | 104 ++++++++++++++++++++ cmd/grype-db/cli/commands/utils.go | 59 ++++++----- cmd/grype-db/cli/options/format.go | 30 ++++++ pkg/provider/providers/vunnel/provider.go | 9 +- 7 files changed, 188 insertions(+), 26 deletions(-) create mode 100644 cmd/grype-db/cli/commands/list_providers.go create mode 100644 cmd/grype-db/cli/options/format.go diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml index d9d9826c..d2618e06 100644 --- a/.github/workflows/daily-data-sync.yaml +++ b/.github/workflows/daily-data-sync.yaml @@ -19,6 +19,15 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + with: + python: false + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + - name: Read configured providers id: read-providers # TODO: honor CI overrides diff --git a/Makefile b/Makefile index eb9d87b7..88277b53 100644 --- a/Makefile +++ b/Makefile @@ -197,7 +197,7 @@ update-test-fixtures: .PHONY: show-providers show-providers: @# this is used in CI to generate a job matrix, pulling data for each provider concurrently - @cat .grype-db.yaml | python -c 'import yaml; import json; import sys; print(json.dumps([x["name"] for x in yaml.safe_load(sys.stdin).get("provider",{}).get("configs",[])]));' + @go run ./cmd/grype-db list-providers -c publish/.grype-db.yaml -q -o json .PHONY: download-provider-cache download-provider-cache: diff --git a/cmd/grype-db/cli/cli.go b/cmd/grype-db/cli/cli.go index 77ee7d7d..d36ca0c6 100644 --- a/cmd/grype-db/cli/cli.go +++ b/cmd/grype-db/cli/cli.go @@ -40,6 +40,7 @@ func New(opts ...Option) *cobra.Command { root.AddCommand(commands.Pull(app)) root.AddCommand(commands.Build(app)) root.AddCommand(commands.Package(app)) + root.AddCommand(commands.ListProviders(app)) root.AddCommand(cache) return root diff --git a/cmd/grype-db/cli/commands/list_providers.go b/cmd/grype-db/cli/commands/list_providers.go new file mode 100644 index 00000000..2993d604 --- /dev/null +++ b/cmd/grype-db/cli/commands/list_providers.go @@ -0,0 +1,104 @@ +package commands + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/scylladb/go-set/strset" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/provider/providers" + "github.com/anchore/grype-db/pkg/provider/providers/vunnel" +) + +var _ options.Interface = &listProvidersConfig{} + +type listProvidersConfig struct { + options.Format `yaml:",inline" mapstructure:",squash"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *listProvidersConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Format, &o.Provider) +} + +func (o *listProvidersConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Format, &o.Provider) +} + +func ListProviders(app *application.Application) *cobra.Command { + cfg := listProvidersConfig{ + Provider: options.DefaultProvider(), + Format: options.Format{ + Output: "text", + AllowableFormats: []string{"text", "json"}, + }, + } + + cmd := &cobra.Command{ + Use: "list-providers", + Short: "list all configured providers", + Args: chainArgs( + cobra.NoArgs, + func(cmd *cobra.Command, args []string) error { + allowableOutputs := strset.New(cfg.Format.AllowableFormats...) + if !allowableOutputs.Has(cfg.Format.Output) { + return fmt.Errorf("invalid output format: %s (allowable: %s)", cfg.Format.Output, strings.Join(cfg.Format.AllowableFormats, ", ")) + } + return nil + }, + ), + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return runListProviders(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func runListProviders(cfg listProvidersConfig) error { + ps, err := providers.New(cfg.Root, vunnel.Config{ + Executor: cfg.Vunnel.Executor, + DockerTag: cfg.Vunnel.DockerTag, + DockerImage: cfg.Vunnel.DockerImage, + GenerateConfigs: cfg.Vunnel.GenerateConfigs, + ExcludeProviders: cfg.Vunnel.ExcludeProviders, + Env: cfg.Vunnel.Env, + }, cfg.Provider.Configs...) + if err != nil { + if errors.Is(err, providers.ErrNoProviders) { + log.Error("configure a provider via the application config or use -g to generate a list of configs from vunnel") + } + return err + } + + if cfg.Format.Output == "text" { + for _, p := range ps { + fmt.Println(p.ID().Name) + } + } else if cfg.Format.Output == "json" { + names := make([]string, 0, len(ps)) + for _, p := range ps { + names = append(names, p.ID().Name) + } + by, err := json.Marshal(names) + if err != nil { + return err + } + fmt.Println(string(by)) + } + + return nil +} diff --git a/cmd/grype-db/cli/commands/utils.go b/cmd/grype-db/cli/commands/utils.go index d500464f..42adbe7b 100644 --- a/cmd/grype-db/cli/commands/utils.go +++ b/cmd/grype-db/cli/commands/utils.go @@ -37,28 +37,39 @@ func commonConfiguration(app *application.Application, cmd *cobra.Command, opts cmd.SilenceUsage = true cmd.SilenceErrors = true cmd.SetHelpTemplate(`{{if (or .Long .Short)}}{{.Long}}{{if not .Long}}{{.Short}}{{end}} - - {{end}}Usage:{{if .Runnable}} - {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} - {{.CommandPath}} [command]{{end}}{{if .HasExample}} - - {{.Example}}{{end}}{{if gt (len .Aliases) 0}} - - Aliases: - {{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}} - - Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} - - {{if not .CommandPath}}Global {{end}}Flags: - {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if (and .HasAvailableInheritedFlags (not .CommandPath))}} - - Global Flags: - {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} - - Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} - - Use "{{if .CommandPath}}{{.CommandPath}} {{end}}[command] --help" for more information about a command.{{end}} - `) + +{{end}}Usage:{{if .Runnable}} +{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} +{{.CommandPath}} [command]{{end}}{{if .HasExample}} + +{{.Example}}{{end}}{{if gt (len .Aliases) 0}} + +Aliases: +{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} +{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +{{if not .CommandPath}}Global {{end}}Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if (and .HasAvailableInheritedFlags (not .CommandPath))}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} +{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{if .CommandPath}}{{.CommandPath}} {{end}}[command] --help" for more information about a command.{{end}} +`) +} + +func chainArgs(processors ...func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + for _, p := range processors { + if err := p(cmd, args); err != nil { + return err + } + } + return nil + } } diff --git a/cmd/grype-db/cli/options/format.go b/cmd/grype-db/cli/options/format.go new file mode 100644 index 00000000..97ada0db --- /dev/null +++ b/cmd/grype-db/cli/options/format.go @@ -0,0 +1,30 @@ +package options + +import ( + "fmt" + + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Format{} + +type Format struct { + Output string `yaml:"output" json:"output" mapstructure:"output"` + AllowableFormats []string `yaml:"-" json:"-" mapstructure:"-"` +} + +func (o *Format) AddFlags(flags *pflag.FlagSet) { + flags.StringVarP( + &o.Output, + "output", "o", o.Output, + fmt.Sprintf("output format to report results in (allowable values: %s)", o.AllowableFormats), + ) +} + +func (o *Format) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + if err := Bind(v, "output", flags.Lookup("output")); err != nil { + return err + } + return nil +} diff --git a/pkg/provider/providers/vunnel/provider.go b/pkg/provider/providers/vunnel/provider.go index 44c91643..fabc8917 100644 --- a/pkg/provider/providers/vunnel/provider.go +++ b/pkg/provider/providers/vunnel/provider.go @@ -146,8 +146,15 @@ func GenerateConfigs(root string, cfg Config) ([]provider.Config, error) { return nil, err } cmd, args := cmdList[0], cmdList[1:] - out, err := exec.Command(cmd, args...).Output() + + cmdObj := exec.Command(cmd, args...) + sb := strings.Builder{} + cmdObj.Stderr = &sb + out, err := cmdObj.Output() if err != nil { + if sb.Len() > 0 { + log.Errorf("vunnel list failed: %s", sb.String()) + } return nil, fmt.Errorf("unable to execute vunnel list: %w", err) } From 646cbb1f80e080a3b47d17348bb53617d99043b8 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 09:59:25 -0400 Subject: [PATCH 2/7] remove slack notification within matrix Signed-off-by: Alex Goodman --- .github/workflows/daily-data-sync.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml index d2618e06..29b28722 100644 --- a/.github/workflows/daily-data-sync.yaml +++ b/.github/workflows/daily-data-sync.yaml @@ -71,15 +71,6 @@ jobs: - name: Upload the provider workspace state run: make upload-provider-cache provider=${{ matrix.provider }} - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: workflow,eventName - text: Daily Data Sync for ${{ matrix.provider }} failed - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} - if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} - aggregate-cache: name: "Aggregate provider cache" runs-on: ubuntu-20.04 From 548503aea6098d0e12460e93058cf5d7e00e802e Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 09:59:39 -0400 Subject: [PATCH 3/7] ensure all grype-db commands use publish config Signed-off-by: Alex Goodman --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 88277b53..f333c13f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ TEMP_DIR = ./.tmp RESULTS_DIR = $(TEMP_DIR)/results DB_ARCHIVE = ./grype-db-cache.tar.gz -GRYPE_DB = go run ./cmd/$(BIN)/main.go +GRYPE_DB = go run ./cmd/$(BIN)/main.go -c publish/.grype-db.yaml GRYPE_DB_DATA_IMAGE_NAME = ghcr.io/anchore/$(BIN)/data date = $(shell date -u +"%y-%m-%d") @@ -197,7 +197,7 @@ update-test-fixtures: .PHONY: show-providers show-providers: @# this is used in CI to generate a job matrix, pulling data for each provider concurrently - @go run ./cmd/grype-db list-providers -c publish/.grype-db.yaml -q -o json + @$(GRYPE_DB) list-providers -q -o json .PHONY: download-provider-cache download-provider-cache: From f23e72f0a6dc2cd3728b3e2760daca29cf7743f2 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 10:16:50 -0400 Subject: [PATCH 4/7] update aggregation script to use show-providers make target Signed-off-by: Alex Goodman --- .github/scripts/aggregate-all-provider-cache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/aggregate-all-provider-cache.py b/.github/scripts/aggregate-all-provider-cache.py index 453462d2..ad819c77 100755 --- a/.github/scripts/aggregate-all-provider-cache.py +++ b/.github/scripts/aggregate-all-provider-cache.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -import yaml +import json import sys import subprocess -with open(".grype-db.yaml") as f: - providers = [x["name"] for x in yaml.safe_load(f.read()).get("provider", {}).get("configs", [])] +output = subprocess.run("make show-providers", shell=True, check=True, stdout=subprocess.PIPE, stderr=sys.stderr).stdout +providers = json.loads(output) print(f"providers: {providers}") for provider in providers: From 7e715e2ec127a807c31b46bee7faa439570e1ddb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 10:49:41 -0400 Subject: [PATCH 5/7] override list of providers based on what CI used, not what grype-db will produce Signed-off-by: Alex Goodman --- .../scripts/aggregate-all-provider-cache.py | 19 ++++++++++++++++--- .github/workflows/daily-data-sync.yaml | 12 ++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/scripts/aggregate-all-provider-cache.py b/.github/scripts/aggregate-all-provider-cache.py index ad819c77..69391344 100755 --- a/.github/scripts/aggregate-all-provider-cache.py +++ b/.github/scripts/aggregate-all-provider-cache.py @@ -1,11 +1,24 @@ #!/usr/bin/env python3 -import json +import os +import ast import sys import subprocess -output = subprocess.run("make show-providers", shell=True, check=True, stdout=subprocess.PIPE, stderr=sys.stderr).stdout -providers = json.loads(output) +output = os.environ.get("PROVIDERS_USED", None) + +if not output: + print(f"invoking grype-db to get list of providers to use") + output = subprocess.run("make show-providers", shell=True, check=True, stdout=subprocess.PIPE, stderr=sys.stderr).stdout +else: + print("using values from $PROVIDERS_USED environment variable") + +print(f"output: {output!r}") + +# why in the world would we use ast instead of JSON?! +# short answer: python borks when there are strings with single quotes instead of double quotes +providers = ast.literal_eval(output) print(f"providers: {providers}") + for provider in providers: subprocess.run(f"make download-provider-cache provider={provider}", shell=True, check=True, stdout=sys.stdout, stderr=sys.stderr) diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml index 29b28722..e2b7072d 100644 --- a/.github/workflows/daily-data-sync.yaml +++ b/.github/workflows/daily-data-sync.yaml @@ -93,17 +93,9 @@ jobs: echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin - name: Aggregate vulnerability data - # TODO: hook up to matrix override run: make aggregate-all-provider-cache + env: + PROVIDERS_USED: ${{ needs.discover-providers.outputs.providers }} - name: Upload vulnerability data cache image run: make upload-all-provider-cache - - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: workflow,eventName - text: Daily Data Sync aggregation failed - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} - if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} From 66f398737174cb33dcfc0d1f4fb8e0ffef954d9b Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 11:10:51 -0400 Subject: [PATCH 6/7] add discover-providers job explicitly as dependency to access output vars Signed-off-by: Alex Goodman --- .github/workflows/daily-data-sync.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml index e2b7072d..c0680dd6 100644 --- a/.github/workflows/daily-data-sync.yaml +++ b/.github/workflows/daily-data-sync.yaml @@ -74,7 +74,9 @@ jobs: aggregate-cache: name: "Aggregate provider cache" runs-on: ubuntu-20.04 - needs: update-provider + needs: + - update-provider + - discover-providers # set the permissions granted to the github token to read the pull cache from ghcr.io permissions: packages: write From 0ff63f0d8c1e105e7f56deead9e1150aaf42815c Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 16 Mar 2023 14:16:34 -0400 Subject: [PATCH 7/7] allow for selecting on single provider status and validate before upload Signed-off-by: Alex Goodman --- .github/workflows/daily-data-sync.yaml | 12 +++++++ Makefile | 2 ++ cmd/grype-db/cli/commands/cache_status.go | 43 +++++++++++++++++++---- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml index c0680dd6..1538591c 100644 --- a/.github/workflows/daily-data-sync.yaml +++ b/.github/workflows/daily-data-sync.yaml @@ -68,12 +68,24 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: workflow,eventName + text: Daily Data Sync for ${{ matrix.provider }} failed + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} + if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} + - name: Upload the provider workspace state + # even if the job fails, we want to upload yesterdays cache as todays cache to continue the DB build + if: ${{ always() }} run: make upload-provider-cache provider=${{ matrix.provider }} aggregate-cache: name: "Aggregate provider cache" runs-on: ubuntu-20.04 + if: ${{ always() }} needs: - update-provider - discover-providers diff --git a/Makefile b/Makefile index f333c13f..f6092b9c 100644 --- a/Makefile +++ b/Makefile @@ -214,6 +214,7 @@ upload-provider-cache: ci-check $(call title,Uploading "$(provider)" existing provider data cache) @rm -f $(DB_ARCHIVE) + $(GRYPE_DB) cache status -p $(provider) $(GRYPE_DB) cache backup -v --path $(DB_ARCHIVE) -p $(provider) oras push -v $(GRYPE_DB_DATA_IMAGE_NAME)/$(provider):$(date) $(DB_ARCHIVE) --annotation org.opencontainers.image.source=$(SOURCE_REPO_URL) $(TEMP_DIR)/crane tag $(GRYPE_DB_DATA_IMAGE_NAME)/$(provider):$(date) latest @@ -228,6 +229,7 @@ upload-all-provider-cache: ci-check $(call title,Uploading existing provider data cache) @rm -f $(DB_ARCHIVE) + $(GRYPE_DB) cache status $(GRYPE_DB) cache backup -v --path $(DB_ARCHIVE) oras push -v $(GRYPE_DB_DATA_IMAGE_NAME):$(date) $(DB_ARCHIVE) --annotation org.opencontainers.image.source=$(SOURCE_REPO_URL) $(TEMP_DIR)/crane tag $(GRYPE_DB_DATA_IMAGE_NAME):$(date) latest diff --git a/cmd/grype-db/cli/commands/cache_status.go b/cmd/grype-db/cli/commands/cache_status.go index 2c658b60..3541d58d 100644 --- a/cmd/grype-db/cli/commands/cache_status.go +++ b/cmd/grype-db/cli/commands/cache_status.go @@ -6,10 +6,14 @@ import ( "time" "github.com/gookit/color" + "github.com/scylladb/go-set/strset" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/anchore/grype-db/cmd/grype-db/application" "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/log" "github.com/anchore/grype-db/pkg/provider" "github.com/anchore/grype-db/pkg/provider/entry" ) @@ -17,13 +21,24 @@ import ( var _ options.Interface = &cacheStatusConfig{} type cacheStatusConfig struct { - options.Store `yaml:"provider" json:"provider" mapstructure:"provider"` + Provider struct { + options.Store `yaml:",inline" mapstructure:",squash"` + options.Selection `yaml:",inline" mapstructure:",squash"` + } `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *cacheStatusConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Provider.Store, &o.Provider.Selection) +} + +func (o *cacheStatusConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Provider.Store, &o.Provider.Selection) } func CacheStatus(app *application.Application) *cobra.Command { - cfg := cacheStatusConfig{ - Store: options.DefaultStore(), - } + cfg := cacheStatusConfig{} + cfg.Provider.Store = options.DefaultStore() + cfg.Provider.Selection = options.DefaultSelection() cmd := &cobra.Command{ Use: "status", @@ -43,7 +58,7 @@ func CacheStatus(app *application.Application) *cobra.Command { } func cacheStatus(cfg cacheStatusConfig) error { - providerNames, err := readProviderNamesFromRoot(cfg.Store.Root) + providerNames, err := readProviderNamesFromRoot(cfg.Provider.Root) if err != nil { return err } @@ -56,8 +71,15 @@ func cacheStatus(cfg cacheStatusConfig) error { var sds []*provider.State var errs []error + allowableProviders := strset.New(cfg.Provider.IncludeFilter...) + for _, name := range providerNames { - workspace := provider.NewWorkspace(cfg.Store.Root, name) + if allowableProviders.Size() > 0 && !allowableProviders.Has(name) { + log.WithFields("provider", name).Trace("skipping...") + continue + } + + workspace := provider.NewWorkspace(cfg.Provider.Root, name) sd, err := workspace.ReadState() if err != nil { sds = append(sds, nil) @@ -75,6 +97,8 @@ func cacheStatus(cfg cacheStatusConfig) error { sds = append(sds, sd) } + success := true + for idx, sd := range sds { validMsg := "valid" isValid := true @@ -98,6 +122,8 @@ func cacheStatus(cfg cacheStatusConfig) error { } } + success = success && isValid + fmt.Printf(" • %s\n", name) statusFmt := color.HiRed if isValid { @@ -108,6 +134,11 @@ func cacheStatus(cfg cacheStatusConfig) error { fmt.Printf(" └── status: %s\n", statusFmt.Sprint(validMsg)) } + + if !success { + os.Exit(1) + } + return nil }