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:
Dan Luhring 2021-10-20 09:50:59 -04:00 committed by GitHub
parent 637a061532
commit 19a513a42a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 35 deletions

View file

@ -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
View 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, " "))
}
})
}
}

View file

@ -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())
}

View file

@ -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())
}

View 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)
}
}

View file

@ -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)
}
}