Browse code

add support for COPY to docker build

This adds a COPY command to docker build which works like ADD, but is
only for local files and it doesn't extract files.

Docker-DCO-1.1-Signed-off-by: Cristian Staretu <cristian.staretu@gmail.com> (github: unclejack)

unclejack authored on 2014/05/29 02:53:16
Showing 16 changed files
... ...
@@ -290,6 +290,46 @@ The copy obeys the following rules:
290 290
 - If `<dest>` doesn't exist, it is created along with all missing directories
291 291
   in its path.
292 292
 
293
+## COPY
294
+
295
+    COPY <src> <dest>
296
+
297
+The `COPY` instruction will copy new files from `<src>` and add them to the
298
+container's filesystem at path `<dest>`.
299
+
300
+`<src>` must be the path to a file or directory relative to the source directory
301
+being built (also called the *context* of the build).
302
+
303
+`<dest>` is the absolute path to which the source will be copied inside the
304
+destination container.
305
+
306
+All new files and directories are created with a uid and gid of 0.
307
+
308
+> **Note**:
309
+> If you build using STDIN (`docker build - < somefile`), there is no
310
+> build context, so `COPY` can't be used.
311
+
312
+The copy obeys the following rules:
313
+
314
+- The `<src>` path must be inside the *context* of the build;
315
+  you cannot `COPY ../something /something`, because the first step of a
316
+  `docker build` is to send the context directory (and subdirectories) to the
317
+  docker daemon.
318
+
319
+- If `<src>` is a directory, the entire directory is copied, including
320
+  filesystem metadata.
321
+
322
+- If `<src>` is any other kind of file, it is copied individually along with
323
+  its metadata. In this case, if `<dest>` ends with a trailing slash `/`, it
324
+  will be considered a directory and the contents of `<src>` will be written
325
+  at `<dest>/base(<src>)`.
326
+
327
+- If `<dest>` does not end with a trailing slash, it will be considered a
328
+  regular file and the contents of `<src>` will be written at `<dest>`.
329
+
330
+- If `<dest>` doesn't exist, it is created along with all missing directories
331
+  in its path.
332
+
293 333
 ## ENTRYPOINT
294 334
 
295 335
 ENTRYPOINT has two forms:
