From 95645ed00c8624a24da12120ade2a0ede60c84bc Mon Sep 17 00:00:00 2001 From: William Murphy Date: Mon, 11 Mar 2024 15:52:24 -0400 Subject: [PATCH] Fail cache status on empty results (#257) Previously, grype-db cache status would exit zero in a few situations that should have failed validation. Specifically, if the cached results had zero rows, or if "-p some-provider" was passed, but "some-provider" no cache for "some-provider" was present on disk, the command would exit zero. Instead, exit non-zero if a provider is explicitly requested on the command line but missing, and add a --min-rows option that defaults to zero, so that providers with zero rows fail validation. If zero rows is acceptable, "--min-rows -1" can be passed. Signed-off-by: Will Murphy --- cmd/grype-db/cli/commands/cache_status.go | 65 ++++++++--- .../cli/commands/cache_status_test.go | 101 ++++++++++++++++++ 2 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 cmd/grype-db/cli/commands/cache_status_test.go diff --git a/cmd/grype-db/cli/commands/cache_status.go b/cmd/grype-db/cli/commands/cache_status.go index 3541d58d..033eb677 100644 --- a/cmd/grype-db/cli/commands/cache_status.go +++ b/cmd/grype-db/cli/commands/cache_status.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "os" + "strings" "time" "github.com/gookit/color" @@ -13,7 +14,6 @@ import ( "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" ) @@ -25,9 +25,15 @@ type cacheStatusConfig struct { options.Store `yaml:",inline" mapstructure:",squash"` options.Selection `yaml:",inline" mapstructure:",squash"` } `yaml:"provider" json:"provider" mapstructure:"provider"` + minRows int64 `yaml:"min-rows" mapstructure:"min-rows"` } func (o *cacheStatusConfig) AddFlags(flags *pflag.FlagSet) { + flags.Int64VarP( + &o.minRows, + "min-rows", "", o.minRows, + "fail validation unless more than this many rows are present in the provider results", + ) options.AddAllFlags(flags, &o.Provider.Store, &o.Provider.Selection) } @@ -63,22 +69,12 @@ func cacheStatus(cfg cacheStatusConfig) error { return err } - if len(providerNames) == 0 { - fmt.Println("no provider state cache found") - return nil - } + providerNames, missingProvidersErr := validateRequestedProviders(providerNames, cfg.Provider.IncludeFilter) var sds []*provider.State var errs []error - allowableProviders := strset.New(cfg.Provider.IncludeFilter...) - for _, name := range providerNames { - 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 { @@ -115,10 +111,13 @@ func cacheStatus(cfg cacheStatusConfig) error { if sd != nil { name = sd.Provider - count, err = entry.Count(sd.Store, sd.ResultPaths()) + counter := func() (int64, error) { + return entry.Count(sd.Store, sd.ResultPaths()) + } + count, err = validateCount(cfg, counter) if err != nil { isValid = false - validMsg = fmt.Sprintf("INVALID (unable to count entries: %s)", err.Error()) + validMsg = fmt.Sprintf("INVALID (%s)", err.Error()) } } @@ -135,6 +134,11 @@ func cacheStatus(cfg cacheStatusConfig) error { fmt.Printf(" └── status: %s\n", statusFmt.Sprint(validMsg)) } + if missingProvidersErr != nil { + success = false + fmt.Printf("INVALID (%s)\n", missingProvidersErr.Error()) + } + if !success { os.Exit(1) } @@ -142,6 +146,39 @@ func cacheStatus(cfg cacheStatusConfig) error { return nil } +func validateCount(cfg cacheStatusConfig, counter func() (int64, error)) (int64, error) { + count, err := counter() + if err != nil { + return 0, fmt.Errorf("unable to count entries: %w", err) + } + if count <= cfg.minRows { + return 0, fmt.Errorf("data has %d rows, must have more than %d", count, cfg.minRows) + } + return count, nil +} + +// validateRequestedProviders takes the set of providers found on disk, and the set of providers +// requested at the command line. It returns the subset of providers on disk that were requested. +// If providers were requested that are not present on disk, it returns an error. +// If no providers are explicitly requested, it returns the entire set. +func validateRequestedProviders(providersOnDisk []string, requestedProviders []string) ([]string, error) { + if len(requestedProviders) == 0 { + return providersOnDisk, nil + } + var result []string + requestedSet := strset.New(requestedProviders...) + for _, p := range providersOnDisk { + if requestedSet.Has(p) { + result = append(result, p) + requestedSet.Remove(p) + } + } + if requestedSet.Size() > 0 { + return nil, fmt.Errorf("providers requested but not present on disk: %s", strings.Join(requestedSet.List(), ", ")) + } + return result, nil +} + func readProviderNamesFromRoot(root string) ([]string, error) { // list all the directories in "root" listing, err := os.ReadDir(root) diff --git a/cmd/grype-db/cli/commands/cache_status_test.go b/cmd/grype-db/cli/commands/cache_status_test.go new file mode 100644 index 00000000..65990fdd --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_status_test.go @@ -0,0 +1,101 @@ +package commands + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_validateCount(t *testing.T) { + tests := []struct { + name string + cfg cacheStatusConfig + counter func() (int64, error) + want int64 + wantErr assert.ErrorAssertionFunc + }{ + { + name: "empty count passes when min-rows is -1", + cfg: cacheStatusConfig{minRows: -1}, + counter: func() (int64, error) { + return 0, nil + }, + }, + { + name: "empty count fails when min-rows is 0", + cfg: cacheStatusConfig{minRows: 0}, + counter: func() (int64, error) { + return 0, nil + }, + wantErr: assert.Error, + }, + { + name: "large count passes when min-rows is less", + cfg: cacheStatusConfig{minRows: 12}, + counter: func() (int64, error) { + return 13, nil + }, + want: 13, + }, + { + name: "error is reported when counter returns error", + counter: func() (int64, error) { + return 0, fmt.Errorf("could not count records") + }, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = assert.NoError + } + count, err := validateCount(tt.cfg, tt.counter) + if !tt.wantErr(t, err) { + return + } + assert.Equal(t, tt.want, count) + }) + } +} + +func Test_validateRequestedProviders(t *testing.T) { + tests := []struct { + name string + providersOnDisk []string + requestedProviders []string + want []string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "no requested providers means on disk state is ok", + providersOnDisk: []string{"alpine", "some-provider", "void-linux", "gentoo"}, + want: []string{"alpine", "some-provider", "void-linux", "gentoo"}, + }, + { + name: "requesting subset of providers works", + providersOnDisk: []string{"alpine", "some-provider", "void-linux", "gentoo"}, + requestedProviders: []string{"alpine", "void-linux"}, + want: []string{"alpine", "void-linux"}, + }, + { + name: "requesting missing provider result in error", + providersOnDisk: []string{"alpine", "some-provider", "void-linux", "gentoo"}, + requestedProviders: []string{"alpine", "void-linux", "missing"}, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateRequestedProviders(tt.providersOnDisk, tt.requestedProviders) + if tt.wantErr == nil { + tt.wantErr = assert.NoError + } + if !tt.wantErr(t, err, fmt.Sprintf("validateRequestedProviders(%v, %v)", tt.providersOnDisk, tt.requestedProviders)) { + return + } + assert.Equalf(t, tt.want, got, "validateRequestedProviders(%v, %v)", tt.providersOnDisk, tt.requestedProviders) + }) + } +}