1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

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>
This commit is contained in:
Boaz Shuster 2015-10-28 02:29:21 +02:00 committed by ripcurld00d
parent 67620bc028
commit 60b4db7eb1
8 changed files with 201 additions and 45 deletions

View file

@ -3,6 +3,7 @@ package client
import ( import (
"archive/tar" "archive/tar"
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -42,7 +43,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true)
flTags := opts.NewListOpts(validateTag) flTags := opts.NewListOpts(validateTag)
cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format")
suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the build output and print image ID on success")
noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image") noCache := cmd.Bool([]string{"-no-cache"}, false, "Do not use cache when building the image")
rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build") rm := cmd.Bool([]string{"-rm"}, true, "Remove intermediate containers after a successful build")
forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers") forceRm := cmd.Bool([]string{"-force-rm"}, false, "Always remove intermediate containers")
@ -87,20 +88,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
contextDir string contextDir string
tempDir string tempDir string
relDockerfile string relDockerfile string
progBuff io.Writer
buildBuff io.Writer
) )
progBuff = cli.out
buildBuff = cli.out
if *suppressOutput {
progBuff = bytes.NewBuffer(nil)
buildBuff = bytes.NewBuffer(nil)
}
switch { switch {
case specifiedContext == "-": case specifiedContext == "-":
tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName) tempDir, relDockerfile, err = getContextFromReader(cli.in, *dockerfileName)
case urlutil.IsGitURL(specifiedContext) && hasGit: case urlutil.IsGitURL(specifiedContext) && hasGit:
tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName) tempDir, relDockerfile, err = getContextFromGitURL(specifiedContext, *dockerfileName)
case urlutil.IsURL(specifiedContext): case urlutil.IsURL(specifiedContext):
tempDir, relDockerfile, err = getContextFromURL(cli.out, specifiedContext, *dockerfileName) tempDir, relDockerfile, err = getContextFromURL(progBuff, specifiedContext, *dockerfileName)
default: default:
contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName) contextDir, relDockerfile, err = getContextFromLocalDir(specifiedContext, *dockerfileName)
} }
if err != nil { if err != nil {
if *suppressOutput && urlutil.IsURL(specifiedContext) {
fmt.Fprintln(cli.err, progBuff)
}
return fmt.Errorf("unable to prepare context: %s", err) return fmt.Errorf("unable to prepare context: %s", err)
} }
@ -169,7 +182,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile) context = replaceDockerfileTarWrapper(context, newDockerfile, relDockerfile)
// Setup an upload progress bar // Setup an upload progress bar
progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(cli.out, true) progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon") var body io.Reader = progress.NewProgressReader(context, progressOutput, 0, "", "Sending build context to Docker daemon")
@ -230,13 +243,16 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
return err return err
} }
err = jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut) err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut)
if err != nil { if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok { if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1 // If no error code is set, default to 1
if jerr.Code == 0 { if jerr.Code == 0 {
jerr.Code = 1 jerr.Code = 1
} }
if *suppressOutput {
fmt.Fprintf(cli.err, "%s%s", progBuff, buildBuff)
}
return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} return Cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
} }
} }
@ -246,6 +262,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
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.`) 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.`)
} }
// Everything worked so if -q was provided the output from the daemon
// should be just the image ID and we'll print that to stdout.
if *suppressOutput {
fmt.Fprintf(cli.out, "%s", buildBuff)
}
// Since the build was successful, now we must tag any of the resolved // Since the build was successful, now we must tag any of the resolved
// images from the above Dockerfile rewrite. // images from the above Dockerfile rewrite.
for _, resolved := range resolvedTags { for _, resolved := range resolvedTags {

View file

@ -1,6 +1,7 @@
package build package build
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@ -72,6 +73,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
authConfigs = map[string]types.AuthConfig{} authConfigs = map[string]types.AuthConfig{}
authConfigsEncoded = r.Header.Get("X-Registry-Config") authConfigsEncoded = r.Header.Get("X-Registry-Config")
buildConfig = &dockerfile.Config{} buildConfig = &dockerfile.Config{}
notVerboseBuffer = bytes.NewBuffer(nil)
) )
if authConfigsEncoded != "" { if authConfigsEncoded != "" {
@ -90,6 +92,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
defer output.Close() defer output.Close()
sf := streamformatter.NewJSONStreamFormatter() sf := streamformatter.NewJSONStreamFormatter()
errf := func(err error) error { errf := func(err error) error {
if !buildConfig.Verbose && notVerboseBuffer.Len() > 0 {
output.Write(notVerboseBuffer.Bytes())
}
// Do not write the error in the http output if it's still empty. // Do not write the error in the http output if it's still empty.
// This prevents from writing a 200(OK) when there is an internal error. // This prevents from writing a 200(OK) when there is an internal error.
if !output.Flushed() { if !output.Flushed() {
@ -170,6 +175,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
// Look at code in DetectContextFromRemoteURL for more information. // Look at code in DetectContextFromRemoteURL for more information.
createProgressReader := func(in io.ReadCloser) io.ReadCloser { createProgressReader := func(in io.ReadCloser) io.ReadCloser {
progressOutput := sf.NewProgressOutput(output, true) progressOutput := sf.NewProgressOutput(output, true)
if !buildConfig.Verbose {
progressOutput = sf.NewProgressOutput(notVerboseBuffer, true)
}
return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL) return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL)
} }
@ -199,6 +207,9 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
AuthConfigs: authConfigs, AuthConfigs: authConfigs,
Archiver: defaultArchiver, Archiver: defaultArchiver,
} }
if !buildConfig.Verbose {
docker.OutOld = notVerboseBuffer
}
b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{ModifiableContext: context}, nil) b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{ModifiableContext: context}, nil)
if err != nil { if err != nil {
@ -206,6 +217,10 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
} }
b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf} b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf} b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf}
if !buildConfig.Verbose {
b.Stdout = &streamformatter.StdoutFormatter{Writer: notVerboseBuffer, StreamFormatter: sf}
b.Stderr = &streamformatter.StderrFormatter{Writer: notVerboseBuffer, StreamFormatter: sf}
}
if closeNotifier, ok := w.(http.CloseNotifier); ok { if closeNotifier, ok := w.(http.CloseNotifier); ok {
finished := make(chan struct{}) finished := make(chan struct{})
@ -235,5 +250,12 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
} }
} }
// Everything worked so if -q was provided the output from the daemon
// should be just the image ID and we'll print that to stdout.
if !buildConfig.Verbose {
stdout := &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
fmt.Fprintf(stdout, "%s\n", string(imgID))
}
return nil return nil
} }

