Browse code

Change the quiet flag behavior in the build command

Right now, the quiet (-q, --quiet) flag ignores the output
generated from within the container.

However, it ought to be quiet in a way that all kind
of diagnostic output should be ignored, unless the build
process fails.

This patch makes the quiet flag behave in the following way:
1. If the build process succeeds, stdout contains the image ID
and stderr is empty.
2. If the build process fails, stdout is empty and stderr
has the error message and the diagnostic output of that process.

If the quiet flag is not set, then everything goes to stdout
and error messages, if there are any, go to stderr.

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>

Boaz Shuster authored on 2015/10/28 09:29:21
Showing 8 changed files
... ...
@@ -3,6 +3,7 @@ package client
3 3
 import (
4 4
 	"archive/tar"
5 5
 	"bufio"
6
+	"bytes"
6 7
 	"fmt"
7 8
 	"io"
8 9
 	"io/ioutil"
... ...
@@ -42,7 +43,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
42 42
 	cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true)
43 43
 	flTags := opts.NewListOpts(validateTag)
44 44
 	cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format")
45
-	suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers")
45
+	suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the build output and print image ID on success")
46 46
 	noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image")
47 47
 	rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build")
48 48
 	forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers")
... ...
@@ -87,20 +88,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
87 87
 		contextDir    string
88 88
 		tempDir       string
89 89
 		relDockerfile string
90
+		progBuff      io.Writer
91
+		buildBuff     io.Writer
90 92
 	)
91 93
 
94
+	progBuff = cli.out
95
+	buildBuff = cli.out
96
+	if *suppressOutput {
97
+		progBuff = bytes.NewBuffer(nil)
98
+		buildBuff = bytes.NewBuffer(nil)
99
+	}
100
+
92 101
 	switch {
93 102
 	case specifiedContext == "-":
94 103
 		tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName)
95 104
 	case urlutil.IsGitURL(specifiedContext) && hasGit:
96 105
 		tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName)
97 106
 	case urlutil.IsURL(specifiedContext):
98
-		tempDir, relDockerfile, err = getContextFromURL(cli.out, specifiedContext, *dockerfileName)
107
+		tempDir, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName)
99 108
 	default:
100 109
 		contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName)
101 110
 	}
102 111
 
103 112
 	if err != nil {
113
+		if *suppressOutput && urlutil.IsURL(specifiedContext) {
114
+			fmt.Fprintln(cli.err, progBuff)
115
+		}
104 116
 		return fmt.Errorf("unable to prepare context: %s", err)
105 117
 	}
106 118
 
... ...
@@ -169,7 +182,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
169 169
 	context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile)
170 170
 
171 171
 	// Setup an upload progress bar
172
-	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(cli.out, true)
172
+	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
173 173
 
174 174
 	var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon")
175 175
 
... ...
@@ -230,13 +243,16 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
230 230
 		return err
231 231
 	}
232 232
 
233
-	err = jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut)
233
+	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut)
234 234
 	if err != nil {
235 235
 		if jerr, ok := err.(*jsonmessage.JSONError); ok {
236 236
 			// If no error code is set, default to 1
237 237
 			if jerr.Code == 0 {
238 238
 				jerr.Code = 1
239 239
 			}
240
+			if *suppressOutput {
241
+				fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff)
242
+			}
240 243
 			return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
241 244
 		}
242 245
 	}
... ...
@@ -246,6 +262,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
246 246
 		fmt.Fprintln(cli.err, `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`)
247 247
 	}
248 248
 
249
+	// Everything worked so if -q was provided the output from the daemon
250
+	// should be just the image ID and we'll print that to stdout.
251
+	if *suppressOutput {
252
+		fmt.Fprintf(cli.out, "%s", buildBuff)
253
+	}
249 254
 	// Since the build was successful, now we must tag any of the resolved
250 255
 	// images from the above Dockerfile rewrite.
251 256
 	for _, resolved := range resolvedTags {
... ...
@@ -1,6 +1,7 @@
1 1
 package build
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"encoding/base64"
5 6
 	"encoding/json"
6 7
 	"errors"
... ...
@@ -72,6 +73,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
72 72
 		authConfigs        = map[string]types.AuthConfig{}
73 73
 		authConfigsEncoded = r.Header.Get("X-Registry-Config")
74 74
 		buildConfig        = &dockerfile.Config{}
75
+		notVerboseBuffer   = bytes.NewBuffer(nil)
75 76
 	)
76 77
 
