Browse code

builder: fix `COPY --from` should preserve ownership

When copying between stages, or copying from an image,
ownership of the copied files should not be changed, unless
the `--chown` option is set (in which case ownership of copied
files should be updated to the specified user/group).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2018/12/19 06:30:08
Showing 5 changed files
... ...
@@ -64,6 +64,7 @@ type copyInstruction struct {
64 64
 	dest                    string
65 65
 	chownStr                string
66 66
 	allowLocalDecompression bool
67
+	preserveOwnership       bool
67 68
 }
68 69
 
69 70
 // copier reads a raw COPY or ADD command, fetches remote sources using a downloader,
... ...
@@ -466,7 +467,7 @@ func downloadSource(output io.Writer, stdout io.Writer, srcURL string) (remote b
466 466
 
467 467
 type copyFileOptions struct {
468 468
 	decompress bool
469
-	identity   idtools.Identity
469
+	identity   *idtools.Identity
470 470
 	archiver   Archiver
471 471
 }
472 472
 
... ...
@@ -532,7 +533,7 @@ func isArchivePath(driver containerfs.ContainerFS, path string) bool {
532 532
 	return err == nil
533 533
 }
534 534
 
535
-func copyDirectory(archiver Archiver, source, dest *copyEndpoint, identity idtools.Identity) error {
535
+func copyDirectory(archiver Archiver, source, dest *copyEndpoint, identity *idtools.Identity) error {
536 536
 	destExists, err := isExistingDirectory(dest)
537 537
 	if err != nil {
538 538
 		return errors.Wrapf(err, "failed to query destination path")
... ...
@@ -541,28 +542,40 @@ func copyDirectory(archiver Archiver, source, dest *copyEndpoint, identity idtoo
541 541
 	if err := archiver.CopyWithTar(source.path, dest.path); err != nil {
542 542
 		return errors.Wrapf(err, "failed to copy directory")
543 543
 	}
544
-	// TODO: @gupta-ak. Investigate how LCOW permission mappings will work.
545
-	return fixPermissions(source.path, dest.path, identity, !destExists)
544
+	if identity != nil {
545
+		// TODO: @gupta-ak. Investigate how LCOW permission mappings will work.
546
+		return fixPermissions(source.path, dest.path, *identity, !destExists)
547
+	}
548
+	return nil
546 549
 }
547 550
 
548
-func copyFile(archiver Archiver, source, dest *copyEndpoint, identity idtools.Identity) error {
551
+func copyFile(archiver Archiver, source, dest *copyEndpoint, identity *idtools.Identity) error {
549 552
 	if runtime.GOOS == "windows" && dest.driver.OS() == "linux" {
550 553
 		// LCOW
551 554
 		if err := dest.driver.MkdirAll(dest.driver.Dir(dest.path), 0755); err != nil {
552 555
 			return errors.Wrapf(err, "failed to create new directory")
553 556
 		}
554 557
 	} else {
555
-		if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest.path), 0755, identity); err != nil {
556
-			// Normal containers
557
-			return errors.Wrapf(err, "failed to create new directory")
558
+		if identity == nil {
559
+			if err := os.MkdirAll(filepath.Dir(dest.path), 0755); err != nil {
560
+				return err
561
+			}
562
+		} else {
563
+			if err := idtools.MkdirAllAndChownNew(filepath.Dir(dest.path), 0755, *identity); err != nil {
564
+				// Normal containers
565
+				return errors.Wrapf(err, "failed to create new directory")
566
+			}
558 567
 		}
559 568
 	}
560 569
 
561 570
 	if err := archiver.CopyFileWithTar(source.path, dest.path); err != nil {
562 571
 		return errors.Wrapf(err, "failed to copy file")
563 572
 	}
564
-	// TODO: @gupta-ak. Investigate how LCOW permission mappings will work.
565
-	return fixPermissions(source.path, dest.path, identity, false)
573
+	if identity != nil {
574
+		// TODO: @gupta-ak. Investigate how LCOW permission mappings will work.
575
+		return fixPermissions(source.path, dest.path, *identity, false)
576
+	}
577
+	return nil
566 578
 }
567 579
 
568 580
 func endsInSlash(driver containerfs.Driver, path string) bool {
... ...
@@ -127,7 +127,9 @@ func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error {
127 127
 		return err
128 128
 	}
129 129
 	copyInstruction.chownStr = c.Chown
130
-
130
+	if c.From != "" && copyInstruction.chownStr == "" {
131
+		copyInstruction.preserveOwnership = true
132
+	}
131 133
 	return d.builder.performCopy(d, copyInstruction)
132 134
 }
133 135
 
... ...
@@ -204,7 +204,9 @@ func (b *Builder) performCopy(req dispatchRequest, inst copyInstruction) error {
204 204
 		opts := copyFileOptions{
205 205
 			decompress: inst.allowLocalDecompression,
206 206
 			archiver:   b.getArchiver(info.root, destInfo.root),
207
-			identity:   identity,
207
+		}
208
+		if !inst.preserveOwnership {
209
+			opts.identity = &identity
208 210
 		}
209 211
 		if err := performCopyForInfo(destInfo, info, opts); err != nil {
210 212
 			return errors.Wrapf(err, "failed to copy files")
... ...
@@ -522,6 +522,45 @@ func TestBuildWithEmptyDockerfile(t *testing.T) {
522 522
 	}
523 523
 }
524 524
 
525
+func TestBuildPreserveOwnership(t *testing.T) {
526
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
527
+	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
528
+
529
+	ctx := context.Background()
530
+
531
+	dockerfile, err := ioutil.ReadFile("testdata/Dockerfile.testBuildPreserveOwnership")
532
+	assert.NilError(t, err)
533
+
534
+	source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
535
+	defer source.Close()
536
+
537
+	apiclient := testEnv.APIClient()
538
+
539
+	for _, target := range []string{"copy_from", "copy_from_chowned"} {
540
+		t.Run(target, func(t *testing.T) {
541
+			resp, err := apiclient.ImageBuild(
542
+				ctx,
543
+				source.AsTarReader(t),
544
+				types.ImageBuildOptions{
545
+					Remove:      true,
546
+					ForceRemove: true,
547
+					Target:      target,
548
+				},
549
+			)
550
+			assert.NilError(t, err)
551
+
552
+			out := bytes.NewBuffer(nil)
553
+			assert.NilError(t, err)
554
+			_, err = io.Copy(out, resp.Body)
555
+			_ = resp.Body.Close()
556
+			if err != nil {
557
+				t.Log(out)
558
+			}
559
+			assert.NilError(t, err)
560
+		})
561
+	}
562
+}
563
+
525 564
 func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
526 565
 	err := w.WriteHeader(&tar.Header{
527 566
 		Name:     fn,
528 567
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+# Set up files and directories with known ownership
1
+FROM busybox AS source
2
+RUN touch /file && chown 100:200 /file \
3
+ && mkdir -p /dir/subdir \
4
+ && touch /dir/subdir/nestedfile \
5
+ && chown 100:200 /dir \
6
+ && chown 101:201 /dir/subdir \
7
+ && chown 102:202 /dir/subdir/nestedfile
8
+
9
+FROM busybox AS test_base
10
+RUN mkdir -p /existingdir/existingsubdir \
11
+ && touch /existingdir/existingfile \
12
+ && chown 500:600 /existingdir \
13
+ && chown 501:601 /existingdir/existingsubdir \
14
+ && chown 501:601 /existingdir/existingfile
15
+
16
+
17
+# Copy files from the source stage
18
+FROM test_base AS copy_from
19
+COPY --from=source /file .
20
+# Copy to a non-existing target directory creates the target directory (as root), then copies the _contents_ of the source directory into it
21
+COPY --from=source /dir /dir
22
+# Copying to an existing target directory will copy the _contents_ of the source directory into it
23
+COPY --from=source /dir/. /existingdir
24
+
25
+RUN e="100:200"; p="/file"                         ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
26
+ && e="0:0";     p="/dir"                          ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
27
+ && e="101:201"; p="/dir/subdir"                   ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
28
+ && e="102:202"; p="/dir/subdir/nestedfile"        ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
29
+# Existing files and directories ownership should not be modified
30
+ && e="500:600"; p="/existingdir"                  ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
31
+ && e="501:601"; p="/existingdir/existingsubdir"   ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
32
+ && e="501:601"; p="/existingdir/existingfile"     ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
33
+# But new files and directories should maintain their ownership
34
+ && e="101:201"; p="/existingdir/subdir"           ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
35
+ && e="102:202"; p="/existingdir/subdir/nestedfile"; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi
36
+
37
+
38
+# Copy files from the source stage and chown them.
39
+FROM test_base AS copy_from_chowned
40
+COPY --from=source --chown=300:400 /file .
41
+# Copy to a non-existing target directory creates the target directory (as root), then copies the _contents_ of the source directory into it
42
+COPY --from=source --chown=300:400 /dir /dir
43
+# Copying to an existing target directory copies the _contents_ of the source directory into it
44
+COPY --from=source --chown=300:400 /dir/. /existingdir
45
+
46
+RUN e="300:400"; p="/file"                         ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
47
+ && e="300:400"; p="/dir"                          ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
48
+ && e="300:400"; p="/dir/subdir"                   ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
49
+ && e="300:400"; p="/dir/subdir/nestedfile"        ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
50
+# Existing files and directories ownership should not be modified
51
+ && e="500:600"; p="/existingdir"                  ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
52
+ && e="501:601"; p="/existingdir/existingsubdir"   ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
53
+ && e="501:601"; p="/existingdir/existingfile"     ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
54
+# But new files and directories should be chowned
55
+ && e="300:400"; p="/existingdir/subdir"           ; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi \
56
+ && e="300:400"; p="/existingdir/subdir/nestedfile"; a=`stat -c "%u:%g" "$p"`; if [ "$a" != "$e" ]; then echo "incorrect ownership on $p. expected $e, got $a"; exit 1; fi