296 336
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN mkdir /exists
4
+RUN touch /exists/exists_file
5
+RUN chown -R dockerio.dockerio /exists
6
+COPY test_dir/ /exists/
7
+RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
8
+RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
9
+RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ]
0 10
new file mode 100644
1 11
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN touch /exists
4
+RUN chown dockerio.dockerio exists
5
+COPY test_dir /
6
+RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ]
7
+RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
0 8
new file mode 100644
1 9
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+FROM busybox
1
+COPY https://index.docker.io/robots.txt /
0 2
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+FROM scratch
1
+COPY . /
0 2
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN mkdir /exists
4
+RUN touch /exists/exists_file
5
+RUN chown -R dockerio.dockerio /exists
6
+COPY test_file /exists/
7
+RUN [ $(ls -l / | grep exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
8
+RUN [ $(ls -l /exists/test_file | awk '{print $3":"$4}') = 'root:root' ]
9
+RUN [ $(ls -l /exists/exists_file | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
0 10
new file mode 100644
1 11
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN touch /exists
4
+RUN chown dockerio.dockerio /exists
5
+COPY test_file /test_dir/
6
+RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ]
7
+RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ]
8
+RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
0 9
new file mode 100644
1 10
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN touch /exists
4
+RUN chown dockerio.dockerio /exists
5
+COPY test_file /
6
+RUN [ $(ls -l /test_file | awk '{print $3":"$4}') = 'root:root' ]
7
+RUN [ $(ls -l /test_file | awk '{print $1}') = '-rw-r--r--' ]
8
+RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
0 9
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+FROM busybox
1
+COPY test_file .
0 2
new file mode 100644
... ...
@@ -0,0 +1,11 @@
0
+FROM busybox
1
+RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd
2
+RUN echo 'dockerio:x:1001:' >> /etc/group
3
+RUN touch /exists
4
+RUN chown dockerio.dockerio exists
5
+COPY test_dir /test_dir
6
+RUN [ $(ls -l / | grep test_dir | awk '{print $3":"$4}') = 'root:root' ]
7
+RUN [ $(ls -l / | grep test_dir | awk '{print $1}') = 'drwxr-xr-x' ]
8
+RUN [ $(ls -l /test_dir/test_file | awk '{print $3":"$4}') = 'root:root' ]
9
+RUN [ $(ls -l /test_dir/test_file | awk '{print $1}') = '-rw-r--r--' ]
10
+RUN [ $(ls -l /exists | awk '{print $3":"$4}') = 'dockerio:dockerio' ]
... ...
@@ -237,6 +237,181 @@ func TestAddEtcToRoot(t *testing.T) {
237 237
 	logDone("build - add etc directory to root")
238 238
 }
239 239
 
240
+func TestCopySingleFileToRoot(t *testing.T) {
241
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy", "SingleFileToRoot")
242
+	f, err := os.OpenFile(filepath.Join(buildDirectory, "test_file"), os.O_CREATE, 0644)
243
+	if err != nil {
244
+		t.Fatal(err)
245
+	}
246
+	f.Close()
247
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", ".")
248
+	buildCmd.Dir = buildDirectory
249
+	out, exitCode, err := runCommandWithOutput(buildCmd)
250
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
251
+
252
+	if err != nil || exitCode != 0 {
253
+		t.Fatal("failed to build the image")
254
+	}
255
+
256
+	deleteImages("testcopyimg")
257
+
258
+	logDone("build - copy single file to root")
259
+}
260
+
261
+// Issue #3960: "ADD src ." hangs - adapted for COPY
262
+func TestCopySingleFileToWorkdir(t *testing.T) {
263
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy", "SingleFileToWorkdir")
264
+	f, err := os.OpenFile(filepath.Join(buildDirectory, "test_file"), os.O_CREATE, 0644)
265
+	if err != nil {
266
+		t.Fatal(err)
267
+	}
268
+	f.Close()
269
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", ".")
270
+	buildCmd.Dir = buildDirectory
271
+	done := make(chan error)
272
+	go func() {
273
+		out, exitCode, err := runCommandWithOutput(buildCmd)
274
+		if err != nil || exitCode != 0 {
275
+			done <- fmt.Errorf("build failed to complete: %s %v", out, err)
276
+			return
277
+		}
278
+		done <- nil
279
+	}()
280
+	select {
281
+	case <-time.After(5 * time.Second):
282
+		if err := buildCmd.Process.Kill(); err != nil {
283
+			fmt.Printf("could not kill build (pid=%d): %v\n", buildCmd.Process.Pid, err)
284
+		}
285
+		t.Fatal("build timed out")
286
+	case err := <-done:
287
+		if err != nil {
288
+			t.Fatal(err)
289
+		}
290
+	}
291
+
292
+	deleteImages("testcopyimg")
293
+
294
+	logDone("build - copy single file to workdir")
295
+}
296
+
297
+func TestCopySingleFileToExistDir(t *testing.T) {
298
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
299
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "SingleFileToExistDir")
300
+	buildCmd.Dir = buildDirectory
301
+	out, exitCode, err := runCommandWithOutput(buildCmd)
302
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
303
+
304
+	if err != nil || exitCode != 0 {
305
+		t.Fatal("failed to build the image")
306
+	}
307
+
308
+	deleteImages("testcopyimg")
309
+
310
+	logDone("build - add single file to existing dir")
311
+}
312
+
313
+func TestCopySingleFileToNonExistDir(t *testing.T) {
314
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
315
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "SingleFileToNonExistDir")
316
+	buildCmd.Dir = buildDirectory
317
+	out, exitCode, err := runCommandWithOutput(buildCmd)
318
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
319
+
320
+	if err != nil || exitCode != 0 {
321
+		t.Fatal("failed to build the image")
322
+	}
323
+
324
+	deleteImages("testcopyimg")
325
+
326
+	logDone("build - copy single file to non-existing dir")
327
+}
328
+
329
+func TestCopyDirContentToRoot(t *testing.T) {
330
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
331
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "DirContentToRoot")
332
+	buildCmd.Dir = buildDirectory
333
+	out, exitCode, err := runCommandWithOutput(buildCmd)
334
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
335
+
336
+	if err != nil || exitCode != 0 {
337
+		t.Fatal("failed to build the image")
338
+	}
339
+
340
+	deleteImages("testcopyimg")
341
+
342
+	logDone("build - copy directory contents to root")
343
+}
344
+
345
+func TestCopyDirContentToExistDir(t *testing.T) {
346
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
347
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "DirContentToExistDir")
348
+	buildCmd.Dir = buildDirectory
349
+	out, exitCode, err := runCommandWithOutput(buildCmd)
350
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
351
+
352
+	if err != nil || exitCode != 0 {
353
+		t.Fatal("failed to build the image")
354
+	}
355
+
356
+	deleteImages("testcopyimg")
357
+
358
+	logDone("build - copy directory contents to existing dir")
359
+}
360
+
361
+func TestCopyWholeDirToRoot(t *testing.T) {
362
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy", "WholeDirToRoot")
363
+	test_dir := filepath.Join(buildDirectory, "test_dir")
364
+	if err := os.MkdirAll(test_dir, 0755); err != nil {
365
+		t.Fatal(err)
366
+	}
367
+	f, err := os.OpenFile(filepath.Join(test_dir, "test_file"), os.O_CREATE, 0644)
368
+	if err != nil {
369
+		t.Fatal(err)
370
+	}
371
+	f.Close()
372
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", ".")
373
+	buildCmd.Dir = buildDirectory
374
+	out, exitCode, err := runCommandWithOutput(buildCmd)
375
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
376
+
377
+	if err != nil || exitCode != 0 {
378
+		t.Fatal("failed to build the image")
379
+	}
380
+
381
+	deleteImages("testcopyimg")
382
+
383
+	logDone("build - copy whole directory to root")
384
+}
385
+
386
+func TestCopyEtcToRoot(t *testing.T) {
387
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
388
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "EtcToRoot")
389
+	buildCmd.Dir = buildDirectory
390
+	out, exitCode, err := runCommandWithOutput(buildCmd)
391
+	errorOut(err, t, fmt.Sprintf("build failed to complete: %v %v", out, err))
392
+
393
+	if err != nil || exitCode != 0 {
394
+		t.Fatal("failed to build the image")
395
+	}
396
+
397
+	deleteImages("testcopyimg")
398
+	logDone("build - copy etc directory to root")
399
+}
400
+
401
+func TestCopyDisallowRemote(t *testing.T) {
402
+	buildDirectory := filepath.Join(workingDirectory, "build_tests", "TestCopy")
403
+	buildCmd := exec.Command(dockerBinary, "build", "-t", "testcopyimg", "DisallowRemote")
404
+	buildCmd.Dir = buildDirectory
405
+	out, exitCode, err := runCommandWithOutput(buildCmd)
406
+
407
+	if err == nil || exitCode == 0 {
408
+		t.Fatalf("building the image should've failed; output: %s", out)
409
+	}
410
+
411
+	deleteImages("testcopyimg")
412
+	logDone("build - copy - disallow copy from remote")
413
+}
414
+
240 415
 // Issue #5270 - ensure we throw a better error than "unexpected EOF"
