Browse code

Fix build with `ADD` urls without any sub path

This fix tries to address the issue raised in #34208 where
in Dockerfile an `ADD` followed by an url without any sub path
will cause an error.

The issue is because the temporary filename relies on the sub path.

An integration test has been added.

This fix fixes #34208.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2017/07/22 08:54:29
Showing 2 changed files
... ...
@@ -3,6 +3,7 @@ package dockerfile
3 3
 import (
4 4
 	"fmt"
5 5
 	"io"
6
+	"mime"
6 7
 	"net/http"
7 8
 	"net/url"
8 9
 	"os"
... ...
@@ -24,6 +25,8 @@ import (
24 24
 	"github.com/pkg/errors"
25 25
 )
26 26
 
27
+const unnamedFilename = "__unnamed__"
28
+
27 29
 type pathCache interface {
28 30
 	Load(key interface{}) (value interface{}, ok bool)
29 31
 	Store(key, value interface{})
... ...
@@ -85,8 +88,7 @@ func (o *copier) createCopyInstruction(args []string, cmdName string) (copyInstr
85 85
 
86 86
 	// Work in daemon-specific filepath semantics
87 87
 	inst.dest = filepath.FromSlash(args[last])
88
-
89
-	infos, err := o.getCopyInfosForSourcePaths(args[0:last])
88
+	infos, err := o.getCopyInfosForSourcePaths(args[0:last], inst.dest)
90 89
 	if err != nil {
91 90
 		return inst, errors.Wrapf(err, "%s failed", cmdName)
92 91
 	}
... ...
@@ -99,10 +101,11 @@ func (o *copier) createCopyInstruction(args []string, cmdName string) (copyInstr
99 99
 
100 100
 // getCopyInfosForSourcePaths iterates over the source files and calculate the info
101 101
 // needed to copy (e.g. hash value if cached)
102
-func (o *copier) getCopyInfosForSourcePaths(sources []string) ([]copyInfo, error) {
102
+// The dest is used in case source is URL (and ends with "/")
103
+func (o *copier) getCopyInfosForSourcePaths(sources []string, dest string) ([]copyInfo, error) {
103 104
 	var infos []copyInfo
104 105
 	for _, orig := range sources {
105
-		subinfos, err := o.getCopyInfoForSourcePath(orig)
106
+		subinfos, err := o.getCopyInfoForSourcePath(orig, dest)
106 107
 		if err != nil {
107 108
 			return nil, err
108 109
 		}
... ...
@@ -115,7 +118,7 @@ func (o *copier) getCopyInfosForSourcePaths(sources []string) ([]copyInfo, error
115 115
 	return infos, nil
116 116
 }
117 117
 
118
-func (o *copier) getCopyInfoForSourcePath(orig string) ([]copyInfo, error) {
118
+func (o *copier) getCopyInfoForSourcePath(orig, dest string) ([]copyInfo, error) {
119 119
 	if !urlutil.IsURL(orig) {
120 120
 		return o.calcCopyInfo(orig, true)
121 121
 	}
... ...
@@ -123,6 +126,14 @@ func (o *copier) getCopyInfoForSourcePath(orig string) ([]copyInfo, error) {
123 123
 	if err != nil {
124 124
 		return nil, err
125 125
 	}
126
+	// If path == "" then we are unable to determine filename from src
127
+	// We have to make sure dest is available
128
+	if path == "" {
129
+		if strings.HasSuffix(dest, "/") {
130
+			return nil, errors.Errorf("cannot determine filename for source %s", orig)
131
+		}
132
+		path = unnamedFilename
133
+	}
126 134
 	o.tmpPaths = append(o.tmpPaths, remote.Root())
127 135
 
128 136
 	hash, err := remote.Hash(path)
... ...
@@ -301,22 +312,40 @@ func errOnSourceDownload(_ string) (builder.Source, string, error) {
301 301
 	return nil, "", errors.New("source can't be a URL for COPY")
302 302
 }
303 303
 
304
+func getFilenameForDownload(path string, resp *http.Response) string {
305
+	// Guess filename based on source
306
+	if path != "" && !strings.HasSuffix(path, "/") {
307
+		if filename := filepath.Base(filepath.FromSlash(path)); filename != "" {
308
+			return filename
309
+		}
310
+	}
311
+
312
+	// Guess filename based on Content-Disposition
313
+	if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" {
314
+		if _, params, err := mime.ParseMediaType(contentDisposition); err == nil {
315
+			if params["filename"] != "" && !strings.HasSuffix(params["filename"], "/") {
316
+				if filename := filepath.Base(filepath.FromSlash(params["filename"])); filename != "" {
317
+					return filename
318
+				}
319
+			}
320
+		}
321
+	}
322
+	return ""
323
+}
324
+
304 325
 func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote builder.Source, p string, err error) {
305 326
 	u, err := url.Parse(srcURL)
306 327
 	if err != nil {
307 328
 		return
308 329
 	}
309
-	filename := filepath.Base(filepath.FromSlash(u.Path)) // Ensure in platform semantics
310
-	if filename == "" {
311
-		err = errors.Errorf("cannot determine filename from url: %s", u)
312
-		return
313
-	}
314 330
 
315 331
 	resp, err := remotecontext.GetWithStatusError(srcURL)
316 332
 	if err != nil {
317 333
 		return
318 334
 	}
319 335
 
336
+	filename := getFilenameForDownload(u.Path, resp)
337
+
320 338
 	// Prepare file in a tmp dir
321 339
 	tmpDir, err := ioutils.TempDir("", "docker-remote")
322 340
 	if err != nil {
... ...
@@ -327,7 +356,13 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b
327 327
 			os.RemoveAll(tmpDir)
328 328
 		}
329 329
 	}()
330
-	tmpFileName := filepath.Join(tmpDir, filename)
330
+	// If filename is empty, the returned filename will be "" but
331
+	// the tmp filename will be created as "__unnamed__"
332
+	tmpFileName := filename
333
+	if filename == "" {
334
+		tmpFileName = unnamedFilename
335
+	}
336
+	tmpFileName = filepath.Join(tmpDir, tmpFileName)
331 337
 	tmpFile, err := os.OpenFile(tmpFileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
332 338
 	if err != nil {
333 339
 		return
... ...
@@ -6506,3 +6506,19 @@ RUN touch /foop
6506 6506
 	c.Assert(err, check.IsNil)
6507 6507
 	c.Assert(d.String(), checker.Equals, getIDByName(c, name))
6508 6508
 }
6509
+
6510
+// Test case for #34208
6511
+func (s *DockerSuite) TestBuildAddHTTPRoot(c *check.C) {
6512
+	testRequires(c, Network, DaemonIsLinux)
6513
+	buildImageSuccessfully(c, "buildaddhttproot", build.WithDockerfile(`
6514
+                FROM scratch
6515
+                ADD http://example.com/index.html /example1
6516
+                ADD http://example.com /example2
6517
+                ADD http://example.com /example3`))
6518
+	buildImage("buildaddhttprootfailure", build.WithDockerfile(`
6519
+                FROM scratch
6520
+                ADD http://example.com/ /`)).Assert(c, icmd.Expected{
6521
+		ExitCode: 1,
6522
+		Err:      "cannot determine filename for source http://example.com/",
6523
+	})
6524
+}