From edcb41451aa388abe03d4f837ab5ee316a53a030 Mon Sep 17 00:00:00 2001 From: Johan Euphrosine Date: Fri, 9 May 2014 15:26:41 -0700 Subject: [PATCH] api/client/build: allow tar as context for docker build - Docker-DCO-1.1-Signed-off-by: Johan Euphrosine (github: proppy) --- api/client/commands.go | 25 +++++++--- archive/archive.go | 10 ++++ docs/sources/reference/builder.md | 13 ++++-- docs/sources/reference/commandline/cli.md | 11 +++-- .../build_tests/TestContextTar/Dockerfile | 3 ++ .../build_tests/TestContextTar/foo | 1 + integration-cli/docker_cli_build_test.go | 46 +++++++++++++++++++ 7 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 integration-cli/build_tests/TestContextTar/Dockerfile create mode 100644 integration-cli/build_tests/TestContextTar/foo diff --git a/api/client/commands.go b/api/client/commands.go index 0cdf3f1acb..ca5f3e5bb5 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -36,6 +36,10 @@ import ( "github.com/dotcloud/docker/utils/filters" ) +const ( + tarHeaderSize = 512 +) + func (cli *DockerCli) CmdHelp(args ...string) error { if len(args) > 0 { method, exists := cli.getMethod(args[0]) @@ -113,13 +117,22 @@ func (cli *DockerCli) CmdBuild(args ...string) error { _, err = exec.LookPath("git") hasGit := err == nil if cmd.Arg(0) == "-" { - // As a special case, 'docker build -' will build from an empty context with the - // contents of stdin as a Dockerfile - dockerfile, err := ioutil.ReadAll(cli.in) - if err != nil { - return err + // As a special case, 'docker build -' will build from either an empty context with the + // contents of stdin as a Dockerfile, or a tar-ed context from stdin. + buf := bufio.NewReader(cli.in) + magic, err := buf.Peek(tarHeaderSize) + if err != nil && err != io.EOF { + return fmt.Errorf("failed to peek context header from stdin: %v", err) + } + if !archive.IsArchive(magic) { + dockerfile, err := ioutil.ReadAll(buf) + if err != nil { + return fmt.Errorf("failed to read Dockerfile from stdin: %v", err) + } + context, err = archive.Generate("Dockerfile", string(dockerfile)) + } else { + context = ioutil.NopCloser(buf) } - context, err = archive.Generate("Dockerfile", string(dockerfile)) } else if utils.IsURL(cmd.Arg(0)) && (!utils.IsGIT(cmd.Arg(0)) || !hasGit) { isRemote = true } else { diff --git a/archive/archive.go b/archive/archive.go index 1982218b46..202558b7bd 100644 --- a/archive/archive.go +++ b/archive/archive.go @@ -43,6 +43,16 @@ const ( Xz ) +func IsArchive(header []byte) bool { + compression := DetectCompression(header) + if compression != Uncompressed { + return true + } + r := tar.NewReader(bytes.NewBuffer(header)) + _, err := r.Next() + return err == nil +} + func DetectCompression(source []byte) Compression { for compression, m := range map[Compression][]byte{ Bzip2: {0x42, 0x5A, 0x68}, diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index 82b91a5b41..cf729664e2 100644 --- a/docs/sources/reference/builder.md +++ b/docs/sources/reference/builder.md @@ -238,10 +238,15 @@ All new files and directories are created with a uid and gid of 0. In the case where `` is a remote file URL, the destination will have permissions 600. > **Note**: -> If you build using STDIN (`docker build - < somefile`), there is no -> build context, so the Dockerfile can only contain a URL based ADD -> statement. +> If you build by passing a Dockerfile through STDIN (`docker build - < somefile`), +> there is no build context, so the Dockerfile can only contain a URL +> based ADD statement. +> You can also pass a compressed archive through STDIN: +> (`docker build - < archive.tar.gz`), the `Dockerfile` at the root of +> the archive and the rest of the archive will get used at the context +> of the build. +> > **Note**: > If your URL files are protected using authentication, you will need to > use `RUN wget` , `RUN curl` @@ -361,7 +366,7 @@ execute in `/bin/sh -c`: FROM ubuntu ENTRYPOINT wc -l - -For example, that Dockerfile's image will *always* take stdin as input +For example, that Dockerfile's image will *always* take STDIN as input ("-") and print the number of lines ("-l"). If you wanted to make this optional but default, you could use a CMD: diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index b92eeb3fbc..3437464cbb 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -274,12 +274,17 @@ and the tag will be `2.0` $ sudo docker build - < Dockerfile -This will read a Dockerfile from *stdin* without +This will read a Dockerfile from STDIN without context. Due to the lack of a context, no contents of any local directory will be sent to the `docker` daemon. Since there is no context, a Dockerfile `ADD` only works if it refers to a remote URL. + $ sudo docker build - < context.tar.gz + +This will build an image for a compressed context read from STDIN. +Supported formats are: bzip2, gzip and xz. + $ sudo docker build github.com/creack/docker-firefox This will clone the GitHub repository and use the cloned repository as @@ -531,7 +536,7 @@ URLs must start with `http` and point to a single file archive (.tar, .tar.gz, .tgz, .bzip, .tar.xz, or .txz) containing a root filesystem. If you would like to import from a local directory or archive, you can use the `-` parameter to take the -data from *stdin*. +data from STDIN. ### Examples @@ -543,7 +548,7 @@ This will create a new untagged image. **Import from a local file:** -Import to docker via pipe and *stdin*. +Import to docker via pipe and STDIN. $ cat exampleimage.tgz | sudo docker import - exampleimagelocal:new diff --git a/integration-cli/build_tests/TestContextTar/Dockerfile b/integration-cli/build_tests/TestContextTar/Dockerfile new file mode 100644 index 0000000000..41380570c1 --- /dev/null +++ b/integration-cli/build_tests/TestContextTar/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox +ADD foo /foo +CMD ["cat", "/foo"] diff --git a/integration-cli/build_tests/TestContextTar/foo b/integration-cli/build_tests/TestContextTar/foo new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/integration-cli/build_tests/TestContextTar/foo @@ -0,0 +1 @@ +foo diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 9a360c1964..48075912b4 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" "time" + + "github.com/dotcloud/docker/archive" ) func TestBuildCacheADD(t *testing.T) { @@ -1130,6 +1132,50 @@ func TestBuildADDLocalAndRemoteFilesWithCache(t *testing.T) { logDone("build - add local and remote file with cache") } +func testContextTar(t *testing.T, compression archive.Compression) { + contextDirectory := filepath.Join(workingDirectory, "build_tests", "TestContextTar") + context, err := archive.Tar(contextDirectory, compression) + + if err != nil { + t.Fatalf("failed to build context tar: %v", err) + } + buildCmd := exec.Command(dockerBinary, "build", "-t", "contexttar", "-") + buildCmd.Stdin = context + + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + t.Fatalf("build failed to complete: %v %v", out, err) + } + deleteImages("contexttar") + logDone(fmt.Sprintf("build - build an image with a context tar, compression: %v", compression)) +} + +func TestContextTarGzip(t *testing.T) { + testContextTar(t, archive.Gzip) +} + +func TestContextTarNoCompression(t *testing.T) { + testContextTar(t, archive.Uncompressed) +} + +func TestNoContext(t *testing.T) { + buildCmd := exec.Command(dockerBinary, "build", "-t", "nocontext", "-") + buildCmd.Stdin = strings.NewReader("FROM busybox\nCMD echo ok\n") + + out, exitCode, err := runCommandWithOutput(buildCmd) + if err != nil || exitCode != 0 { + t.Fatalf("build failed to complete: %v %v", out, err) + } + + out, exitCode, err = cmd(t, "run", "nocontext") + if out != "ok\n" { + t.Fatalf("run produced invalid output: %q, expected %q", out, "ok") + } + + deleteImages("nocontext") + logDone("build - build an image with no context") +} + // TODO: TestCaching func TestBuildADDLocalAndRemoteFilesWithoutCache(t *testing.T) { name := "testbuildaddlocalandremotefilewithoutcache"