Signed-off-by: Moysés Borges <moysesb@gmail.com>
| ... | ... |
@@ -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 |
+} |