Browse code

Support downloading remote tarball contexts in builder jobs.

Signed-off-by: Moysés Borges <moysesb@gmail.com>

Moysés Borges authored on 2015/04/04 23:54:43
Showing 11 changed files
... ...
@@ -2,6 +2,7 @@ package builder
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"errors"
5 6
 	"fmt"
6 7
 	"io"
7 8
 	"io/ioutil"
... ...
@@ -17,6 +18,7 @@ import (
17 17
 	"github.com/docker/docker/pkg/archive"
18 18
 	"github.com/docker/docker/pkg/httputils"
19 19
 	"github.com/docker/docker/pkg/parsers"
20
+	"github.com/docker/docker/pkg/progressreader"
20 21
 	"github.com/docker/docker/pkg/streamformatter"
21 22
 	"github.com/docker/docker/pkg/urlutil"
22 23
 	"github.com/docker/docker/registry"
... ...
@@ -24,6 +26,10 @@ import (
24 24
 	"github.com/docker/docker/utils"
25 25
 )
26 26
 
27
+// When downloading remote contexts, limit the amount (in bytes)
28
+// to be read from the response body in order to detect its Content-Type
29
+const maxPreambleLength = 100
30
+
27 31
 // whitelist of commands allowed for a commit/import
28 32
 var validCommitCommands = map[string]bool{
29 33
 	"entrypoint": true,
... ...
@@ -91,6 +97,7 @@ func Build(d *daemon.Daemon, buildConfig *Config) error {
91 91
 		tag      string
92 92
 		context  io.ReadCloser
93 93
 	)
94
+	sf := streamformatter.NewJSONStreamFormatter()
94 95
 
95 96
 	repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName)
96 97
 	if repoName != "" {
... ...
@@ -121,27 +128,50 @@ func Build(d *daemon.Daemon, buildConfig *Config) error {
121 121
 	} else if urlutil.IsURL(buildConfig.RemoteURL) {
122 122
 		f, err := httputils.Download(buildConfig.RemoteURL)
123 123
 		if err != nil {
124
-			return err
124
+			return fmt.Errorf("Error downloading remote context %s: %v", buildConfig.RemoteURL, err)
125 125
 		}
126 126
 		defer f.Body.Close()
127
-		dockerFile, err := ioutil.ReadAll(f.Body)
127
+		ct := f.Header.Get("Content-Type")
128
+		clen := int(f.ContentLength)
129
+		contentType, bodyReader, err := inspectResponse(ct, f.Body, clen)
130
+
131
+		defer bodyReader.Close()
132
+
128 133
 		if err != nil {
129
-			return err
134
+			return fmt.Errorf("Error detecting content type for remote %s: %v", buildConfig.RemoteURL, err)
130 135
 		}
136
+		if contentType == httputils.MimeTypes.TextPlain {
137
+			dockerFile, err := ioutil.ReadAll(bodyReader)
138
+			if err != nil {
139
+				return err
140
+			}
131 141
 
132
-		// When we're downloading just a Dockerfile put it in
133
-		// the default name - don't allow the client to move/specify it
134
-		buildConfig.DockerfileName = api.DefaultDockerfileName
142
+			// When we're downloading just a Dockerfile put it in
143
+			// the default name - don't allow the client to move/specify it
144
+			buildConfig.DockerfileName = api.DefaultDockerfileName
135 145
 
136
-		c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile))
137
-		if err != nil {
138
-			return err
146
+			c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile))
147
+			if err != nil {
148
+				return err
149
+			}
150
+			context = c
151
+		} else {
152
+			// Pass through - this is a pre-packaged context, presumably
153
+			// with a Dockerfile with the right name inside it.
154
+			prCfg := progressreader.Config{
155
+				In:        bodyReader,
156
+				Out:       buildConfig.Stdout,
157
+				Formatter: sf,
158
+				Size:      clen,
159
+				NewLines:  true,
160
+				ID:        "Downloading context",
161
+				Action:    buildConfig.RemoteURL,
162
+			}
163
+			context = progressreader.New(prCfg)
139 164
 		}
140
-		context = c
141 165
 	}
142
-	defer context.Close()
143 166
 
144
-	sf := streamformatter.NewJSONStreamFormatter()
167
+	defer context.Close()
145 168
 
146 169
 	builder := &Builder{
147 170
 		Daemon: d,
... ...
@@ -241,3 +271,48 @@ func Commit(d *daemon.Daemon, name string, c *daemon.ContainerCommitConfig) (str
241 241
 
242 242
 	return img.ID, nil
243 243
 }
244
+
245
+// inspectResponse looks into the http response data at r to determine whether its
246
+// content-type is on the list of acceptable content types for remote build contexts.
247
+// This function returns:
248
+//    - a string representation of the detected content-type
249
+//    - an io.Reader for the response body
250
+//    - an error value which will be non-nil either when something goes wrong while
251
+//      reading bytes from r or when the detected content-type is not acceptable.
252
+func inspectResponse(ct string, r io.ReadCloser, clen int) (string, io.ReadCloser, error) {
253
+	plen := clen
254
+	if plen <= 0 || plen > maxPreambleLength {
255
+		plen = maxPreambleLength
256
+	}
257
+
258
+	preamble := make([]byte, plen, plen)
259
+	rlen, err := r.Read(preamble)
260
+	if rlen == 0 {
261
+		return ct, r, errors.New("Empty response")
262
+	}
263
+	if err != nil && err != io.EOF {
264
+		return ct, r, err
265
+	}
266
+
267
+	preambleR := bytes.NewReader(preamble)
268
+	bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
269
+	// Some web servers will use application/octet-stream as the default
270
+	// content type for files without an extension (e.g. 'Dockerfile')
271
+	// so if we receive this value we better check for text content
272
+	contentType := ct
273
+	if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
274
+		contentType, _, err = httputils.DetectContentType(preamble)
275
+		if err != nil {
276
+			return contentType, bodyReader, err
277
+		}
278
+	}
279
+
280
+	contentType = selectAcceptableMIME(contentType)
281
+	var cterr error
282
+	if len(contentType) == 0 {
283
+		cterr = fmt.Errorf("unsupported Content-Type %q", ct)
284
+		contentType = ct
285
+	}
286
+
287
+	return contentType, bodyReader, cterr
288
+}
244 289
new file mode 100644
... ...
@@ -0,0 +1,113 @@
0
+package builder
1
+
2
+import (
3
+	"bytes"
4
+	"io/ioutil"
5
+	"testing"
6
+)
7
+
8
+var textPlainDockerfile = "FROM busybox"
9
+var binaryContext = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00} //xz magic
10
+
11
+func TestInspectEmptyResponse(t *testing.T) {
12
+	ct := "application/octet-stream"
13
+	br := ioutil.NopCloser(bytes.NewReader([]byte("")))
14
+	contentType, bReader, err := inspectResponse(ct, br, 0)
15
+	if err == nil {
16
+		t.Fatalf("Should have generated an error for an empty response")
17
+	}
18
+	if contentType != "application/octet-stream" {
19
+		t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType)
20
+	}
21
+	body, err := ioutil.ReadAll(bReader)
22
+	if err != nil {
23
+		t.Fatal(err)
24
+	}
25
+	if len(body) != 0 {
26
+		t.Fatal("response body should remain empty")
27
+	}
28
+}
29
+
30
+func TestInspectResponseBinary(t *testing.T) {
31
+	ct := "application/octet-stream"
32
+	br := ioutil.NopCloser(bytes.NewReader(binaryContext))
33
+	contentType, bReader, err := inspectResponse(ct, br, len(binaryContext))
34
+	if err != nil {
35
+		t.Fatal(err)
36
+	}
37
+	if contentType != "application/octet-stream" {
38
+		t.Fatalf("Content type should be 'application/octet-stream' but is %q", contentType)
39
+	}
40
+	body, err := ioutil.ReadAll(bReader)
41
+	if err != nil {
42
+		t.Fatal(err)
43
+	}
44
+	if len(body) != len(binaryContext) {
45
+		t.Fatalf("Wrong response size %d, should be == len(binaryContext)", len(body))
46
+	}
47
+	for i := range body {
48
+		if body[i] != binaryContext[i] {
49
+			t.Fatalf("Corrupted response body at byte index %d", i)
50
+		}
51
+	}
52
+}
53
+
54
+func TestResponseUnsupportedContentType(t *testing.T) {
55
+	content := []byte(textPlainDockerfile)
56
+	ct := "application/json"
57
+	br := ioutil.NopCloser(bytes.NewReader(content))
58
+	contentType, bReader, err := inspectResponse(ct, br, len(textPlainDockerfile))
59
+
60
+	if err == nil {
61
+		t.Fatal("Should have returned an error on content-type 'application/json'")
62
+	}
63
+	if contentType != ct {
64
+		t.Fatalf("Should not have altered content-type: orig: %s, altered: %s", ct, contentType)
65
+	}
66
+	body, err := ioutil.ReadAll(bReader)
67
+	if err != nil {
68
+		t.Fatal(err)
69
+	}
70
+	if string(body) != textPlainDockerfile {
71
+		t.Fatalf("Corrupted response body %s", body)
72
+	}
73
+}
74
+
75
+func TestInspectResponseTextSimple(t *testing.T) {
76
+	content := []byte(textPlainDockerfile)
77
+	ct := "text/plain"
78
+	br := ioutil.NopCloser(bytes.NewReader(content))
79
+	contentType, bReader, err := inspectResponse(ct, br, len(content))
80
+	if err != nil {
81
+		t.Fatal(err)
82
+	}
83
+	if contentType != "text/plain" {
84
+		t.Fatalf("Content type should be 'text/plain' but is %q", contentType)
85
+	}
86
+	body, err := ioutil.ReadAll(bReader)
87
+	if err != nil {
88
+		t.Fatal(err)
89
+	}
90
+	if string(body) != textPlainDockerfile {
91
+		t.Fatalf("Corrupted response body %s", body)
92
+	}
93
+}
94
+
95
+func TestInspectResponseEmptyContentType(t *testing.T) {
96
+	content := []byte(textPlainDockerfile)
97
+	br := ioutil.NopCloser(bytes.NewReader(content))
98
+	contentType, bodyReader, err := inspectResponse("", br, len(content))
99
+	if err != nil {
100
+		t.Fatal(err)
101
+	}
102
+	if contentType != "text/plain" {
103
+		t.Fatalf("Content type should be 'text/plain' but is %q", contentType)
104
+	}
105
+	body, err := ioutil.ReadAll(bodyReader)
106
+	if err != nil {
107
+		t.Fatal(err)
108
+	}
109
+	if string(body) != textPlainDockerfile {
110
+		t.Fatalf("Corrupted response body %s", body)
111
+	}
112
+}
... ...
@@ -1,9 +1,18 @@
1 1
 package builder
