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>
| ... | ... |
@@ -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 |