mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
Silence usage and errors on root command (#462)
* Silence usage and errors on root command Signed-off-by: Dan Luhring <dan.luhring@anchore.com> * show help when no args are given Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove comments Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add cli test for help behavior Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
637a061532
commit
19a513a42a
6 changed files with 115 additions and 35 deletions
17
cmd/root.go
17
cmd/root.go
|
@ -42,8 +42,9 @@ var ignoreNonFixedMatches = []match.IgnoreRule{
|
|||
var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName),
|
||||
Short: "A vulnerability scanner for container images and filesystems",
|
||||
Long: format.Tprintf(`
|
||||
Short: "A vulnerability scanner for container images, filesystems, and SBOMs",
|
||||
Long: format.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
||||
{{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
||||
|
@ -63,7 +64,9 @@ You can also pipe in Syft JSON directly:
|
|||
`, map[string]interface{}{
|
||||
"appName": internal.ApplicationName,
|
||||
}),
|
||||
Args: validateRootArgs,
|
||||
Args: validateRootArgs,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if appConfig.Dev.ProfileCPU {
|
||||
defer profile.Start(profile.CPUProfile).Stop()
|
||||
|
@ -275,10 +278,12 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
}
|
||||
|
||||
func validateRootArgs(cmd *cobra.Command, args []string) error {
|
||||
// the user must specify at least one argument OR wait for input on stdin IF it is a pipe
|
||||
if len(args) == 0 && !internal.IsPipedInput() {
|
||||
// return an error with no message for the user, which will implicitly show the help text (but no specific error)
|
||||
return fmt.Errorf("")
|
||||
// in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code.
|
||||
if err := cmd.Help(); err != nil {
|
||||
return fmt.Errorf("unable to display help: %w", err)
|
||||
}
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
return cobra.MaximumNArgs(1)(cmd, args)
|
||||
|
|
39
test/cli/cmd_test.go
Normal file
39
test/cli/cmd_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
env map[string]string
|
||||
assertions []traitAssertion
|
||||
}{
|
||||
{
|
||||
name: "no-args-shows-help",
|
||||
args: []string{},
|
||||
assertions: []traitAssertion{
|
||||
assertInOutput("an image/directory argument is required"), // specific error that should be shown
|
||||
assertInOutput("A vulnerability scanner for container images, filesystems, and SBOMs"), // excerpt from help description
|
||||
assertFailingReturnCode,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd, stdout, stderr := runGrype(t, test.env, test.args...)
|
||||
for _, traitFn := range test.assertions {
|
||||
traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
if t.Failed() {
|
||||
t.Log("STDOUT:\n", stdout)
|
||||
t.Log("STDERR:\n", stderr)
|
||||
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ func TestJsonDescriptor(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd, stdout, stderr := runGrypeCommand(t, test.env, test.args...)
|
||||
cmd, stdout, stderr := runGrype(t, test.env, test.args...)
|
||||
for _, traitAssertionFn := range test.assertions {
|
||||
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func TestRegistryAuth(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd, stdout, stderr := runGrypeCommand(t, test.env, test.args...)
|
||||
cmd, stdout, stderr := runGrype(t, test.env, test.args...)
|
||||
for _, traitAssertionFn := range test.assertions {
|
||||
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||
}
|
||||
|
|
27
test/cli/trait_assertions_test.go
Normal file
27
test/cli/trait_assertions_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
)
|
||||
|
||||
type traitAssertion func(tb testing.TB, stdout, stderr string, rc int)
|
||||
|
||||
func assertInOutput(data string) traitAssertion {
|
||||
return func(tb testing.TB, stdout, stderr string, _ int) {
|
||||
tb.Helper()
|
||||
|
||||
if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) {
|
||||
tb.Errorf("data=%q was NOT found in any output, but should have been there", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertFailingReturnCode(tb testing.TB, _, _ string, rc int) {
|
||||
tb.Helper()
|
||||
if rc == 0 {
|
||||
tb.Errorf("expected a failure but got rc=%d", rc)
|
||||
}
|
||||
}
|
|
@ -12,26 +12,19 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
)
|
||||
|
||||
type traitAssertion func(tb testing.TB, stdout, stderr string, rc int)
|
||||
func getFixtureImage(tb testing.TB, fixtureImageName string) string {
|
||||
tb.Helper()
|
||||
|
||||
func assertInOutput(data string) traitAssertion {
|
||||
return func(tb testing.TB, stdout, stderr string, _ int) {
|
||||
if !strings.Contains(stripansi.Strip(stderr), data) && !strings.Contains(stripansi.Strip(stdout), data) {
|
||||
tb.Errorf("data=%q was NOT found in any output, but should have been there", data)
|
||||
}
|
||||
}
|
||||
imagetest.GetFixtureImage(tb, "docker-archive", fixtureImageName)
|
||||
return imagetest.GetFixtureImageTarPath(tb, fixtureImageName)
|
||||
}
|
||||
|
||||
func getFixtureImage(t testing.TB, fixtureImageName string) string {
|
||||
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
||||
return imagetest.GetFixtureImageTarPath(t, fixtureImageName)
|
||||
}
|
||||
func getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd {
|
||||
tb.Helper()
|
||||
|
||||
func getGrypeCommand(t testing.TB, args ...string) *exec.Cmd {
|
||||
var binaryLocation string
|
||||
if os.Getenv("GRYPE_BINARY_LOCATION") != "" {
|
||||
// GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary
|
||||
|
@ -40,11 +33,11 @@ func getGrypeCommand(t testing.TB, args ...string) *exec.Cmd {
|
|||
// note: there is a subtle - vs _ difference between these versions
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/grype-macos_darwin_%s/grype", runtime.GOARCH))
|
||||
binaryLocation = path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype-macos_darwin_%s/grype", runtime.GOARCH))
|
||||
case "linux":
|
||||
binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/grype_linux_%s/grype", runtime.GOARCH))
|
||||
binaryLocation = path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype_linux_%s/grype", runtime.GOARCH))
|
||||
default:
|
||||
t.Fatalf("unsupported OS: %s", runtime.GOOS)
|
||||
tb.Fatalf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -57,8 +50,22 @@ func getGrypeCommand(t testing.TB, args ...string) *exec.Cmd {
|
|||
)
|
||||
}
|
||||
|
||||
func runGrypeCommand(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
|
||||
cmd := getGrypeCommand(t, args...)
|
||||
func runGrype(tb testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
|
||||
tb.Helper()
|
||||
|
||||
cmd := getGrypeCommand(tb, args...)
|
||||
if env == nil {
|
||||
env = make(map[string]string)
|
||||
}
|
||||
|
||||
// we should not have tests reaching out for app update checks
|
||||
env["GRYPE_CHECK_FOR_APP_UPDATE"] = "false"
|
||||
|
||||
stdout, stderr := runCommand(cmd, env)
|
||||
return cmd, stdout, stderr
|
||||
}
|
||||
|
||||
func runCommand(cmd *exec.Cmd, env map[string]string) (string, string) {
|
||||
if env != nil {
|
||||
cmd.Env = append(os.Environ(), envMapToSlice(env)...)
|
||||
}
|
||||
|
@ -69,7 +76,7 @@ func runGrypeCommand(t testing.TB, env map[string]string, args ...string) (*exec
|
|||
// ignore errors since this may be what the test expects
|
||||
cmd.Run()
|
||||
|
||||
return cmd, stdout.String(), stderr.String()
|
||||
return stdout.String(), stderr.String()
|
||||
}
|
||||
|
||||
func envMapToSlice(env map[string]string) (envList []string) {
|
||||
|
@ -82,32 +89,34 @@ func envMapToSlice(env map[string]string) (envList []string) {
|
|||
return
|
||||
}
|
||||
|
||||
func repoRoot(t testing.TB) string {
|
||||
t.Helper()
|
||||
func repoRoot(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find repo root dir: %+v", err)
|
||||
tb.Fatalf("unable to find repo root dir: %+v", err)
|
||||
}
|
||||
absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
|
||||
if err != nil {
|
||||
t.Fatal("unable to get abs path to repo root:", err)
|
||||
tb.Fatal("unable to get abs path to repo root:", err)
|
||||
}
|
||||
return absRepoRoot
|
||||
}
|
||||
|
||||
func attachFileToCommandStdin(t testing.TB, file io.Reader, command *exec.Cmd) {
|
||||
func attachFileToCommandStdin(tb testing.TB, file io.Reader, command *exec.Cmd) {
|
||||
tb.Helper()
|
||||
|
||||
stdin, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(stdin, file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
err = stdin.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue