Browse code

api/client/build: allow tar as context for docker build -

Docker-DCO-1.1-Signed-off-by: Johan Euphrosine <proppy@google.com> (github: proppy)

Johan Euphrosine authored on 2014/05/10 07:26:41
Showing 7 changed files
... ...
@@ -36,6 +36,10 @@ import (
36 36
 	"github.com/dotcloud/docker/utils/filters"
37 37
 )
38 38
 
39
+const (
40
+	tarHeaderSize = 512
41
+)
42
+
39 43
 func (cli *DockerCli) CmdHelp(args ...string) error {
40 44
 	if len(args) > 0 {
41 45
 		method, exists := cli.getMethod(args[0])
... ...
@@ -113,13 +117,22 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
113 113
 	_, err = exec.LookPath("git")
114 114
 	hasGit := err == nil
115 115
 	if cmd.Arg(0) == "-" {
116
-		// As a special case, 'docker build -' will build from an empty context with the
117
-		// contents of stdin as a Dockerfile
118
-		dockerfile, err := ioutil.ReadAll(cli.in)
119
-		if err != nil {
120
-			return err
116
+		// As a special case, 'docker build -' will build from either an empty context with the
117
+		// contents of stdin as a Dockerfile, or a tar-ed context from stdin.
118
+		buf := bufio.NewReader(cli.in)
119
+		magic, err := buf.Peek(tarHeaderSize)
120
+		if err != nil && err != io.EOF {
121
+			return fmt.Errorf("failed to peek context header from stdin: %v", err)
122
+		}
123
+		if !archive.IsArchive(magic) {
124
+			dockerfile, err := ioutil.ReadAll(buf)
125
+			if err != nil {
126
+				return fmt.Errorf("failed to read Dockerfile from stdin: %v", err)
127
+			}
128
+			context, err = archive.Generate("Dockerfile", string(dockerfile))
129
+		} else {
130
+			context = ioutil.NopCloser(buf)
121 131
 		}
122
-		context, err = archive.Generate("Dockerfile", string(dockerfile))
123 132
 	} else if utils.IsURL(cmd.Arg(0)) && (!utils.IsGIT(cmd.Arg(0)) || !hasGit) {
124 133
 		isRemote = true
125 134
 	} else {
... ...
@@ -43,6 +43,16 @@ const (
43 43
 	Xz
44 44
 )
45 45
 
46
+func IsArchive(header []byte) bool {
47
+	compression := DetectCompression(header)
48
+	if compression != Uncompressed {
49
+		return true
50
+	}
51
+	r := tar.NewReader(bytes.NewBuffer(header))
52
+	_, err := r.Next()
53
+	return err == nil
54
+}
55
+
46 56
 func DetectCompression(source []byte) Compression {
47 57
 	for compression, m := range map[Compression][]byte{
48 58
 		Bzip2: {0x42, 0x5A, 0x68},
... ...
@@ -238,10 +238,15 @@ All new files and directories are created with a uid and gid of 0.
238 238
 In the case where `<src>` is a remote file URL, the destination will have permissions 600.
239 239
 
240 240
 > **Note**:
241
-> If you build using STDIN (`docker build - < somefile`), there is no
242
-> build context, so the Dockerfile can only contain a URL based ADD
243
-> statement.
244
-
241
+> If you build by passing a Dockerfile through STDIN (`docker build - < somefile`),
242
+> there is no build context, so the Dockerfile can only contain a URL
243
+> based ADD statement.
244
+
245
+> You can also pass a compressed archive through STDIN:
246
+> (`docker build - < archive.tar.gz`), the `Dockerfile` at the root of
247
+> the archive and the rest of the archive will get used at the context
248
+> of the build.
249
+>
245 250
 > **Note**:
246 251
 > If your URL files are protected using authentication, you will need to
247 252
 > use `RUN wget` , `RUN curl`
... ...
@@ -361,7 +366,7 @@ execute in `/bin/sh -c`:
361 361
     FROM ubuntu
362 362
     ENTRYPOINT wc -l -
363 363
 
364
-For example, that Dockerfile's image will *always* take stdin as input
364
+For example, that Dockerfile's image will *always* take STDIN as input
365 365
 ("-") and print the number of lines ("-l"). If you wanted to make this
366 366
 optional but default, you could use a CMD:
367 367
 
... ...
@@ -274,12 +274,17 @@ and the tag will be `2.0`
274 274
 
275 275
     $ sudo docker build - < Dockerfile
276 276
 
277
-This will read a Dockerfile from *stdin* without
277
+This will read a Dockerfile from STDIN without
278 278
 context. Due to the lack of a context, no contents of any local
279 279
 directory will be sent to the `docker` daemon. Since
280 280
 there is no context, a Dockerfile `ADD`
281 281
 only works if it refers to a remote URL.
282 282
 
283
+    $ sudo docker build - < context.tar.gz
284
+
285
+This will build an image for a compressed context read from STDIN.
286
+Supported formats are: bzip2, gzip and xz.
287
+
283 288
     $ sudo docker build github.com/creack/docker-firefox
284 289
 
285 290
 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
531 531
 file archive (.tar, .tar.gz, .tgz, .bzip, .tar.xz, or .txz) containing a
532 532
 root filesystem. If you would like to import from a local directory or
533 533
 archive, you can use the `-` parameter to take the
534
-data from *stdin*.
534
+data from STDIN.
535 535
 
536 536
 ### Examples
537 537
 
... ...
@@ -543,7 +548,7 @@ This will create a new untagged image.
543 543
 
544 544
 **Import from a local file:**
545 545
 
546
-Import to docker via pipe and *stdin*.
546
+Import to docker via pipe and STDIN.
547 547
 
548 548
     $ cat exampleimage.tgz | sudo docker import - exampleimagelocal:new
549 549
 
550 550
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+FROM busybox
1
+ADD foo /foo
2
+CMD ["cat", "/foo"]
0 3
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+foo
... ...
@@ -9,6 +9,8 @@ import (
9 9
 	"strings"
10 10
 	"testing"
11 11
 	"time"
12
+
13
+	"github.com/dotcloud/docker/archive"
12 14
 )
13 15
 
14 16
 func TestBuildCacheADD(t *testing.T) {
... ...
@@ -1130,6 +1132,50 @@ func TestBuildADDLocalAndRemoteFilesWithCache(t *testing.T) {
1130 1130
 	logDone("build - add local and remote file with cache")
1131 1131
 }
1132 1132
 
1133
+func testContextTar(t *testing.T, compression archive.Compression) {
1134
+	contextDirectory := filepath.Join(workingDirectory, "build_tests", "TestContextTar")
1135
+	context, err := archive.Tar(contextDirectory, compression)
1136
+
1137
+	if err != nil {
1138
+		t.Fatalf("failed to build context tar: %v", err)
1139
+	}
1140
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "contexttar", "-")
1141
+	buildCmd.Stdin = context
1142
+
1143
+	out, exitCode, err := runCommandWithOutput(buildCmd)
1144
+	if err != nil || exitCode != 0 {
1145
+		t.Fatalf("build failed to complete: %v %v", out, err)
1146
+	}
1147
+	deleteImages("contexttar")
1148
+	logDone(fmt.Sprintf("build - build an image with a context tar, compression: %v", compression))
1149
+}
1150
+
1151
+func TestContextTarGzip(t *testing.T) {
1152
+	testContextTar(t, archive.Gzip)
1153
+}
1154
+
1155
+func TestContextTarNoCompression(t *testing.T) {
1156
+	testContextTar(t, archive.Uncompressed)
1157
+}
1158
+
1159
+func TestNoContext(t *testing.T) {
1160
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "nocontext", "-")
1161
+	buildCmd.Stdin = strings.NewReader("FROM busybox\nCMD echo ok\n")
1162
+
1163
+	out, exitCode, err := runCommandWithOutput(buildCmd)
1164
+	if err != nil || exitCode != 0 {
1165
+		t.Fatalf("build failed to complete: %v %v", out, err)
1166
+	}
1167
+
1168
+	out, exitCode, err = cmd(t, "run", "nocontext")
1169
+	if out != "ok\n" {
1170
+		t.Fatalf("run produced invalid output: %q, expected %q", out, "ok")
1171
+	}
1172
+
1173
+	deleteImages("nocontext")
1174
+	logDone("build - build an image with no context")
1175
+}
1176
+
1133 1177
 // TODO: TestCaching
1134 1178
 func TestBuildADDLocalAndRemoteFilesWithoutCache(t *testing.T) {
1135 1179
 	name := "testbuildaddlocalandremotefilewithoutcache"