77 78
 	if authConfigsEncoded != "" {
... ...
@@ -90,6 +92,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
90 90
 	defer output.Close()
91 91
 	sf := streamformatter.NewJSONStreamFormatter()
92 92
 	errf := func(err error) error {
93
+		if !buildConfig.Verbose && notVerboseBuffer.Len() > 0 {
94
+			output.Write(notVerboseBuffer.Bytes())
95
+		}
93 96
 		// Do not write the error in the http output if it's still empty.
94 97
 		// This prevents from writing a 200(OK) when there is an internal error.
95 98
 		if !output.Flushed() {
... ...
@@ -170,6 +175,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
170 170
 	// Look at code in DetectContextFromRemoteURL for more information.
171 171
 	createProgressReader := func(in io.ReadCloser) io.ReadCloser {
172 172
 		progressOutput := sf.NewProgressOutput(output, true)
173
+		if !buildConfig.Verbose {
174
+			progressOutput = sf.NewProgressOutput(notVerboseBuffer, true)
175
+		}
173 176
 		return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL)
174 177
 	}
175 178
 
... ...
@@ -199,6 +207,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
199 199
 		AuthConfigs: authConfigs,
200 200
 		Archiver:    defaultArchiver,
201 201
 	}
202
+	if !buildConfig.Verbose {
203
+		docker.OutOld = notVerboseBuffer
204
+	}
202 205
 
203 206
 	b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{ModifiableContext: context}, nil)
204 207
 	if err != nil {
... ...
@@ -206,6 +217,10 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
206 206
 	}
207 207
 	b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
208 208
 	b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf}
209
+	if !buildConfig.Verbose {
210
+		b.Stdout = &streamformatter.StdoutFormatter{Writer: notVerboseBuffer, StreamFormatter: sf}
211
+		b.Stderr = &streamformatter.StderrFormatter{Writer: notVerboseBuffer, StreamFormatter: sf}
212
+	}
209 213
 
210 214
 	if closeNotifier, ok := w.(http.CloseNotifier); ok {
211 215
 		finished := make(chan struct{})
... ...
@@ -235,5 +250,12 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
235 235
 		}
236 236
 	}
237 237
 
238
+	// Everything worked so if -q was provided the output from the daemon
239
+	// should be just the image ID and we'll print that to stdout.
240
+	if !buildConfig.Verbose {
241
+		stdout := &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
242
+		fmt.Fprintf(stdout, "%s\n", string(imgID))
243
+	}
244
+
238 245
 	return nil
239 246
 }