241 416
 // when we can't access files in the context.
242 417
 func TestBuildWithInaccessibleFilesInContext(t *testing.T) {
... ...
@@ -340,7 +340,7 @@ func (b *buildFile) CmdInsert(args string) error {
340 340
 }
341 341
 
342 342
 func (b *buildFile) CmdCopy(args string) error {
343
-	return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
343
+	return b.runContextCommand(args, false, false, "COPY")
344 344
 }
345 345
 
346 346
 func (b *buildFile) CmdWorkdir(workdir string) error {
... ...
@@ -399,7 +399,7 @@ func (b *buildFile) checkPathForAddition(orig string) error {
399 399
 	return nil
400 400
 }
401 401
 
402
-func (b *buildFile) addContext(container *daemon.Container, orig, dest string, remote bool) error {
402
+func (b *buildFile) addContext(container *daemon.Container, orig, dest string, decompress bool) error {
403 403
 	var (
404 404
 		err        error
405 405
 		destExists = true
... ...
@@ -439,8 +439,8 @@ func (b *buildFile) addContext(container *daemon.Container, orig, dest string, r
439 439
 		return copyAsDirectory(origPath, destPath, destExists)
440 440
 	}
441 441
 
442
-	// If we are adding a remote file, do not try to untar it
443
-	if !remote {
442
+	// If we are adding a remote file (or we've been told not to decompress), do not try to untar it
443
+	if decompress {
444 444
 		// First try to unpack the source as an archive
445 445
 		// to support the untar feature we need to clean up the path a little bit
446 446
 		// because tar is very forgiving.  First we need to strip off the archive's
... ...
@@ -473,13 +473,13 @@ func (b *buildFile) addContext(container *daemon.Container, orig, dest string, r
473 473
 	return fixPermissions(resPath, 0, 0)
474 474
 }
475 475
 
476
-func (b *buildFile) CmdAdd(args string) error {
476
+func (b *buildFile) runContextCommand(args string, allowRemote bool, allowDecompression bool, cmdName string) error {
477 477
 	if b.context == nil {
478
-		return fmt.Errorf("No context given. Impossible to use ADD")
478
+		return fmt.Errorf("No context given. Impossible to use %s", cmdName)
479 479
 	}
480 480
 	tmp := strings.SplitN(args, " ", 2)
481 481
 	if len(tmp) != 2 {
482
-		return fmt.Errorf("Invalid ADD format")
482
+		return fmt.Errorf("Invalid %s format", cmdName)
483 483
 	}
484 484
 
485 485
 	orig, err := b.ReplaceEnvMatches(strings.Trim(tmp[0], " \t"))
... ...
@@ -493,7 +493,7 @@ func (b *buildFile) CmdAdd(args string) error {
493 493
 	}
494 494
 
495 495
 	cmd := b.config.Cmd
496
-	b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
496
+	b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, orig, dest)}
497 497
 	defer func(cmd []string) { b.config.Cmd = cmd }(cmd)
498 498
 	b.config.Image = b.image
499 499
 
... ...
@@ -502,11 +502,14 @@ func (b *buildFile) CmdAdd(args string) error {
502 502
 		destPath   = dest
503 503
 		remoteHash string
504 504
 		isRemote   bool
505
+		decompress = true
505 506
 	)
506 507
 
507
-	if utils.IsURL(orig) {
508
+	isRemote = utils.IsURL(orig)
509
+	if isRemote && !allowRemote {
510
+		return fmt.Errorf("Source can't be an URL for %s", cmdName)
511
+	} else if utils.IsURL(orig) {
508 512
 		// Initiate the download
509
-		isRemote = true
510 513
 		resp, err := utils.Download(orig)
511 514
 		if err != nil {
512 515
 			return err
... ...
@@ -608,7 +611,7 @@ func (b *buildFile) CmdAdd(args string) error {
608 608
 				hash = "file:" + h
609 609
 			}
610 610
 		}
611
-		b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", hash, dest)}
611
+		b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) %s %s in %s", cmdName, hash, dest)}
612 612
 		hit, err := b.probeCache()
613 613
 		if err != nil {
614 614
 			return err
... ...
@@ -631,16 +634,23 @@ func (b *buildFile) CmdAdd(args string) error {
631 631
 	}
632 632
 	defer container.Unmount()
633 633
 
634
-	if err := b.addContext(container, origPath, destPath, isRemote); err != nil {
634
+	if !allowDecompression || isRemote {
635
+		decompress = false
636
+	}
637
+	if err := b.addContext(container, origPath, destPath, decompress); err != nil {
635 638
 		return err
636 639
 	}
637 640
 
638
-	if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
641
+	if err := b.commit(container.ID, cmd, fmt.Sprintf("%s %s in %s", cmdName, orig, dest)); err != nil {
639 642
 		return err
640 643
 	}
641 644
 	return nil
642 645
 }
643 646
 
647
+func (b *buildFile) CmdAdd(args string) error {
648
+	return b.runContextCommand(args, true, true, "ADD")
649
+}
650
+
644 651
 func (b *buildFile) create() (*daemon.Container, error) {
645 652
 	if b.image == "" {
646 653
 		return nil, fmt.Errorf("Please provide a source image with `from` prior to run")