Skip to content

Commit

Permalink
Fail cache status on empty results (#257)
Browse files Browse the repository at this point in the history
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 <will.murphy@anchore.com>
  • Loading branch information
willmurphyscode committed Mar 11, 2024
1 parent 08e0071 commit 95645ed
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 14 deletions.
65 changes: 51 additions & 14 deletions cmd/grype-db/cli/commands/cache_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os"
"strings"
"time"

"github.com/gookit/color"
Expand All @@ -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"
)
Expand All @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
}
}

Expand All @@ -135,13 +134,51 @@ 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)
}

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)
Expand Down
101 changes: 101 additions & 0 deletions cmd/grype-db/cli/commands/cache_status_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit 95645ed

Please sign in to comment.