View file

@ -524,11 +524,9 @@ func (b *Builder) create() (string, error) {
func (b *Builder) run(cID string) (err error) { func (b *Builder) run(cID string) (err error) {
errCh := make(chan error) errCh := make(chan error)
if b.Verbose { go func() {
go func() { errCh <- b.docker.ContainerAttach(cID, nil, b.Stdout, b.Stderr, true)
errCh <- b.docker.ContainerAttach(cID, nil, b.Stdout, b.Stderr, true) }()
}()
}
finished := make(chan struct{}) finished := make(chan struct{})
defer close(finished) defer close(finished)
@ -546,11 +544,9 @@ func (b *Builder) run(cID string) (err error) {
return err return err
} }
if b.Verbose { // Block on reading output from container, stop on err or chan closed
// Block on reading output from container, stop on err or chan closed if err := <-errCh; err != nil {
if err := <-errCh; err != nil { return err
return err
}
} }
if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 { if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 {

View file

@ -95,7 +95,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d '
complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage'
complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l no-cache -d 'Do not use cache when building the image' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l no-cache -d 'Do not use cache when building the image'
complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l pull -d 'Always attempt to pull a newer version of the image' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l pull -d 'Always attempt to pull a newer version of the image'
complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s q -l quiet -d 'Suppress the verbose output generated by the containers' 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'
complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l rm -d 'Remove intermediate containers after a successful build' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l rm -d 'Remove intermediate containers after a successful build'
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' 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'

View file

@ -30,7 +30,7 @@ parent = "smn_cli"
--memory-swap="" Total memory (memory + swap), `-1` to disable swap --memory-swap="" Total memory (memory + swap), `-1` to disable swap
--no-cache=false Do not use cache when building the image --no-cache=false Do not use cache when building the image
--pull=false Always attempt to pull a newer version of the image --pull=false Always attempt to pull a newer version of the image
-q, --quiet=false Suppress the verbose output generated by the containers -q, --quiet=false Suppress the build output and print image ID on success
--rm=true Remove intermediate containers after a successful build --rm=true Remove intermediate containers after a successful build
--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`. --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`.
-t, --tag=[] Name and optionally a tag in the 'name:tag' format -t, --tag=[] Name and optionally a tag in the 'name:tag' format

View file

@ -4941,6 +4941,110 @@ func (s *DockerSuite) TestBuildLabelsCache(c *check.C) {
} }
func (s *DockerSuite) TestBuildNotVerboseSuccess(c *check.C) {
testRequires(c, DaemonIsLinux)
// This test makes sure that -q works correctly when build is successful:
// stdout has only the image ID (long image ID) and stderr is empty.
var stdout, stderr string
var err error
outRegexp := regexp.MustCompile("^(sha256:|)[a-z0-9]{64}\\n$")
tt := []struct {
Name string
BuildFunc func(string)
}{
{
Name: "quiet_build_stdin_success",
BuildFunc: func(name string) {
_, stdout, stderr, err = buildImageWithStdoutStderr(name, "FROM busybox", true, "-q", "--force-rm", "--rm")
},
},
{
Name: "quiet_build_ctx_success",
BuildFunc: func(name string) {
ctx, err := fakeContext("FROM busybox", map[string]string{
"quiet_build_success_fctx": "test",
})
if err != nil {
c.Fatalf("Failed to create context: %s", err.Error())
}
defer ctx.Close()
_, stdout, stderr, err = buildImageFromContextWithStdoutStderr(name, ctx, true, "-q", "--force-rm", "--rm")
},
},
{
Name: "quiet_build_git_success",
BuildFunc: func(name string) {
git, err := newFakeGit("repo", map[string]string{
"Dockerfile": "FROM busybox",
}, true)
if err != nil {
c.Fatalf("Failed to create the git repo: %s", err.Error())
}
defer git.Close()
_, stdout, stderr, err = buildImageFromGitWithStdoutStderr(name, git, true, "-q", "--force-rm", "--rm")
},
},
}
for _, te := range tt {
te.BuildFunc(te.Name)
if err != nil {
c.Fatalf("Test %s shouldn't fail, but got the following error: %s", te.Name, err.Error())
}
if outRegexp.Find([]byte(stdout)) == nil {
c.Fatalf("Test %s expected stdout to match the [%v] regexp, but it is [%v]", te.Name, outRegexp, stdout)
}
if stderr != "" {
c.Fatalf("Test %s expected stderr to be empty, but it is [%#v]", te.Name, stderr)
}
}
}
func (s *DockerSuite) TestBuildNotVerboseFailure(c *check.C) {
testRequires(c, DaemonIsLinux)
// This test makes sure that -q works correctly when build fails by
// comparing between the stderr output in quiet mode and in stdout
// and stderr output in verbose mode
tt := []struct {
TestName string
BuildCmds string
}{
{"quiet_build_no_from_at_the_beginning", "RUN whoami"},
{"quiet_build_unknown_instr", "FROMD busybox"},
{"quiet_build_not_exists_image", "FROM busybox11"},
}
for _, te := range tt {
_, _, qstderr, qerr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "-q", "--force-rm", "--rm")
_, vstdout, vstderr, verr := buildImageWithStdoutStderr(te.TestName, te.BuildCmds, false, "--force-rm", "--rm")
if verr == nil || qerr == nil {
c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", te.TestName))
}
if qstderr != vstdout+vstderr {
c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", te.TestName, qstderr, vstdout))
}
}
}
func (s *DockerSuite) TestBuildNotVerboseFailureRemote(c *check.C) {
testRequires(c, DaemonIsLinux)
// This test ensures that when given a wrong URL, stderr in quiet mode and
// stdout and stderr in verbose mode are identical.
URL := "http://bla.bla.com"
Name := "quiet_build_wrong_remote"
_, _, qstderr, qerr := buildImageWithStdoutStderr(Name, "", false, "-q", "--force-rm", "--rm", URL)
_, vstdout, vstderr, verr := buildImageWithStdoutStderr(Name, "", false, "--force-rm", "--rm", URL)
if qerr == nil || verr == nil {
c.Fatal(fmt.Errorf("Test [%s] expected to fail but didn't", Name))
}
if qstderr != vstdout+vstderr {
c.Fatal(fmt.Errorf("Test[%s] expected that quiet stderr and verbose stdout are equal; quiet [%v], verbose [%v]", Name, qstderr, vstdout))
}
}
func (s *DockerSuite) TestBuildStderr(c *check.C) { func (s *DockerSuite) TestBuildStderr(c *check.C) {
testRequires(c, DaemonIsLinux) testRequires(c, DaemonIsLinux)
// This test just makes sure that no non-error output goes // This test just makes sure that no non-error output goes
@ -5589,34 +5693,6 @@ func (s *DockerSuite) TestBuildDotDotFile(c *check.C) {
} }
} }
func (s *DockerSuite) TestBuildNotVerbose(c *check.C) {
testRequires(c, DaemonIsLinux)
ctx, err := fakeContext("FROM busybox\nENV abc=hi\nRUN echo $abc there", map[string]string{})
if err != nil {
c.Fatal(err)
}
defer ctx.Close()
// First do it w/verbose - baseline
out, _, err := dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "-t", "verbose", ".")
if err != nil {
c.Fatalf("failed to build the image w/o -q: %s, %v", out, err)
}
if !strings.Contains(out, "hi there") {
c.Fatalf("missing output:%s\n", out)
}
// Now do it w/o verbose
out, _, err = dockerCmdInDir(c, ctx.Dir, "build", "--no-cache", "-q", "-t", "verbose", ".")
if err != nil {
c.Fatalf("failed to build the image w/ -q: %s, %v", out, err)
}
if strings.Contains(out, "hi there") {
c.Fatalf("Bad output, should not contain 'hi there':%s", out)
}
}
func (s *DockerSuite) TestBuildRUNoneJSON(c *check.C) { func (s *DockerSuite) TestBuildRUNoneJSON(c *check.C) {
testRequires(c, DaemonIsLinux) testRequires(c, DaemonIsLinux)
name := "testbuildrunonejson" name := "testbuildrunonejson"

View file

@ -1308,6 +1308,47 @@ func buildImageFromContextWithOut(name string, ctx *FakeContext, useCache bool,
return id, out, nil return id, out, nil
} }
func buildImageFromContextWithStdoutStderr(name string, ctx *FakeContext, useCache bool, buildFlags ...string) (string, string, string, error) {
args := []string{"build", "-t", name}
if !useCache {
args = append(args, "--no-cache")
}
args = append(args, buildFlags...)
args = append(args, ".")
buildCmd := exec.Command(dockerBinary, args...)
buildCmd.Dir = ctx.Dir
stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
if err != nil || exitCode != 0 {
return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
}
id, err := getIDByName(name)
if err != nil {
return "", stdout, stderr, err
}
return id, stdout, stderr, nil
}
func buildImageFromGitWithStdoutStderr(name string, ctx *fakeGit, useCache bool, buildFlags ...string) (string, string, string, error) {
args := []string{"build", "-t", name}
if !useCache {
args = append(args, "--no-cache")
}
args = append(args, buildFlags...)
args = append(args, ctx.RepoURL)
buildCmd := exec.Command(dockerBinary, args...)
stdout, stderr, exitCode, err := runCommandWithStdoutStderr(buildCmd)
if err != nil || exitCode != 0 {
return "", stdout, stderr, fmt.Errorf("failed to build the image: %s", stdout)
}
id, err := getIDByName(name)
if err != nil {
return "", stdout, stderr, err
}
return id, stdout, stderr, nil
}
func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) { func buildImageFromPath(name, path string, useCache bool, buildFlags ...string) (string, error) {
args := []string{"build", "-t", name} args := []string{"build", "-t", name}
if !useCache { if !useCache {

View file

@ -81,7 +81,7 @@ set as the **URL**, the repository is cloned locally and then sent as the contex
Always attempt to pull a newer version of the image. The default is *false*. Always attempt to pull a newer version of the image. The default is *false*.
**-q**, **--quiet**=*true*|*false* **-q**, **--quiet**=*true*|*false*
Suppress the verbose output generated by the containers. The default is *false*. Suppress the build output and print image ID on success. The default is *false*.
**--rm**=*true*|*false* **--rm**=*true*|*false*
Remove intermediate containers after a successful build. The default is *true*. Remove intermediate containers after a successful build. The default is *true*.