diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 3d595e0393..d67ddfe9ff 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -77,16 +77,20 @@ func (o buildOptions) contextFromStdin() bool { return o.context == "-" } -// NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli command.Cli) *cobra.Command { +func newBuildOptions() buildOptions { ulimits := make(map[string]*units.Ulimit) - options := buildOptions{ + return buildOptions{ tags: opts.NewListOpts(validateTag), buildArgs: opts.NewListOpts(opts.ValidateEnv), ulimits: opts.NewUlimitOpt(&ulimits), labels: opts.NewListOpts(opts.ValidateEnv), extraHosts: opts.NewListOpts(opts.ValidateExtraHost), } +} + +// NewBuildCommand creates a new `docker build` command +func NewBuildCommand(dockerCli command.Cli) *cobra.Command { + options := newBuildOptions() cmd := &cobra.Command{ Use: "build [OPTIONS] PATH | URL | -", @@ -237,13 +241,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error { } excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin()) - - compression := archive.Uncompressed - if options.compress { - compression = archive.Gzip - } buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ - Compression: compression, ExcludePatterns: excludes, }) if err != nil { @@ -292,6 +290,13 @@ func runBuild(dockerCli command.Cli, options buildOptions) error { } } + if options.compress { + buildCtx, err = build.Compress(buildCtx) + if err != nil { + return err + } + } + // Setup an upload progress bar progressOutput := streamformatter.NewProgressOutput(progBuff) if !dockerCli.Out().IsTerminal() { diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index b1457bdcc2..a98cd7b237 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/pools" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" @@ -375,3 +376,27 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl }) return buildCtx, randomName, nil } + +// Compress the build context for sending to the API +func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { + pipeReader, pipeWriter := io.Pipe() + + go func() { + compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip) + if err != nil { + pipeWriter.CloseWithError(err) + } + defer buildCtx.Close() + + if _, err := pools.Copy(compressWriter, buildCtx); err != nil { + pipeWriter.CloseWithError( + errors.Wrap(err, "failed to compress context")) + compressWriter.Close() + return + } + compressWriter.Close() + pipeWriter.Close() + }() + + return pipeReader, nil +} diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go new file mode 100644 index 0000000000..bf77292a85 --- /dev/null +++ b/cli/command/image/build_test.go @@ -0,0 +1,70 @@ +package image + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/archive" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { + dest, err := ioutil.TempDir("", "test-build-compress-dest") + require.NoError(t, err) + defer os.RemoveAll(dest) + + var dockerfileName string + fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + buffer := new(bytes.Buffer) + tee := io.TeeReader(context, buffer) + + assert.NoError(t, archive.Untar(tee, dest, nil)) + dockerfileName = options.Dockerfile + + header := buffer.Bytes()[:10] + assert.Equal(t, archive.Gzip, archive.DetectCompression(header)) + + body := new(bytes.Buffer) + return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil + } + + cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild}, ioutil.Discard) + dockerfile := bytes.NewBufferString(` + FROM alpine:3.6 + COPY foo / + `) + cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile))) + + dir, err := ioutil.TempDir("", "test-build-compress") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("some content"), 0644) + + options := newBuildOptions() + options.compress = true + options.dockerfileName = "-" + options.context = dir + + err = runBuild(cli, options) + require.NoError(t, err) + + files, err := ioutil.ReadDir(dest) + require.NoError(t, err) + actual := []string{} + for _, fileInfo := range files { + actual = append(actual, fileInfo.Name()) + } + sort.Strings(actual) + assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual) +} diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go index 0df6fa4f77..b91eb7bd84 100644 --- a/cli/command/image/client_test.go +++ b/cli/command/image/client_test.go @@ -27,6 +27,7 @@ type fakeClient struct { imageInspectFunc func(image string) (types.ImageInspect, []byte, error) imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error) + imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error) } func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error { @@ -114,3 +115,10 @@ func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.Hist } return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil } + +func (cli *fakeClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + if cli.imageBuildFunc != nil { + return cli.imageBuildFunc(ctx, context, options) + } + return types.ImageBuildResponse{Body: ioutil.NopCloser(strings.NewReader(""))}, nil +} diff --git a/cli/internal/test/cli.go b/cli/internal/test/cli.go index 9565ae8e2d..378ee9cdc2 100644 --- a/cli/internal/test/cli.go +++ b/cli/internal/test/cli.go @@ -18,15 +18,17 @@ type FakeCli struct { out *command.OutStream err io.Writer in *command.InStream + server command.ServerInfo } // NewFakeCli returns a Cli backed by the fakeCli func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { return &FakeCli{ - client: client, - out: command.NewOutStream(out), - err: ioutil.Discard, - in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + client: client, + out: command.NewOutStream(out), + err: ioutil.Discard, + in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + configfile: configfile.New("configfile"), } } @@ -69,3 +71,8 @@ func (c *FakeCli) In() *command.InStream { func (c *FakeCli) ConfigFile() *configfile.ConfigFile { return c.configfile } + +// ServerInfo returns API server information for the server used by this client +func (c *FakeCli) ServerInfo() command.ServerInfo { + return c.server +}