2 2
 
3 3
 import (
4
+	"regexp"
4 5
 	"strings"
5 6
 )
6 7
 
8
+const acceptableRemoteMIME = `(?:application/(?:(?:x\-)?tar|octet\-stream|((?:x\-)?(?:gzip|bzip2?|xz)))|(?:text/plain))`
9
+
10
+var mimeRe = regexp.MustCompile(acceptableRemoteMIME)
11
+
12
+func selectAcceptableMIME(ct string) string {
13
+	return mimeRe.FindString(ct)
14
+}
15
+
7 16
 func handleJsonArgs(args []string, attributes map[string]bool) []string {
8 17
 	if len(args) == 0 {
9 18
 		return []string{}
10 19
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package builder
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+)
6
+
7
+func TestSelectAcceptableMIME(t *testing.T) {
8
+	validMimeStrings := []string{
9
+		"application/x-bzip2",
10
+		"application/bzip2",
11
+		"application/gzip",
12
+		"application/x-gzip",
13
+		"application/x-xz",
14
+		"application/xz",
15
+		"application/tar",
16
+		"application/x-tar",
17
+		"application/octet-stream",
18
+		"text/plain",
19
+	}
20
+
21
+	invalidMimeStrings := []string{
22
+		"",
23
+		"application/octet",
24
+		"application/json",
25
+	}
26
+
27
+	for _, m := range invalidMimeStrings {
28
+		if len(selectAcceptableMIME(m)) > 0 {
29
+			err := fmt.Errorf("Should not have accepted %q", m)
30
+			t.Fatal(err)
31
+		}
32
+	}
33
+
34
+	for _, m := range validMimeStrings {
35
+		if str := selectAcceptableMIME(m); str == "" {
36
+			err := fmt.Errorf("Should have accepted %q", m)
37
+			t.Fatal(err)
38
+		}
39
+	}
40
+}
... ...
@@ -1181,13 +1181,17 @@ or being killed.
1181 1181
 
1182 1182
 Query Parameters:
1183 1183
 
1184
--   **dockerfile** - Path within the build context to the Dockerfile. This is 
1185
-        ignored if `remote` is specified and points to an individual filename.
1186
--   **t** – A repository name (and optionally a tag) to apply to
1184
+-   **dockerfile** - Path within the build context to the `Dockerfile`. This is
1185
+        ignored if `remote` is specified and points to an external `Dockerfile`.
1186
+-   **t** – Repository name (and optionally a tag) to be applied to
1187 1187
         the resulting image in case of success.
1188
--   **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the 
1189
-        URI specifies a filename, the file's contents are placed into a file 
1190
-		called `Dockerfile`.
1188
+-   **remote** – A Git repository URI or HTTP/HTTPS context URI. If the
1189
+        URI points to a single text file, the file's contents are placed into
1190
+        a file called `Dockerfile` and the image is built from that file. If
1191
+        the URI points to a tarball, the file is downloaded by the daemon and
1192
+        the contents therein used as the context for the build. If the URI
1193
+        points to a tarball and the `dockerfile` parameter is also specified,
1194
+        there must be a file with the corresponding path inside the tarball.
1191 1195
 -   **q** – Suppress verbose build output.
1192 1196
 -   **nocache** – Do not use the cache when building the image.
1193 1197
 -   **pull** - Attempt to pull the image even if an older image exists locally.
... ...
@@ -706,13 +706,17 @@ to any of the files in the context. For example, your build can use an
706 706
 [*ADD*](/reference/builder/#add) instruction to reference a file in the
707 707
 context.
708 708
 
709
-The `URL` parameter can specify the location of a Git repository; the repository
710
-acts as the build context. The system recursively clones the repository and its
711
-submodules using a `git clone --depth 1 --recursive` command. This command runs
712
-in a temporary directory on your local host. After the command succeeds, the
713
-directory is sent to the Docker daemon as the context. Local clones give you the
714
-ability to access private repositories using local user credentials, VPNs, and
715
-so forth.
709
+The `URL` parameter can refer to three kinds of resources: Git repositories,
710
+pre-packaged tarball contexts and plain text files. 
711
+
712
+#### Git repositories
713
+When the `URL` parameter points to the location of a Git repository, the
714
+repository acts as the build context. The system recursively clones the
715
+repository and its submodules using a `git clone --depth 1 --recursive`
716
+command. This command runs in a temporary directory on your local host. After
717
+the command succeeds, the directory is sent to the Docker daemon as the
718
+context. Local clones give you the ability to access private repositories using
719
+local user credentials, VPN's, and so forth.
716 720
 
717 721
 Git URLs accept context configuration in their fragment section, separated by a
718 722
 colon `:`.  The first part represents the reference that Git will check out,
... ...
@@ -739,21 +743,34 @@ Build Syntax Suffix | Commit Used | Build Context Used
739 739
 `myrepo.git#mybranch:myfolder` | `refs/heads/mybranch` | `/myfolder`
740 740
 `myrepo.git#abcdef:myfolder` | `sha1 = abcdef` | `/myfolder`
741 741
 
742
-Instead of specifying a context, you can pass a single Dockerfile in the `URL`
743
-or pipe the file in via `STDIN`. To pipe a Dockerfile from `STDIN`:
742
+#### Tarball contexts
743
+If you pass an URL to a remote tarball, the URL itself is sent to the daemon:
744 744
 
745
-    docker build - < Dockerfile
745
+    $ docker build http://server/context.tar.gz
746 746
 
747
-If you use STDIN or specify a `URL`, the system places the contents into a file
748
-called `Dockerfile`, and any `-f`, `--file` option is ignored. In this
749
-scenario, there is no context.
747
+The download operation will be performed on the host the Docker daemon is
748
+running on, which is not necessarily the same host from which the build command
749
+is being issued. The Docker daemon will fetch `context.tar.gz` and use it as the
750
+build context. Tarball contexts must be tar archives conforming to the standard
751
+`tar` UNIX format and can be compressed with any one of the 'xz', 'bzip2',
752
+'gzip' or 'identity' (no compression) formats.
753
+
754
+#### Text files
755
+Instead of specifying a context, you can pass a single `Dockerfile` in the
756
+`URL` or pipe the file in via `STDIN`. To pipe a `Dockerfile` from `STDIN`:
757
+
758
+    $ docker build - < Dockerfile
759
+
760
+If you use `STDIN` or specify a `URL` pointing to a plain text file, the system
761
+places the contents into a file called `Dockerfile`, and any `-f`, `--file`
762
+option is ignored. In this scenario, there is no context.
750 763
 
751 764
 By default the `docker build` command will look for a `Dockerfile` at the root
752 765
 of the build context. The `-f`, `--file`, option lets you specify the path to
753 766
 an alternative file to use instead. This is useful in cases where the same set
754 767
 of files are used for multiple builds. The path must be to a file within the
755
-build context. If a relative path is specified then it must to be relative to
756
-the current directory.
768
+build context. If a relative path is specified then it is interpreted as
769
+relative to the root of the context.
757 770
 
758 771
 In most cases, it's best to put each Dockerfile in an empty directory. Then,
759 772
 add to that directory only the files needed for building the Dockerfile. To
... ...
@@ -883,6 +900,29 @@ The Dockerfile at the root of the repository is used as Dockerfile. Note that
883 883
 you can specify an arbitrary Git repository by using the `git://` or `git@`
884 884
 schema.
885 885
 
886
+
887
+    $ docker build -f ctx/Dockerfile http://server/ctx.tar.gz
888
+    Downloading context: http://server/ctx.tar.gz [===================>]    240 B/240 B
889
+    Step 0 : FROM busybox
890
+     ---> 8c2e06607696
891
+    Step 1 : ADD ctx/container.cfg /
892
+     ---> e7829950cee3
893
+    Removing intermediate container b35224abf821
894
+    Step 2 : CMD /bin/ls
895
+     ---> Running in fbc63d321d73
896
+     ---> 3286931702ad
897
+    Removing intermediate container fbc63d321d73
898
+    Successfully built 377c409b35e4
899
+
900
+
901
+This will send the URL `http://server/ctx.tar.gz` to the Docker daemon, which
902
+will download and extract the referenced tarball. The `-f ctx/Dockerfile`
903
+parameter specifies a path inside `ctx.tar.gz` to the `Dockerfile` that will
904
+be used to build the image. Any `ADD` commands in that `Dockerfile` that
905
+refer to local paths must be relative to the root of the contents inside
906
+`ctx.tar.gz`. In the example above, the tarball contains a directory `ctx/`,
907
+so the `ADD ctx/container.cfg /` operation works as expected.
908
+
886 909
     $ docker build -f Dockerfile.debug .
887 910
 
888 911
 This will use a file called `Dockerfile.debug` for the build instructions
... ...
@@ -534,6 +534,91 @@ RUN find /tmp/`,
534 534
 	}
535 535
 }
536 536
 
537
+func (s *DockerSuite) TestBuildApiRemoteTarballContext(c *check.C) {
538
+	buffer := new(bytes.Buffer)
539
+	tw := tar.NewWriter(buffer)
540
+	defer tw.Close()
541
+
542
+	dockerfile := []byte("FROM busybox")
543
+	if err := tw.WriteHeader(&tar.Header{
544
+		Name: "Dockerfile",
545
+		Size: int64(len(dockerfile)),
546
+	}); err != nil {
547
+		c.Fatalf("failed to write tar file header: %v", err)
548
+	}
549
+	if _, err := tw.Write(dockerfile); err != nil {
550
+		c.Fatalf("failed to write tar file content: %v", err)
551
+	}
552
+	if err := tw.Close(); err != nil {
553
+		c.Fatalf("failed to close tar archive: %v", err)
554
+	}
555
+
556
+	server, err := fakeBinaryStorage(map[string]*bytes.Buffer{
557
+		"testT.tar": buffer,
558
+	})
559
+	c.Assert(err, check.IsNil)
560
+
561
+	defer server.Close()
562
+
563
+	res, _, err := sockRequestRaw("POST", "/build?remote="+server.URL()+"/testT.tar", nil, "application/tar")
564
+	c.Assert(err, check.IsNil)
565
+	c.Assert(res.StatusCode, check.Equals, http.StatusOK)
566
+}
567
+
568
+func (s *DockerSuite) TestBuildApiRemoteTarballContextWithCustomDockerfile(c *check.C) {
569
+	buffer := new(bytes.Buffer)
570
+	tw := tar.NewWriter(buffer)
571
+	defer tw.Close()
572
+
573
+	dockerfile := []byte(`FROM busybox
574
+RUN echo 'wrong'`)
575
+	if err := tw.WriteHeader(&tar.Header{
576
+		Name: "Dockerfile",
577
+		Size: int64(len(dockerfile)),
578
+	}); err != nil {
579
+		c.Fatalf("failed to write tar file header: %v", err)
580
+	}
581
+	if _, err := tw.Write(dockerfile); err != nil {
582
+		c.Fatalf("failed to write tar file content: %v", err)
583
+	}
584
+
585
+	custom := []byte(`FROM busybox
586
+RUN echo 'right'
587
+`)
588
+	if err := tw.WriteHeader(&tar.Header{
589
+		Name: "custom",
590
+		Size: int64(len(custom)),
591
+	}); err != nil {
592
+		c.Fatalf("failed to write tar file header: %v", err)
593
+	}
594
+	if _, err := tw.Write(custom); err != nil {
595
+		c.Fatalf("failed to write tar file content: %v", err)
596
+	}
597
+
598
+	if err := tw.Close(); err != nil {
599
+		c.Fatalf("failed to close tar archive: %v", err)
600
+	}
601
+
602
+	server, err := fakeBinaryStorage(map[string]*bytes.Buffer{
603
+		"testT.tar": buffer,
604
+	})
605
+	c.Assert(err, check.IsNil)
606
+
607
+	defer server.Close()
608
+	url := "/build?dockerfile=custom&remote=" + server.URL() + "/testT.tar"
609
+	res, body, err := sockRequestRaw("POST", url, nil, "application/tar")
610
+	c.Assert(err, check.IsNil)
611
+	c.Assert(res.StatusCode, check.Equals, http.StatusOK)
612
+
613
+	defer body.Close()
614
+	content, err := readBody(body)
615
+	c.Assert(err, check.IsNil)
616
+
617
+	if strings.Contains(string(content), "wrong") {
618
+		c.Fatalf("Build used the wrong dockerfile.")
619
+	}
620
+}
621
+
537 622
 func (s *DockerSuite) TestBuildApiLowerDockerfile(c *check.C) {
538 623
 	git, err := fakeGIT("repo", map[string]string{
539 624
 		"dockerfile": `FROM busybox
... ...
@@ -4167,6 +4167,46 @@ func (s *DockerSuite) TestBuildFromGITWithContext(c *check.C) {
4167 4167
 	}
4168 4168
 }
4169 4169
 
4170
+func (s *DockerSuite) TestBuildFromRemoteTarball(c *check.C) {
4171
+	name := "testbuildfromremotetarball"
4172
+
4173
+	buffer := new(bytes.Buffer)
4174
+	tw := tar.NewWriter(buffer)
4175
+	defer tw.Close()
4176
+
4177
+	dockerfile := []byte(`FROM busybox
4178
+					MAINTAINER docker`)
4179
+	if err := tw.WriteHeader(&tar.Header{
4180
+		Name: "Dockerfile",
4181
+		Size: int64(len(dockerfile)),
4182
+	}); err != nil {
4183
+		c.Fatalf("failed to write tar file header: %v", err)
4184
+	}
4185
+	if _, err := tw.Write(dockerfile); err != nil {
4186
+		c.Fatalf("failed to write tar file content: %v", err)
4187
+	}
4188
+	if err := tw.Close(); err != nil {
4189
+		c.Fatalf("failed to close tar archive: %v", err)
4190
+	}
4191
+
4192
+	server, err := fakeBinaryStorage(map[string]*bytes.Buffer{
4193
+		"testT.tar": buffer,
4194
+	})
4195
+	c.Assert(err, check.IsNil)
4196
+
4197
+	defer server.Close()
4198
+
4199
+	_, err = buildImageFromPath(name, server.URL()+"/testT.tar", true)
4200
+	c.Assert(err, check.IsNil)
4201
+
4202
+	res, err := inspectField(name, "Author")
4203
+	c.Assert(err, check.IsNil)
4204
+
4205
+	if res != "docker" {
4206
+		c.Fatalf("Maintainer should be docker, got %s", res)
4207
+	}
4208
+}
4209
+
4170 4210
 func (s *DockerSuite) TestBuildCleanupCmdOnEntrypoint(c *check.C) {
4171 4211
 	name := "testbuildcmdcleanuponentrypoint"
4172 4212
 	if _, err := buildImage(name,
... ...
@@ -632,6 +632,10 @@ type FakeContext struct {
632 632
 }
633 633
 
634 634
 func (f *FakeContext) Add(file, content string) error {
635
+	return f.addFile(file, []byte(content))
636
+}
637
+
638
+func (f *FakeContext) addFile(file string, content []byte) error {
635 639
 	filepath := path.Join(f.Dir, file)
636 640
 	dirpath := path.Dir(filepath)
637 641
 	if dirpath != "." {
... ...
@@ -639,7 +643,8 @@ func (f *FakeContext) Add(file, content string) error {
639 639
 			return err
640 640
 		}
641 641
 	}
642
-	return ioutil.WriteFile(filepath, []byte(content), 0644)
642
+	return ioutil.WriteFile(filepath, content, 0644)
643
+
643 644
 }
644 645
 
645 646
 func (f *FakeContext) Delete(file string) error {
... ...
@@ -651,11 +656,7 @@ func (f *FakeContext) Close() error {
651 651
 	return os.RemoveAll(f.Dir)
652 652
 }
653 653
 
654
-func fakeContextFromDir(dir string) *FakeContext {
655
-	return &FakeContext{dir}
656
-}
657
-
658
-func fakeContextWithFiles(files map[string]string) (*FakeContext, error) {
654
+func fakeContextFromNewTempDir() (*FakeContext, error) {
659 655
 	tmp, err := ioutil.TempDir("", "fake-context")
660 656
 	if err != nil {
661 657
 		return nil, err
... ...
@@ -663,8 +664,18 @@ func fakeContextWithFiles(files map[string]string) (*FakeContext, error) {
663 663
 	if err := os.Chmod(tmp, 0755); err != nil {
664 664
 		return nil, err
665 665
 	}
666
+	return fakeContextFromDir(tmp), nil
667
+}
668
+
669
+func fakeContextFromDir(dir string) *FakeContext {
670
+	return &FakeContext{dir}
671
+}
666 672
 
667
-	ctx := fakeContextFromDir(tmp)
673
+func fakeContextWithFiles(files map[string]string) (*FakeContext, error) {
674
+	ctx, err := fakeContextFromNewTempDir()
675
+	if err != nil {
676
+		return nil, err
677
+	}
668 678
 	for file, content := range files {
669 679
 		if err := ctx.Add(file, content); err != nil {
670 680
 			ctx.Close()
... ...
@@ -701,6 +712,19 @@ type FakeStorage interface {
701 701
 	CtxDir() string
702 702
 }
703 703
 
704
+func fakeBinaryStorage(archives map[string]*bytes.Buffer) (FakeStorage, error) {
705
+	ctx, err := fakeContextFromNewTempDir()
706
+	if err != nil {
707
+		return nil, err
708
+	}
709
+	for name, content := range archives {
710
+		if err := ctx.addFile(name, content.Bytes()); err != nil {
711
+			return nil, err
712
+		}
713
+	}
714
+	return fakeStorageWithContext(ctx)
715
+}
716
+
704 717
 // fakeStorage returns either a local or remote (at daemon machine) file server
705 718
 func fakeStorage(files map[string]string) (FakeStorage, error) {
706 719
 	ctx, err := fakeContextWithFiles(files)
... ...
@@ -37,13 +37,18 @@ daemon, not by the CLI, so the whole context must be transferred to the daemon.
37 37
 The Docker CLI reports "Sending build context to Docker daemon" when the context is sent to 
38 38
 the daemon.
39 39
 
40
-When a single Dockerfile is given as the URL, then no context is set.
41
-When a Git repository is set as the **URL**, the repository is used
42
-as context.
40
+When the URL to a tarball archive or to a single Dockerfile is given, no context is sent from
41
+the client to the Docker daemon. When a Git repository is set as the **URL**, the repository is
42
+cloned locally and then sent as the context.
43 43
 
44 44
 # OPTIONS
45 45
 **-f**, **--file**=*PATH/Dockerfile*
46
-   Path to the Dockerfile to use. If the path is a relative path then it must be relative to the current directory. The file must be within the build context. The default is *Dockerfile*.
46
+   Path to the Dockerfile to use. If the path is a relative path and you are
47
+   building from a local directory, then the path must be relative to that
48
+   directory. If you are building from a remote URL pointing to either a
49
+   tarball or a Git repository, then the path must be relative to the root of
50
+   the remote context. In all cases, the file must be within the build context.
51
+   The default is *Dockerfile*.
47 52
 
48 53
 **--force-rm**=*true*|*false*
49 54
    Always remove intermediate containers, even after unsuccessful builds. The default is *false*.
... ...
@@ -209,6 +214,17 @@ repository.
209 209
 
210 210
 Note: You can set an arbitrary Git repository via the `git://` schema.
211 211
 
212
+## Building an image using a URL to a tarball'ed context
213
+
214
+This will send the URL itself to the Docker daemon. The daemon will fetch the
215
+tarball archive, decompress it and use its contents as the build context. If you
216
+pass an *-f PATH/Dockerfile* option as well, the system will look for that file
217
+inside the contents of the tarball.
218
+
219
+    docker build -f dev/Dockerfile https://10.10.10.1/docker/context.tar.gz
220
+
221
+Note: supported compression formats are 'xz', 'bzip2', 'gzip' and 'identity' (no compression).
222
+
212 223
 # HISTORY
213 224
 March 2014, Originally compiled by William Henry (whenry at redhat dot com)
214 225
 based on docker.com source material and internal work.
215 226
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package httputils
1
+
2
+import (
3
+	"mime"
4
+	"net/http"
5
+)
6
+
7
+var MimeTypes = struct {
8
+	TextPlain   string
9
+	Tar         string
10
+	OctetStream string
11
+}{"text/plain", "application/tar", "application/octet-stream"}
12
+
13
+// DetectContentType returns a best guess representation of the MIME
14
+// content type for the bytes at c.  The value detected by
15
+// http.DetectContentType is guaranteed not be nil, defaulting to
16
+// application/octet-stream when a better guess cannot be made. The
17
+// result of this detection is then run through mime.ParseMediaType()
18
+// which separates it from any parameters.
19
+// Note that calling this function does not advance the Reader at r
20
+func DetectContentType(c []byte) (string, map[string]string, error) {
21
+
22
+	ct := http.DetectContentType(c)
23
+	contentType, args, err := mime.ParseMediaType(ct)
24
+	if err != nil {
25
+		return "", nil, err
26
+	}
27
+
28
+	return contentType, args, nil
29
+}