diff --git a/api/compose/api.go b/api/compose/api.go index 48f83c949..3014401fe 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -86,6 +86,8 @@ type BuildOptions struct { Args types.Mapping // NoCache disables cache use NoCache bool + // Quiet make the build process not output to the console + Quiet bool } // CreateOptions group options of the Create API diff --git a/cli/cmd/compose/build.go b/cli/cmd/compose/build.go index 7c250b5f1..4f5e83e70 100644 --- a/cli/cmd/compose/build.go +++ b/cli/cmd/compose/build.go @@ -97,6 +97,7 @@ func runBuild(ctx context.Context, opts buildOptions, services []string) error { Progress: opts.progress, Args: types.NewMapping(opts.args), NoCache: opts.noCache, + Quiet: opts.quiet, }) }) return err diff --git a/cli/metrics/metrics.go b/cli/metrics/metrics.go index e73bc6f83..1811e0c3e 100644 --- a/cli/metrics/metrics.go +++ b/cli/metrics/metrics.go @@ -57,6 +57,17 @@ func isCommandFlag(word string) bool { return utils.StringContains(commandFlags, word) } +// HasQuietFlag returns true if one of the arguments is `--quiet` or `-q` +func HasQuietFlag(args []string) bool { + for _, a := range args { + switch a { + case "--quiet", "-q": + return true + } + } + return false +} + // GetCommand get the invoked command func GetCommand(args []string) string { result := "" diff --git a/cli/metrics/metrics_test.go b/cli/metrics/metrics_test.go index 6223d8feb..8711a07b8 100644 --- a/cli/metrics/metrics_test.go +++ b/cli/metrics/metrics_test.go @@ -22,6 +22,36 @@ import ( "gotest.tools/v3/assert" ) +func TestHasQuietFlag(t *testing.T) { + cases := []struct { + name string + args []string + expected bool + }{ + { + name: "long flag", + args: []string{"build", "-t", "tag", "--quiet", "."}, + expected: true, + }, + { + name: "short flag", + args: []string{"build", "-t", "tag", "-q", "."}, + expected: true, + }, + { + name: "no flag", + args: []string{"build", "-t", "tag", "."}, + expected: false, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := HasQuietFlag(c.args) + assert.Equal(t, c.expected, result) + }) + } +} + func TestGetCommand(t *testing.T) { testCases := []struct { name string diff --git a/cli/mobycli/exec.go b/cli/mobycli/exec.go index d710fdb29..9fddaacad 100644 --- a/cli/mobycli/exec.go +++ b/cli/mobycli/exec.go @@ -80,7 +80,7 @@ func Exec(root *cobra.Command) { os.Exit(1) } command := metrics.GetCommand(os.Args[1:]) - if command == "build" { + if command == "build" && !metrics.HasQuietFlag(os.Args[1:]) { utils.DisplayScanSuggestMsg() } metrics.Track(store.DefaultContextType, os.Args[1:], metrics.SuccessStatus) diff --git a/local/compose/build.go b/local/compose/build.go index 8b93aa9c6..35a113f48 100644 --- a/local/compose/build.go +++ b/local/compose/build.go @@ -75,7 +75,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti err := s.build(ctx, project, opts, Containers{}, options.Progress) if err == nil { - if len(imagesToBuild) > 0 { + if len(imagesToBuild) > 0 && !options.Quiet { utils.DisplayScanSuggestMsg() } } diff --git a/local/e2e/compose/scan_message_test.go b/local/e2e/compose/scan_message_test.go index f1b252ba4..be87c68b6 100644 --- a/local/e2e/compose/scan_message_test.go +++ b/local/e2e/compose/scan_message_test.go @@ -42,19 +42,43 @@ func TestDisplayScanMessageAfterBuild(t *testing.T) { t.Run("display when docker build", func(t *testing.T) { res := c.RunDockerCmd("build", "-t", "test-image-scan-msg", "./fixtures/simple-build-test/nginx-build") - res.Assert(t, icmd.Expected{Out: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) + defer c.RunDockerCmd("rmi", "-f", "test-image-scan-msg") + res.Assert(t, icmd.Expected{Err: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) }) - t.Run("do not display if envvar DOCKER_SCAN_SUGGEST=false", func(t *testing.T) { + t.Run("do not display with docker build and quiet flag", func(t *testing.T) { + res := c.RunDockerCmd("build", "-t", "test-image-scan-msg-quiet", "--quiet", "./fixtures/simple-build-test/nginx-build") + defer c.RunDockerCmd("rmi", "-f", "test-image-scan-msg-quiet") + assert.Assert(t, !strings.Contains(res.Combined(), "docker scan")) + + res = c.RunDockerCmd("build", "-t", "test-image-scan-msg-q", "-q", "./fixtures/simple-build-test/nginx-build") + defer c.RunDockerCmd("rmi", "-f", "test-image-scan-msg-q") + assert.Assert(t, !strings.Contains(res.Combined(), "docker scan")) + }) + + t.Run("do not display if envvar DOCKER_SCAN_SUGGEST=false", func(t *testing.T) { cmd := c.NewDockerCmd("build", "-t", "test-image-scan-msg", "./fixtures/build-test/nginx-build") + defer c.RunDockerCmd("rmi", "-f", "test-image-scan-msg") cmd.Env = append(cmd.Env, "DOCKER_SCAN_SUGGEST=false") res := icmd.StartCmd(cmd) assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined()) }) t.Run("display on compose build", func(t *testing.T) { - res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "build") - res.Assert(t, icmd.Expected{Out: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) + res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test-compose-build", "build") + defer c.RunDockerCmd("rmi", "-f", "scan-msg-test-compose-build_nginx") + res.Assert(t, icmd.Expected{Err: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) + }) + + t.Run("do not display on compose build with quiet flag", func(t *testing.T) { + res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test-quiet", "build", "--quiet") + assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined()) + res = c.RunDockerCmd("rmi", "-f", "scan-msg-test-quiet_nginx") + assert.Assert(t, !strings.Contains(res.Combined(), "No such image")) + + res = c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test-q", "build", "-q") + defer c.RunDockerCmd("rmi", "-f", "scan-msg-test-q_nginx") + assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined()) }) _ = c.RunDockerOrExitError("rmi", "scan-msg-test_nginx") @@ -62,12 +86,12 @@ func TestDisplayScanMessageAfterBuild(t *testing.T) { t.Run("display on compose up if image is built", func(t *testing.T) { res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "up", "-d") defer c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "down") - res.Assert(t, icmd.Expected{Out: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) + res.Assert(t, icmd.Expected{Err: "Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them"}) }) t.Run("do not display on compose up if no image built", func(t *testing.T) { // re-run the same Compose aproject res := c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "up", "-d") - defer c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "down") + defer c.RunDockerCmd("compose", "-f", "./fixtures/simple-build-test/compose.yml", "-p", "scan-msg-test", "down", "--rmi", "all") assert.Assert(t, !strings.Contains(res.Combined(), "docker scan"), res.Combined()) }) diff --git a/utils/scan_suggest.go b/utils/scan_suggest.go index c8eb7bca6..943a9b7b9 100644 --- a/utils/scan_suggest.go +++ b/utils/scan_suggest.go @@ -39,8 +39,7 @@ func DisplayScanSuggestMsg() { if scanAlreadyInvoked() { return } - fmt.Println() - fmt.Println("Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them") + fmt.Fprintf(os.Stderr, "\nUse 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them\n") } func scanAlreadyInvoked() bool {