... ...
@@ -524,11 +524,9 @@ func (b *Builder) create() (string, error) {
524 524
 
525 525
 func (b *Builder) run(cID string) (err error) {
526 526
 	errCh := make(chan error)
527
-	if b.Verbose {
528
-		go func() {
529
-			errCh <- b.docker.ContainerAttach(cID, nil, b.Stdout, b.Stderr, true)
530
-		}()
531
-	}
527
+	go func() {
528
+		errCh <- b.docker.ContainerAttach(cID, nil, b.Stdout, b.Stderr, true)
529
+	}()
532 530
 
533 531
 	finished := make(chan struct{})
534 532
 	defer close(finished)
... ...
@@ -546,11 +544,9 @@ func (b *Builder) run(cID string) (err error) {
546 546
 		return err
547 547
 	}
548 548
 
549
-	if b.Verbose {
550
-		// Block on reading output from container, stop on err or chan closed
551
-		if err := <-errCh; err != nil {
552
-			return err
553
-		}
549
+	// Block on reading output from container, stop on err or chan closed
550
+	if err := <-errCh; err != nil {
551
+		return err
554 552
 	}
555 553
 
556 554
 	if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 {
... ...
@@ -95,7 +95,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d '
95 95
 complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage'
96 96
 complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l no-cache -d 'Do not use cache when building the image'
97 97
 complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l pull -d 'Always attempt to pull a newer version of the image'
98
-complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s q -l quiet -d 'Suppress the verbose output generated by the containers'
98
+complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s q -l quiet -d 'Suppress the build output and print image ID on success'
99 99
 complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l rm -d 'Remove intermediate containers after a successful build'
100 100
 complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s t -l tag -d 'Repository name (and optionally a tag) to be applied to the resulting image in case of success'
101 101
 
... ...
@@ -30,7 +30,7 @@ parent = "smn_cli"
30 30
       --memory-swap=""                Total memory (memory + swap), `-1` to disable swap
31 31
       --no-cache=false                Do not use cache when building the image
32 32
       --pull=false                    Always attempt to pull a newer version of the image
33
-      -q, --quiet=false               Suppress the verbose output generated by the containers
33
+      -q, --quiet=false               Suppress the build output and print image ID on success
34 34
       --rm=true                       Remove intermediate containers after a successful build
35 35
       --shm-size=[]                   Size of `/dev/shm`. The format is `<number><unit>`. `number` must be greater than `0`.  Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). If you omit the unit, the system uses bytes. If you omit the size entirely, the system uses `64m`.
36 36
       -t, --tag=[]                    Name and optionally a tag in the 'name:tag' format
... ...
@@ -4941,6 +4941,110 @@ func (s *DockerSuite) TestBuildLabelsCache(c *check.C) {
4941 4941
 
4942 4942
 }
4943 4943
 
4944
+func (s *DockerSuite) TestBuildNotVerboseSuccess(c *check.C) {
4945
+	testRequires(c, DaemonIsLinux)
4946
+	// This test makes sure that -q works correctly when build is successful:
4947
+	// stdout has only the image ID (long image ID) and stderr is empty.
4948
+	var stdout, stderr string
4949
+	var err error
4950
+	outRegexp := regexp.MustCompile("^(sha256:|)[a-z0-9]{64}\\n$")
4951
+
4952
+	tt := []struct {
4953
+		Name      string
4954
+		BuildFunc func(string)
4955
+	}{
4956
+		{
4957
+			Name: "quiet_build_stdin_success",
4958
+			BuildFunc: func(name string) {
4959
+				_, stdout, stderr, err = buildImageWithStdoutStderr(name, "FROM busybox", true, "-q", "--force-rm", "--rm")
4960
+			},
4961
+		},
4962
+		{
4963
+			Name: "quiet_build_ctx_success",
4964
+			BuildFunc: func(name string) {
4965
+				ctx, err := fakeContext("FROM busybox", map[string]string{
4966
+					"quiet_build_success_fctx": "test",
4967
+				})
4968
+				if err != nil {
4969
+					c.Fatalf("Failed to create context: %s", err.Error())
4970
+				}
4971
+				defer ctx.Close()
4972
+				_, stdout, stderr, err = buildImageFromContextWithStdoutStderr(name, ctx, true, "-q", "--force-rm", "--rm")
4973
+			},
4974
+		},
4975
+		{
4976
+			Name: "quiet_build_git_success",
4977
+			BuildFunc: func(name string) {
4978
+				git, err := newFakeGit("repo", map[string]string{
4979
+					"Dockerfile": "FROM busybox",
4980
+				}, true)
4981
+				if err != nil {
4982
+					c.Fatalf("Failed to create the git repo: %s", err.Error())
4983
+				}
4984
+				defer git.Close()
4985
+				_, stdout, stderr, err = buildImageFromGitWithStdoutStderr(name, git, true, "-q", "--force-rm", "--rm")
4986
+
4987
+			},
4988
+		},
4989
+	}
4990
+
4991
+	for _, te := range tt {
4992
+		te.BuildFunc(te.Name)
4993
+		if err != nil {
4994
+			c.Fatalf("Test %s shouldn't fail, but got the following error: %s", te.Name, err.Error())
4995
+		}
4996
+		if outRegexp.Find([]byte(stdout)) == nil {
4997
+			c.Fatalf("Test %s expected stdout to match the [%v] regexp, but it is [%v]", te.Name, outRegexp, stdout)
4998
+		}
4999
+		if stderr != "" {
5000
+			c.Fatalf("Test %s expected stderr to be empty, but it is [%#v]", te.Name, stderr)
5001
+		}
5002
+	}
5003
+
5004
+}
5005
+
5006
+func (s *DockerSuite) TestBuildNotVerboseFailure(c *check.C) {
5007
+	testRequires(c, DaemonIsLinux)
5008
+	// This test makes sure that -q works correctly when build fails by
5009
+	// comparing between the stderr output in quiet mode and in stdout
5010
+	// and stderr output in verbose mode
5011
+	tt := []struct {
5012
+		TestName  string
5013
+		BuildCmds string
5014
+	}{
5015
+		{"quiet_build_no_from_at_the_beginning", "RUN whoami"},
5016
+		{"quiet_build_unknown_instr", "FROMD busybox"},
5017
+		{"quiet_build_not_exists_image", "FROM busybox11"},
5018
+	}
5019
+
5020
+	for _, te := range tt {
5021
+		_, _, qstderr, qerr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "-q", "--force-rm", "--rm")
5022
+		_, vstdout, vstderr, verr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "--force-rm", "--rm")
5023
+		if verr == nil || qerr == nil {
5024
+			c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", te.TestName))
5025
+		}
5026
+		if qstderr != vstdout+vstderr {
5027
+			c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", te.TestName, qstderr, vstdout))
5028
+		}
5029
+	}
5030
+}
5031
+
5032
+func (s *DockerSuite) TestBuildNotVerboseFailureRemote(c *check.C) {
5033
+	testRequires(c, DaemonIsLinux)
5034
+	// This test ensures that when given a wrong URL, stderr in quiet mode and
5035
+	// stdout and stderr in verbose mode are identical.
5036
+	URL := "http://bla.bla.com"
5037
+	Name := "quiet_build_wrong_remote"
5038
+	_, _, qstderr, qerr := buildImageWithStdoutStderr(Name, "", false, "-q", "--force-rm", "--rm", URL)
5039
+	_, vstdout, vstderr, verr := buildImageWithStdoutStderr(Name, "", false, "--force-rm", "--rm", URL)
5040
+	if qerr == nil || verr == nil {
5041
+		c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", Name))
5042
+	}
5043
+	if qstderr != vstdout+vstderr {
5044
+		c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", Name, qstderr, vstdout))
5045
+	}
5046
+}
5047
+
4944 5048
 func (s *DockerSuite) TestBuildStderr(c *check.C) {
4945 5049
 	testRequires(c, DaemonIsLinux)
4946 5050
 	// This test just makes sure that no non-error output goes
... ...
@@ -5589,34 +5693,6 @@ func (s *DockerSuite) TestBuildDotDotFile(c *check.C) {
5589 5589
 	}
5590 5590
 }
5591 5591
 
5592
-func (s *DockerSuite) TestBuildNotVerbose(c *check.C) {
5593
-	testRequires(c, DaemonIsLinux)
5594
-	ctx, err := fakeContext("FROM busybox\nENV abc=hi\nRUN echo $abc there", map[string]string{})
5595
-	if err != nil {
5596
-		c.Fatal(err)
5597
-	}
5598
-	defer ctx.Close()
5599
-
5600
-	// First do it w/verbose - baseline
5601
-	out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "-t", "verbose", ".")
5602
-	if err != nil {
5603
-		c.Fatalf("failed to build the image w/o -q: %s, %v", out, err)
5604
-	}
5605
-	if !strings.Contains(out, "hi there") {
5606
-		c.Fatalf("missing output:%s\n", out)
5607
-	}
5608
-
5609
-	// Now do it w/o verbose
5610
-	out, _, err = dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "-q", "-t", "verbose", ".")
5611
-	if err != nil {
5612
-		c.Fatalf("failed to build the image w/ -q: %s, %v", out, err)
5613
-	}
5614
-	if strings.Contains(out, "hi there") {
5615
-		c.Fatalf("Bad output, should not contain 'hi there':%s", out)
5616
-	}
5617
-
5618
-}
5619
-
5620 5592
 func (s *DockerSuite) TestBuildRUNoneJSON(c *check.C) {
5621 5593
 	testRequires(c, DaemonIsLinux)
5622 5594
 	name := "testbuildrunonejson"
... ...
@@ -1308,6 +1308,47 @@ func buildImageFromContextWithOut(name string, ctx *FakeContext, useCache bool,
1308 1308
 	return id, out, nil
1309 1309
 }
1310 1310
 
1311
+func buildImageFromContextWithStdoutStderr(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, string, string, error) {
1312
+	args := []string{"build", "-t", name}
1313
+	if !useCache {
1314
+		args = append(args, "--no-cache")
1315
+	}
1316
+	args = append(args, buildFlags...)
1317
+	args = append(args, ".")
1318
+	buildCmd := exec.Command(dockerBinary, args...)
1319
+	buildCmd.Dir = ctx.Dir
1320
+
1321
+	stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
1322
+	if err != nil || exitCode != 0 {
1323
+		return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
1324
+	}
1325
+	id, err := getIDByName(name)
1326
+	if err != nil {
1327
+		return "", stdout, stderr, err
1328
+	}
1329
+	return id, stdout, stderr, nil
1330
+}
1331
+
1332
+func buildImageFromGitWithStdoutStderr(name string, ctx *fakeGit, useCache bool, buildFlags ...string) (string, string, string, error) {
1333
+	args := []string{"build", "-t", name}
1334
+	if !useCache {
1335
+		args = append(args, "--no-cache")
1336
+	}
1337
+	args = append(args, buildFlags...)
1338
+	args = append(args, ctx.RepoURL)
1339
+	buildCmd := exec.Command(dockerBinary, args...)
1340
+
1341
+	stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
1342
+	if err != nil || exitCode != 0 {
1343
+		return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
1344
+	}
1345
+	id, err := getIDByName(name)
1346
+	if err != nil {
1347
+		return "", stdout, stderr, err
1348
+	}
1349
+	return id, stdout, stderr, nil
1350
+}
1351
+
1311 1352
 func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) {
1312 1353
 	args := []string{"build", "-t", name}
1313 1354
 	if !useCache {
... ...
@@ -81,7 +81,7 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
81 81
    Always attempt to pull a newer version of the image. The default is *false*.
82 82
 
83 83
 **-q**, **--quiet**=*true*|*false*
84
-   Suppress the verbose output generated by the containers. The default is *false*.
84
+   Suppress the build output and print image ID on success. The default is *false*.
85 85
 
86 86
 **--rm**=*true*|*false*
87 87
    Remove intermediate containers after a successful build. The default is *true*.