Repository: uber/makisu
Pull request #322 (2020-04-23 15:16:31)

Update README.md


Patch:
diff --git a/README.md b/README.md
index 99ae97f..a8e3ea1 100644
--- a/README.md
+++ b/README.md
@@ -182,7 +182,7 @@ It works very well with a subset of Docker build scenarios given a Bazel build f
 ### Kaniko
 
 Kaniko provides good compatibility with Docker and executes build commands in userspace without the need for Docker daemon, although it must still run inside a container. Kaniko offers smooth integration with Kubernetes, making it a competent tool for Kubernetes users.
-On the other hand, Makisu has some performance tweaks for large images (especially those with node_modules), allows cache to expire, and offers more control over cache generation through #!COMMIT, make it optimal for complex workflows.
+On the other hand, Makisu has some performance tweaks for large images with multi-phase builds by avoiding unnecessary disk scans, and offers more control over cache generation and layer size through #!COMMIT, make it optimal for complex workflows.
 
 ### BuildKit / img
 
diff --git a/lib/client/client.go b/lib/client/client.go
index 207524a..7fdeb1e 100644
--- a/lib/client/client.go
+++ b/lib/client/client.go
@@ -30,7 +30,6 @@ import (
 
 	"github.com/uber/makisu/lib/fileio"
 	"github.com/uber/makisu/lib/log"
-	"github.com/uber/makisu/lib/utils"
 )
 
 // MakisuClient is the struct that allows communication with a makisu worker.
@@ -149,14 +148,9 @@ func (cli *MakisuClient) prepareContext(context string) (string, error) {
 	targetContext := fmt.Sprintf("context-%d", rand.Intn(10000))
 	targetPath := filepath.Join(cli.LocalSharedPath, targetContext)
 
-	uid, gid, err := utils.GetUIDGID()
-	if err != nil {
-		return "", err
-	}
-
 	log.Infof("Copying context to worker filesystem: %s => %s", context, targetPath)
 	start := time.Now()
-	if err := fileio.NewCopier(nil).CopyDir(context, targetPath, uid, gid); err != nil {
+	if err := fileio.NewCopier([]string{}).CopyDir(context, targetPath); err != nil {
 		return "", err
 	}
 	log.Infow("Copied context", "duration", time.Since(start))
diff --git a/lib/fileio/copy.go b/lib/fileio/copy.go
index a524b7e..73ae5dd 100644
--- a/lib/fileio/copy.go
+++ b/lib/fileio/copy.go
@@ -29,130 +29,138 @@ import (
 )
 
 // Copier provides helper functions that copy files and directories to specified
-// locations.
-type Copier interface {
-	CopyFile(source, target string, uid, gid int) error
-	CopyDir(source, target string, uid, gid int) error
-	CopyDirPreserveOwner(source, target string) error
-	CopyFilePreserveOwner(source, target string) error
+// locations. Common behaviors regardless of the parameters:
+// - If target diretory's ancestors do not exist, they will be created with
+//   default 0755 permission and owned by root.
+// - If target directory already exists, its permission and owner will be
+//   preserved.
+// - Symlinks are copied with original targets (not guaranteed to be valid).
+//
+// Then there are 4 scenarios for handling file/directory permissions:
+// - ADD/COPY without flags, from context:
+//   - If target directory doesn't exist, it will be created with default 0755
+//     permission and owned by root.
+//   - For directories and files under source, permissions will be preserved,
+//     and owners will be root.
+//   - Use dstDirOwner without overwrite and dstFileAndChildrenOwner with
+//     overwrite, set uid:gid to 0:0.
+// - ADD/COPY --from, without other flags:
+//   - If target directory doesn't exist, it will be created with default 0755
+//     permission and owned by root.
+//   - For directories and files under source, permissions and owners will be
+//     preserved.
+//   - Leave dstDirOwner and dstFileAndChildrenOwner empty.
+// - ADD/COPY --chown, with or wihout --from:
+//   - If target directory doesn't exist, it will be created with default 0755
+//     permission and owned by given uid/gid.
+//   - For directories and files under source, permissions are kept, but owner
+//     will be changed to given uid/gid.
+//   - Use dstDirOwner without overwrite and dstFileAndChildrenOwner with
+//     overwrite.
+// - ADD/COPY --from --archive:
+//   - If target directory doesn't exist, it will be created with default 0755
+//     permission and owned by source dir's uid/gid (directly given as copier
+//     parameters).
+//   - For directories and files under source, permissions and owners will be
+//     preserved.
+//   - Use dstDirOwner without overwrite and source dir's uid/gid.
+type Copier struct {
+	blacklist []string
+
+	// Owner info for dst dir.
+	dstDirOwner *Owner
+	// Owner info for dst dir's children, or if dst is to be a file.
+	dstFileAndChildrenOwner *Owner
 }
 
-type copier struct {
-	blacklist []string
+// Owner is a tuple of uid+gid, and a flag to indicate whether to overwrite
+// existing owner.
+type Owner struct {
+	uid       int
+	gid       int
+	overwrite bool
 }
 
 // NewCopier initializes a new copier object. Files from provided blacklist will
 // be ignored.
-func NewCopier(blacklist []string) Copier {
-	return &copier{
+func NewCopier(blacklist []string, opts ...CopyOption) *Copier {
+	c := &Copier{
 		blacklist: append(blacklist),
 	}
+	for _, opt := range opts {
+		opt(c)
+	}
+	return c
 }
 
-// NewInternalCopier initializes a new copier object. It is used for copying
-// checkpointed files from sandbox dir, and there is no need to blacklist any
-// path, since they would have been filtered out by checkpoint.
-func NewInternalCopier() Copier {
-	return &copier{}
+type CopyOption func(*Copier)
+
+func WithDstDirOwner(uid, gid int, overwrite bool) CopyOption {
+	return func(c *Copier) {
+		c.dstDirOwner = &Owner{
+			uid:       uid,
+			gid:       gid,
+			overwrite: overwrite,
+		}
+	}
 }
 
-// CopyFile copies the content and permissions of the file at src to dst.
-// If the target file exists, its contents and permissions will be replaced.
-// If the parent directories of dst do not exist, they will be created with
-// the given uid/gid.
-// For symlinks, the link target will be copied as-is.
-func (c copier) CopyFile(source, target string, uid, gid int) error {
-	// Make target parent directories (uid and gid will be computed from the sources one).
-	targetDir := filepath.Dir(target)
-	if err := mkdirAll(targetDir, os.ModePerm, uid, gid, false); err != nil {
-		return fmt.Errorf("mkdir all %s: %s", targetDir, err)
+func WithDstFileAndChildrenOwner(uid, gid int, overwrite bool) CopyOption {
+	return func(c *Copier) {
+		c.dstFileAndChildrenOwner = &Owner{
+			uid:       uid,
+			gid:       gid,
+			overwrite: overwrite,
+		}
 	}
-	// Copy file permissions and contents.
-	return c.copyFile(source, target, uid, gid, false)
 }
 
-// CopyFilePreserveOwner follow the behavior of CopyFile but preserve file rights.
-func (c copier) CopyFilePreserveOwner(source, target string) error {
-	// Make target parent directories with passed uid & gid if they don't exist.
+// CopyFile copies the content and permissions of the file at src to dst.
+// If the target file exists, its contents and permissions might be overwritten,
+// depending on copier attributes.
+// For symlinks, the link target will be copied as-is.
+func (c *Copier) CopyFile(source, target string) error {
+	// Make target parent directories (uid and gid will be computed from the sources one).
 	targetDir := filepath.Dir(target)
-	if err := mkdirAll(targetDir, os.ModePerm, 0, 0, true); err != nil {
+	if err := c.mkdirAll(targetDir); err != nil {
 		return fmt.Errorf("mkdir all %s: %s", targetDir, err)
 	}
 	// Copy file permissions and contents.
-	return c.copyFile(source, target, 0, 0, true)
+	return c.copyFile(source, target)
 }
 
 // CopyDir recursively copies the directory at source to target. The source
 // directory must exist, and the target doesn't need to exist but must be a
-// directory if it does. If the target or any of its ancestors do not exist,
-// they are created with default permissions and owned by the given uid/gid.
-// Permissions and ownership are preserved for directories and files under source.
-// Symlinks are copied with their original target (not guaranteed to be valid).
+// directory if it does. Existing children files/dirs will be overwritten.
+// Permissions and owners depend on copier attributes.
 //
-// If src contains dst, this function would break infinite loop silently.
+// Note: If src contains dst, this function would break infinite loop silently.
 // This is needed to defend against scenarios like:
 //   COPY --from=stage1 / /
 // where / will be stashed to some child directory at the end of stage1, and
 // causes infinite loop.
-func (c copier) CopyDir(source, target string, uid, gid int) error {
+func (c *Copier) CopyDir(source, target string) error {
 	if c.isBlacklisted(source) {
 		// Ignore this directory since it's blacklisted.
 		log.Infof("* Ignoring copy of directory %s because it is blacklisted", source)
 		return nil
 	}
 
-	// Make target parent directories with passed uid & gid if they don't exist.
-	if err := mkdirAll(target, os.ModePerm, uid, gid, false); err != nil {
+	// Make target directory and parent directories.
+	if err := c.mkdirAll(target); err != nil {
 		return fmt.Errorf("mkdir all %s: %s", target, err)
 	}
 
 	// Recursively copy contents of source directory.
-	return c.copyDirContents(source, target, target, uid, gid, false)
-}
-
-// CopyDirPreserveOwner follow the behavior of CopyFile but preserve file rights.
-func (c copier) CopyDirPreserveOwner(source, target string) error {
-	if c.isBlacklisted(source) {
-		// Ignore this directory since it's blacklisted.
-		log.Infof("* Ignoring copy of directory %s because it is blacklisted", source)
-		return nil
-	}
-
-	// Use stat instead of Lstat here.
-	// If the source is a symlink, makisu would try to copy the contents rather than the link.
-	fi, _ := os.Stat(source)
-	sourceUid, sourceGid := fileOwners(fi)
-	if _, err := os.Lstat(target); err != nil {
-		if !os.IsNotExist(err) {
-			return fmt.Errorf("stat %s: %s", target, err)
-		}
-
-		// Copy the parent directory of target.
-		targetParentDir := filepath.Dir(target)
-		if err := mkdirAll(targetParentDir, os.ModePerm, 0, 0, true); err != nil {
-			return fmt.Errorf("mkdir parent dir %s: %s", targetParentDir, err)
-		}
-
-		// Preserve the source dir permission and ownership.
-		if err := os.Mkdir(target, fi.Mode()); err != nil {
-			return fmt.Errorf("mkdir %s: %s", target, err)
-		}
-
-		if err := os.Chown(target, sourceUid, sourceGid); err != nil {
-			return fmt.Errorf("chown %s: %s", target, err)
-		}
-	}
-
-	// Recursively copy contents of source directory.
-	return c.copyDirContents(source, target, target, 0, 0, true)
+	return c.copyDirContents(source, target, target)
 }
 
-func (c copier) isBlacklisted(source string) bool {
+func (c *Copier) isBlacklisted(source string) bool {
 	return pathutils.IsDescendantOfAny(source, c.blacklist)
 }
 
 // copyFile copies the permissions and contents of the file at src to dst.
-func (c copier) copyFile(src, dst string, uid, gid int, preserveOwner bool) error {
+func (c *Copier) copyFile(src, dst string) error {
 	fi, err := os.Lstat(src)
 	if err != nil {
 		return fmt.Errorf("lstat %s: %s", src, err)
@@ -179,16 +187,12 @@ func (c copier) copyFile(src, dst string, uid, gid int, preserveOwner bool) erro
 		}
 	}
 
-	if preserveOwner {
-		uid, gid = fileOwners(fi)
-	}
-
 	// Handle regular files.
-	return c.copyRegularFile(fi, src, dst, uid, gid)
+	return c.copyRegularFile(fi, src, dst)
 }
 
 // Open both files, creating dst if need be.
-func (c copier) copyRegularFile(fi os.FileInfo, src, dst string, uid, gid int) error {
+func (c *Copier) copyRegularFile(fi os.FileInfo, src, dst string) error {
 	r, err := os.Open(src)
 	if err != nil {
 		return fmt.Errorf("open %s: %s", dst, err)
@@ -208,9 +212,14 @@ func (c copier) copyRegularFile(fi os.FileInfo, src, dst string, uid, gid int) e
 		return fmt.Errorf("copy %s to %s: %s", src, dst, err)
 	}
 
-	// Change the owner and mode of dst to that of src.
+	// Change the mode of dst to that of src, and update owner accordingly.
 	// Note: Chmod needs to be called after chown, otherwise setuid and setgid
 	// bits could be unset.
+	uid, gid := getFileOwners(fi)
+	if c.dstFileAndChildrenOwner != nil && c.dstFileAndChildrenOwner.overwrite {
+		uid = c.dstFileAndChildrenOwner.uid
+		gid = c.dstFileAndChildrenOwner.gid
+	}
 	if err := os.Chown(dst, uid, gid); err != nil {
 		return fmt.Errorf("chown %s: %s", dst, err)
 	}
@@ -220,7 +229,7 @@ func (c copier) copyRegularFile(fi os.FileInfo, src, dst string, uid, gid int) e
 	return nil
 }
 
-func (c copier) copySymlink(src, dst string) error {
+func (c *Copier) copySymlink(src, dst string) error {
 	// Remove existing file if path exists.
 	if _, err := os.Lstat(dst); err == nil {
 		if err := os.Remove(dst); err != nil {
@@ -238,8 +247,9 @@ func (c copier) copySymlink(src, dst string) error {
 	return nil
 }
 
-// copyDirContents recursively copies the contents of directory src to dst. Both must exist.
-func (c copier) copyDirContents(src, dst, origDst string, uid, gid int, preserveOwner bool) error {
+// copyDirContents recursively copies the contents of directory src to dst.
+// Both must exist.
+func (c *Copier) copyDirContents(src, dst, origDst string) error {
 	entries, err := ioutil.ReadDir(src)
 	if err != nil {
 		return fmt.Errorf("read dir %s: %s", src, err)
@@ -256,14 +266,14 @@ func (c copier) copyDirContents(src, dst, origDst string, uid, gid int, preserve
 		}
 		currDst := filepath.Join(dst, entry.Name())
 		if entry.IsDir() {
-			if err := c.copyDir(currSrc, currDst, uid, gid, preserveOwner); err != nil {
+			if err := c.copyDir(currSrc, currDst); err != nil {
 				return fmt.Errorf("copy dir %s to %s: %s", currSrc, currDst, err)
 			}
-			if err := c.copyDirContents(currSrc, currDst, origDst, uid, gid, preserveOwner); err != nil {
+			if err := c.copyDirContents(currSrc, currDst, origDst); err != nil {
 				return fmt.Errorf("copy dir contents %s to %s: %s", currSrc, currDst, err)
 			}
 		} else {
-			if err := c.copyFile(currSrc, currDst, uid, gid, preserveOwner); err != nil {
+			if err := c.copyFile(currSrc, currDst); err != nil {
 				return fmt.Errorf("copy file %s to %s: %s", currSrc, currDst, err)
 			}
 		}
@@ -272,7 +282,8 @@ func (c copier) copyDirContents(src, dst, origDst string, uid, gid int, preserve
 }
 
 // copyDir copies the directory at src to dst.
-func (c copier) copyDir(src, dst string, uid, gid int, preserveOwner bool) error {
+// Note: This dst is not the dst dir of public CopyDir(), but a descendent.
+func (c *Copier) copyDir(src, dst string) error {
 	srcInfo, err := os.Lstat(src)
 	if err != nil {
 		return fmt.Errorf("lstat %s: %s", src, err)
@@ -284,7 +295,8 @@ func (c copier) copyDir(src, dst string, uid, gid int, preserveOwner bool) error
 		return nil
 	}
 
-	// Make the dst directory with src's mode if it doesn't exist, else chmod it to the same.
+	// Create the dst directory with src's mode if it doesn't exist, else chmod it
+	// to the same.
 	dstInfo, err := os.Lstat(dst)
 	if os.IsNotExist(err) {
 		if err := os.Mkdir(dst, srcInfo.Mode()); err != nil {
@@ -296,26 +308,32 @@ func (c copier) copyDir(src, dst string, uid, gid int, preserveOwner bool) error
 		return fmt.Errorf("dst is not a directory")
 	}
 
-	// Change owner and mode of dst to that of src.
+	// Change mode of dst to that of src, and change owner of dst accordingly.
 	// Note: Chmod needs to be called after chown, otherwise setuid and setgid
 	// bits could be unset.
-	if preserveOwner {
-		uid, gid = fileOwners(srcInfo)
+	if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
+		return fmt.Errorf("chmod %s: %s", dst, err)
+	}
+	uid, gid := getFileOwners(srcInfo)
+	if c.dstFileAndChildrenOwner != nil && c.dstFileAndChildrenOwner.overwrite {
+		uid = c.dstFileAndChildrenOwner.uid
+		gid = c.dstFileAndChildrenOwner.gid
 	}
 	if err := os.Chown(dst, uid, gid); err != nil {
 		return fmt.Errorf("chown %s: %s", dst, err)
 	}
-	if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
-		return fmt.Errorf("chmod %s: %s", dst, err)
-	}
+
 	return nil
 }
 
-// mkdirAll performs the same operation as os.MkdirAll, but also sets the given
-// permissions & owners on all created directories.
-func mkdirAll(dst string, mode os.FileMode, uid, gid int, preserveParentOwner bool) error {
+// mkdirAll performs the same operation as os.MkdirAll, except it also changes
+// permission and owner of dst directory.
+// - Parent directories will have 0755 permission and owner by root.
+// - Target directory's permission and owner will be decided by copier
+//   attributes.
+func (c *Copier) mkdirAll(dst string) error {
 	if dst == "" {
-		return errors.New("empty target directory")
+		return errors.New("empty dst directory")
 	}
 	abs, err := filepath.Abs(filepath.Clean(dst))
 	if err != nil {
@@ -325,33 +343,52 @@ func mkdirAll(dst string, mode os.FileMode, uid, gid int, preserveParentOwner bo
 	split := strings.Split(abs, "/")
 	split[0] = "/"
 	var prevDir string
-	for _, dir := range split {
-		absDir := filepath.Join(prevDir, dir)
-		if fi, err := os.Lstat(absDir); err != nil {
+	for _, dir := range split[:len(split)-1] {
+		currDir := filepath.Join(prevDir, dir)
+		if _, err := os.Lstat(currDir); err != nil {
 			if !os.IsNotExist(err) {
-				return fmt.Errorf("stat %s: %s", absDir, err)
+				return fmt.Errorf("stat %s: %s", currDir, err)
+			}
+
+			// Create dir with default mode and owner.
+			if err := os.Mkdir(currDir, 0755); err != nil {
+				return fmt.Errorf("mkdir %s with default mode 0755: %s", currDir, err)
 			}
-			if err := os.Mkdir(absDir, mode); err != nil {
-				return fmt.Errorf("mkdir %s: %s", absDir, err)
+			if err := os.Chown(currDir, 0, 0); err != nil {
+				return fmt.Errorf("chown %s with default owner (0:0): %s", currDir, err)
 			}
+		}
+		prevDir = currDir
+	}
 
-			// Update file info.
-			fi, _ = os.Lstat(prevDir)
-			if preserveParentOwner {
-				uid, gid = fileOwners(fi)
+	if _, err := os.Lstat(abs); err != nil {
+		if !os.IsNotExist(err) {
+			return fmt.Errorf("stat %s: %s", abs, err)
+		}
+		// Create dst dir with default mode and specified owner.
+		if err := os.Mkdir(abs, 0755); err != nil {
+			return fmt.Errorf("mkdir %s with default mode 0755: %s", abs, err)
+		}
+
+		if c.dstDirOwner != nil {
+			if err := os.Chown(abs, c.dstDirOwner.uid, c.dstDirOwner.gid); err != nil {
+				return fmt.Errorf(
+					"chown %s with owner (%d:%d): %s", abs, c.dstDirOwner.uid, c.dstDirOwner.gid, err)
 			}
-			if err := os.Chown(absDir, uid, gid); err != nil {
-				return fmt.Errorf("chown %s: %s", absDir, err)
+		} else {
+			if err := os.Chown(abs, 0, 0); err != nil {
+				return fmt.Errorf("chown %s with default owner (0:0): %s", abs, err)
+			}
+		}
+	} else {
+		if c.dstDirOwner != nil && c.dstDirOwner.overwrite {
+			// Change dst dir with specified owner.
+			if err := os.Chown(abs, c.dstDirOwner.uid, c.dstDirOwner.gid); err != nil {
+				return fmt.Errorf(
+					"chown %s with owner (%d:%d): %s", abs, c.dstDirOwner.uid, c.dstDirOwner.gid, err)
 			}
 		}
-		prevDir = absDir
 	}
 
 	return nil
 }
-
-// fileOwners returns the uid & gid that own the file.
-func fileOwners(fi os.FileInfo) (uid int, gid int) {
-	stat := utils.FileInfoStat(fi)
-	return int(stat.Uid), int(stat.Gid)
-}
diff --git a/lib/fileio/utils.go b/lib/fileio/utils.go
index f2b249c..52b6b2b 100644
--- a/lib/fileio/utils.go
+++ b/lib/fileio/utils.go
@@ -21,8 +21,16 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+
+	"github.com/uber/makisu/lib/utils"
 )
 
+// getFileOwners returns the uid & gid that own the file.
+func getFileOwners(fi os.FileInfo) (uid int, gid int) {
+	stat := utils.FileInfoStat(fi)
+	return int(stat.Uid), int(stat.Gid)
+}
+
 // ReaderToFile copies the data from a reader to a destination file.
 func ReaderToFile(r io.Reader, dst string) error {
 	dst = filepath.Clean(dst)
diff --git a/lib/parser/dockerfile/arg.go b/lib/parser/dockerfile/arg.go
index e488cad..0516131 100644
--- a/lib/parser/dockerfile/arg.go
+++ b/lib/parser/dockerfile/arg.go
@@ -45,7 +45,7 @@ func newArgDirective(base *baseDirective, state *parsingState) (Directive, error
 		return &ArgDirective{base, name, defaultVal, nil}, nil
 	}
 
-	args, err := splitArgs(base.Args)
+	args, err := splitArgs(base.Args, false)
 	if err != nil {
 		return nil, base.err(err)
 	}
diff --git a/lib/parser/dockerfile/cmd.go b/lib/parser/dockerfile/cmd.go
index 9ad5859..b7206a3 100644
--- a/lib/parser/dockerfile/cmd.go
+++ b/lib/parser/dockerfile/cmd.go
@@ -34,7 +34,7 @@ func newCmdDirective(base *baseDirective, state *parsingState) (Directive, error
 		return &CmdDirective{base, cmd}, nil
 	}
 
-	args, err := splitArgs(base.Args)
+	args, err := splitArgs(base.Args, false)
 	if err != nil {
 		return nil, base.err(err)
 	}
diff --git a/lib/parser/dockerfile/entrypoint.go b/lib/parser/dockerfile/entrypoint.go
index 726df64..694d842 100644
--- a/lib/parser/dockerfile/entrypoint.go
+++ b/lib/parser/dockerfile/entrypoint.go
@@ -14,6 +14,8 @@
 
 package dockerfile
 
+import "strings"
+
 // EntrypointDirective represents the "ENTRYPOINT" dockerfile command.
 type EntrypointDirective struct {
 	*baseDirective
@@ -34,11 +36,17 @@ func newEntrypointDirective(base *baseDirective, state *parsingState) (Directive
 		return &EntrypointDirective{base, entrypoint}, nil
 	}
 
-	args, err := splitArgs(base.Args)
+	// This is the Shell form (https://docs.docker.com/engine/reference/builder/#shell-form-entrypoint-example)
+	// It is expected to wrap the whole entrypoint into a sh -c command
+	var shell []string
+	shell = append(shell, "/bin/sh", "-c")
+	args, err := splitArgs(base.Args, true)
 	if err != nil {
 		return nil, base.err(err)
 	}
-	return &EntrypointDirective{base, args}, nil
+
+	cmd := append(shell, strings.Join(args, " "))
+	return &EntrypointDirective{base, cmd}, nil
 }
 
 // Add this command to the build stage.
diff --git a/lib/parser/dockerfile/healthcheck.go b/lib/parser/dockerfile/healthcheck.go
index 19476ca..5757446 100644
--- a/lib/parser/dockerfile/healthcheck.go
+++ b/lib/parser/dockerfile/healthcheck.go
@@ -54,7 +54,7 @@ func newHealthcheckDirective(base *baseDirective, state *parsingState) (Directiv
 		return nil, base.err(fmt.Errorf("CMD not defined"))
 	}
 
-	flags, err := splitArgs(base.Args[:cmdIndices[0]])
+	flags, err := splitArgs(base.Args[:cmdIndices[0]], false)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse interval")
 	}
@@ -133,7 +133,7 @@ func newHealthcheckDirective(base *baseDirective, state *parsingState) (Directiv
 	}
 
 	// Verify cmd arg is a valid array, but return the whole arg as one string.
-	args, err := splitArgs(remaining)
+	args, err := splitArgs(remaining, false)
 	if err != nil {
 		return nil, base.err(err)
 	}
diff --git a/lib/parser/dockerfile/split_args.go b/lib/parser/dockerfile/split_args.go
index 8514d6b..6ce7ef1 100644
--- a/lib/parser/dockerfile/split_args.go
+++ b/lib/parser/dockerfile/split_args.go
@@ -22,10 +22,10 @@ import (
 
 // splitArgs splits a whitespace-delimited string into an array of arguments,
 // not splitting quoted arguments.
-func splitArgs(input string) ([]string, error) {
+func splitArgs(input string, keepQuotes bool) ([]string, error) {
 	var err error
 	var state splitArgsState = &splitArgsStateSpace{
-		&splitArgsBase{args: make([]string, 0)},
+		&splitArgsBase{args: make([]string, 0), keepQuotes: keepQuotes},
 	}
 	for i := 0; i < len(input); i++ {
 		state, err = state.nextRune(rune(input[i]))
@@ -45,9 +45,10 @@ type splitArgsState interface {
 
 // splitArgsBase defines pieces of data that are used & managed by each state.
 type splitArgsBase struct {
-	args    []string
-	currArg string
-	escaped bool
+	args       []string
+	currArg    string
+	escaped    bool
+	keepQuotes bool
 }
 
 // splitArgsStateSpace is the starting state for the state machine. It should be entered
@@ -60,6 +61,9 @@ func (s *splitArgsStateSpace) nextRune(r rune) (splitArgsState, error) {
 	if unicode.IsSpace(r) {
 		return s, nil
 	} else if r == '"' {
+		if s.keepQuotes {
+			s.currArg += "\""
+		}
 		return &splitArgsStateQuote{s.splitArgsBase}, nil
 	} else if r == '\\' {
 		s.escaped = true
@@ -107,7 +111,7 @@ type splitArgsStateQuote struct{ *splitArgsBase }
 // transitioning to splitArgsStateEndQuote.
 func (s *splitArgsStateQuote) nextRune(r rune) (splitArgsState, error) {
 	if s.escaped {
-		if r != '"' {
+		if r != '"' || s.keepQuotes {
 			s.currArg += "\\"
 		}
 		s.escaped = false
@@ -115,6 +119,9 @@ func (s *splitArgsStateQuote) nextRune(r rune) (splitArgsState, error) {
 		s.escaped = true
 		return s, nil
 	} else if r == '"' {
+		if s.keepQuotes {
+			s.currArg += "\""
+		}
 		s.args = append(s.args, s.currArg)
 		s.currArg = ""
 		return &splitArgsStateEndQuote{s.splitArgsBase}, nil
diff --git a/lib/snapshot/copy_op.go b/lib/snapshot/copy_op.go
index 90682d0..0f4a736 100644
--- a/lib/snapshot/copy_op.go
+++ b/lib/snapshot/copy_op.go
@@ -32,6 +32,7 @@ type CopyOperation struct {
 	dst           string
 	uid           int
 	gid           int
+	chown         bool
 	preserveOwner bool
 
 	blacklist []string
@@ -42,16 +43,20 @@ type CopyOperation struct {
 // NewCopyOperation initializes and validates a CopyOperation. Use "internal" to
 // specify if the copy op is used for copying from previous stages.
 func NewCopyOperation(
-	srcs []string, srcRoot, workDir, dst, chown string,
+	srcs []string, srcRoot, workDir, dst, chownStr string,
 	blacklist []string, internal, preserveOwner bool) (*CopyOperation, error) {
 
 	if err := checkCopyParams(srcs, workDir, dst); err != nil {
 		return nil, fmt.Errorf("check copy param: %s", err)
 	}
+	chown := chownStr != ""
+	if chown && preserveOwner {
+		return nil, fmt.Errorf("both chown and archive are true")
+	}
 
-	uid, gid, err := utils.ResolveChown(chown)
+	uid, gid, err := utils.ResolveChown(chownStr)
 	if err != nil {
-		return nil, fmt.Errorf("resolve chown: %s", err)
+		return nil, fmt.Errorf("resolve chown str: %s", err)
 	}
 
 	relSources := make([]string, len(srcs))
@@ -67,6 +72,7 @@ func NewCopyOperation(
 		dst:           dst,
 		uid:           uid,
 		gid:           gid,
+		chown:         chown,
 		preserveOwner: preserveOwner,
 		blacklist:     blacklist,
 		internal:      internal,
@@ -86,47 +92,55 @@ func (c *CopyOperation) Execute() error {
 		if err != nil {
 			return fmt.Errorf("lstat %s: %s", src, err)
 		}
-		var copier fileio.Copier
+
+		blacklist := c.blacklist
 		if c.internal {
-			copier = fileio.NewInternalCopier()
+			// Copying checkpointed files from sandbox dir, and there is no need to
+			// blacklist any path, since they would have been filtered out by checkpoint.
+			blacklist = []string{}
+		}
+
+		var copier *fileio.Copier
+		if c.chown {
+			// COPY --chown.
+			// Owner decided by --chown.
+			copier = fileio.NewCopier(blacklist,
+				fileio.WithDstDirOwner(c.uid, c.gid, false),
+				fileio.WithDstFileAndChildrenOwner(c.uid, c.gid, true),
+			)
+		} else if !c.internal {
+			// Copying from context, owner should be root if no --chown.
+			// Whether --archive is provided doesn't matter in this case.
+			copier = fileio.NewCopier(blacklist,
+				fileio.WithDstDirOwner(0, 0, false),
+				fileio.WithDstFileAndChildrenOwner(0, 0, true),
+			)
+		} else if c.preserveOwner {
+			// COPY --from --archive.
+			stat := utils.FileInfoStat(fi)
+			copier = fileio.NewCopier(blacklist,
+				fileio.WithDstDirOwner(int(stat.Uid), int(stat.Gid), false))
 		} else {
-			copier = fileio.NewCopier(c.blacklist)
+			// COPY --from.
+			copier = fileio.NewCopier(blacklist)
 		}
+
 		if fi.IsDir() {
 			// Dir to dir
-			if c.preserveOwner {
-				if err := copier.CopyDirPreserveOwner(src, c.dst); err != nil {
-					return fmt.Errorf("copy dir %s to dir %s: %s", src, c.dst, err)
-				}
-			} else {
-				if err := copier.CopyDir(src, c.dst, c.uid, c.gid); err != nil {
-					return fmt.Errorf("copy dir %s to dir %s: %s", src, c.dst, err)
-				}
+			if err := copier.CopyDir(src, c.dst); err != nil {
+				return fmt.Errorf("copy dir %s to dir %s: %s", src, c.dst, err)
 			}
 		} else if isDirFormat(c.dst) {
 			// File to dir
 			targetFilePath := filepath.Join(c.dst, filepath.Base(src))
-			if c.preserveOwner {
-				if err := copier.CopyFilePreserveOwner(src, targetFilePath); err != nil {
-					return fmt.Errorf("copy file %s to dir %s: %s", src, targetFilePath, err)
-				}
-			} else {
-				if err := copier.CopyFile(src, targetFilePath, c.uid, c.gid); err != nil {
-					return fmt.Errorf("copy file %s to dir %s: %s", src, targetFilePath, err)
-				}
+			if err := copier.CopyFile(src, targetFilePath); err != nil {
+				return fmt.Errorf("copy file %s to dir %s: %s", src, targetFilePath, err)
 			}
 		} else {
 			// File to file
-			if c.preserveOwner {
-				if err := copier.CopyFilePreserveOwner(src, c.dst); err != nil {
-					return fmt.Errorf("copy file %s to dir %s: %s", src, c.dst, err)
-				}
-			} else {
-				if err := copier.CopyFile(src, c.dst, c.uid, c.gid); err != nil {
-					return fmt.Errorf("copy file %s to file %s: %s", src, c.dst, err)
-				}
+			if err := copier.CopyFile(src, c.dst); err != nil {
+				return fmt.Errorf("copy file %s to file %s: %s", src, c.dst, err)
 			}
-
 		}
 	}
 	return nil
diff --git a/lib/snapshot/mem_fs.go b/lib/snapshot/mem_fs.go
index 6417f72..7c44540 100644
--- a/lib/snapshot/mem_fs.go
+++ b/lib/snapshot/mem_fs.go
@@ -30,6 +30,7 @@ import (
 	"github.com/uber/makisu/lib/mountutils"
 	"github.com/uber/makisu/lib/pathutils"
 	"github.com/uber/makisu/lib/tario"
+	"github.com/uber/makisu/lib/utils"
 )
 
 // memFSNode represents one node of the directory tree in the merged fs view.
@@ -101,7 +102,6 @@ func (fs *MemFS) Checkpoint(newRoot string, sources []string) error {
 	}
 
 	log.Infof("* Moving directories %v to %s", sources, newRoot)
-	copier := fileio.NewCopier(fs.blacklist)
 	for _, src := range resolvedSources {
 		if !filepath.IsAbs(src) {
 			src = filepath.Join(fs.tree.src, src)
@@ -116,12 +116,20 @@ func (fs *MemFS) Checkpoint(newRoot string, sources []string) error {
 			return fmt.Errorf("stat %s: %s", src, err)
 		}
 
+		fi, err := os.Lstat(src)
+		if err != nil {
+			return fmt.Errorf("lstat %s: %s", src, err)
+		}
+		stat := utils.FileInfoStat(fi)
+		copier := fileio.NewCopier(fs.blacklist,
+			fileio.WithDstDirOwner(int(stat.Uid), int(stat.Gid), false))
+
 		if sourceInfo.IsDir() {
-			if err := copier.CopyDirPreserveOwner(src, dst); err != nil {
+			if err := copier.CopyDir(src, dst); err != nil {
 				return fmt.Errorf("copy dir %s: %s", src, err)
 			}
 		} else {
-			if err := copier.CopyFilePreserveOwner(src, dst); err != nil {
+			if err := copier.CopyFile(src, dst); err != nil {
 				return fmt.Errorf("copy file %s: %s", src, err)
 			}
 		}
diff --git a/lib/storage/root_preserver.go b/lib/storage/root_preserver.go
index 8e5a467..5b759c0 100644
--- a/lib/storage/root_preserver.go
+++ b/lib/storage/root_preserver.go
@@ -58,7 +58,7 @@ func copyOldRootToBackup(rootDir, backupDir string, blacklist []string) error {
 	}
 
 	// TODO: Handle uid, gid preservation
-	if err := copier.CopyDirPreserveOwner(rootDir, backupDir); err != nil {
+	if err := copier.CopyDir(rootDir, backupDir); err != nil {
 		return fmt.Errorf("copy dir %s to dir %s: %s", rootDir, backupDir, err)
 	}
 
@@ -70,7 +70,7 @@ func (r *RootPreserver) RestoreRoot() error {
 	copier := fileio.NewCopier(r.blacklist)
 
 	// TODO: Handle uid, gid preservation
-	if err := copier.CopyDirPreserveOwner(r.SavedRootDir, r.InitialRootDir); err != nil {
+	if err := copier.CopyDir(r.SavedRootDir, r.InitialRootDir); err != nil {
 		return fmt.Errorf("copy dir %s to dir %s: %s", r.SavedRootDir, r.InitialRootDir, err)
 	}
 


Test patch:
diff --git a/lib/fileio/copy_test.go b/lib/fileio/copy_test.go
index 45d599b..6889e83 100644
--- a/lib/fileio/copy_test.go
+++ b/lib/fileio/copy_test.go
@@ -53,7 +53,7 @@ func TestCopyFileDanglingSymlink(t *testing.T) {
 	defer target.Close()
 
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyFile(sourceSymlink, target.Name(), currUID, currGID))
+	require.NoError(c.CopyFile(sourceSymlink, target.Name()))
 
 	result, err := os.Readlink(target.Name())
 	require.NoError(err)
@@ -78,7 +78,7 @@ func TestCopyFileTargetNotExist(t *testing.T) {
 	require.NoError(err)
 
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyFile(source.Name(), target.Name(), currUID, currGID))
+	require.NoError(c.CopyFile(source.Name(), target.Name()))
 
 	result, err := ioutil.ReadFile(target.Name())
 	require.NoError(err)
@@ -105,7 +105,7 @@ func TestCopyFileSetSpecialBit(t *testing.T) {
 	defer os.Remove(target.Name())
 
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyFile(source.Name(), target.Name(), currUID, currGID))
+	require.NoError(c.CopyFile(source.Name(), target.Name()))
 
 	result, err := ioutil.ReadFile(target.Name())
 	require.NoError(err)
@@ -132,7 +132,7 @@ func TestCopyFileTargetEmpty(t *testing.T) {
 	require.NoError(err)
 
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyFile(source.Name(), target.Name(), currUID, currGID))
+	require.NoError(c.CopyFile(source.Name(), target.Name()))
 
 	result, err := ioutil.ReadFile(target.Name())
 	require.NoError(err)
@@ -158,7 +158,7 @@ func TestCopyFileTargetOverwrite(t *testing.T) {
 	require.NoError(err)
 
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyFile(source.Name(), target.Name(), currUID, currGID))
+	require.NoError(c.CopyFile(source.Name(), target.Name()))
 
 	result, err := ioutil.ReadFile(target.Name())
 	require.NoError(err)
@@ -192,7 +192,7 @@ func TestCopyDirectoryTargetNotExist(t *testing.T) {
 
 	// Perform copy.
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyDir(sourceDir, targetDir, currUID, currGID))
+	require.NoError(c.CopyDir(sourceDir, targetDir))
 
 	// Verify.
 	_, err = os.Stat(path.Join(targetDir, path.Base(sourceSubDirOne)))
@@ -241,7 +241,7 @@ func TestCopyDirectoryTargetExists(t *testing.T) {
 
 	// Perform copy.
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyDir(sourceDir, targetDir, currUID, currGID))
+	require.NoError(c.CopyDir(sourceDir, targetDir))
 
 	// Verify.
 	_, err = os.Stat(path.Join(targetDir, path.Base(sourceSubDirOne)))
@@ -286,7 +286,7 @@ func TestCopyDirectoryIncludingSymlink(t *testing.T) {
 
 	// Perform copy.
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyDir(sourceDir, targetDir, currUID, currGID))
+	require.NoError(c.CopyDir(sourceDir, targetDir))
 
 	// Verify.
 	_, err = os.Stat(path.Join(targetDir, path.Base(sourceSubDirOne)))
@@ -332,7 +332,7 @@ func TestCopyDirectoryInfiniteLoop(t *testing.T) {
 
 	// Perform copy.
 	c := NewCopier(pathutils.DefaultBlacklist)
-	require.NoError(c.CopyDir(sourceDir, targetDir, currUID, currGID))
+	require.NoError(c.CopyDir(sourceDir, targetDir))
 
 	// Verify.
 	_, err = os.Stat(path.Join(targetDir, path.Base(sourceSubDirOne)))
diff --git a/lib/parser/dockerfile/entrypoint_test.go b/lib/parser/dockerfile/entrypoint_test.go
index 94a1868..c9e8841 100644
--- a/lib/parser/dockerfile/entrypoint_test.go
+++ b/lib/parser/dockerfile/entrypoint_test.go
@@ -33,8 +33,9 @@ func TestNewEntrypointDirective(t *testing.T) {
 		{"good json", true, `entrypoint ["this", "entrypoint"]`, []string{"this", "entrypoint"}},
 		{"substitution", true, `entrypoint ["${prefix}this", "entrypoint${suffix}"]`, []string{"test_this", "entrypoint_test"}},
 		{"substitution2", true, `entrypoint ["this"$comma "entrypoint"]`, []string{"this", "entrypoint"}},
-		{"good entrypoint", true, "entrypoint this entrypoint", []string{"this", "entrypoint"}},
-		{"substitution", true, "entrypoint ${prefix}this entrypoint$suffix", []string{"test_this", "entrypoint_test"}},
+		{"good entrypoint", true, "entrypoint this entrypoint", []string{"/bin/sh", "-c", "this entrypoint"}},
+		{"substitution", true, "entrypoint ${prefix}this entrypoint$suffix", []string{"/bin/sh", "-c", "test_this entrypoint_test"}},
+		{"substitution", true, `entrypoint "${prefix}this" entrypoint$suffix`, []string{"/bin/sh", "-c", `"test_this" entrypoint_test`}},
 		{"bad json", false, `entrypoint ["this, "entrypoint"]`, nil},
 		{"bad substitution", false, `entrypoint ["${prefixthis", "entrypoint${suffix}"]`, nil},
 	}
diff --git a/lib/parser/dockerfile/split_args_test.go b/lib/parser/dockerfile/split_args_test.go
index bc5eb36..ccff8a7 100644
--- a/lib/parser/dockerfile/split_args_test.go
+++ b/lib/parser/dockerfile/split_args_test.go
@@ -22,24 +22,28 @@ import (
 
 func TestSplitArgs(t *testing.T) {
 	tests := []struct {
-		desc    string
-		input   string
-		succeed bool
-		output  []string
+		desc       string
+		input      string
+		keepQuotes bool
+		succeed    bool
+		output     []string
 	}{
-		{"single", "  a	", true, []string{"a"}},
-		{"single quoted", `  "a   "	`, true, []string{"a   "}},
-		{"quoted contains backslash", `  "a \  "	`, true, []string{`a \  `}},
-		{"quoted contains quote", `  "a \"  "	`, true, []string{`a "  `}},
-		{"single quoted incomplete", `"a `, false, nil},
-		{"multiple", `a=b   c d  "e f" \"g`, true, []string{"a=b", "c", "d", "e f", "\"g"}},
-		{"arg chars", "`1'{}$@#! *&@)(*_&", true, []string{"`1'{}$@#!", "*&@)(*_&"}},
-		{"non-escapes", "\\`1'{}$\\@#! *&\\;)\\(*_&", true, []string{"\\`1'{}$\\@#!", "*&\\;)\\(*_&"}},
+		{"single", "  a	", false, true, []string{"a"}},
+		{"single quoted", `  "a   "	`, false, true, []string{"a   "}},
+		{"quoted contains backslash", `  "a \  "	`, false, true, []string{`a \  `}},
+		{"quoted contains quote", `  "a \"  "	`, false, true, []string{`a "  `}},
+		{"single quoted incomplete", `"a `, false, false, nil},
+		{"multiple", `a=b   c d  "e f" \"g`, false, true, []string{"a=b", "c", "d", "e f", "\"g"}},
+		{"arg chars", "`1'{}$@#! *&@)(*_&", false, true, []string{"`1'{}$@#!", "*&@)(*_&"}},
+		{"non-escapes", "\\`1'{}$\\@#! *&\\;)\\(*_&", false, true, []string{"\\`1'{}$\\@#!", "*&\\;)\\(*_&"}},
+		{"keep quotes", `echo "${MESSAGE}" "multi \"word"`, true, true, []string{"echo", `"${MESSAGE}"`, `"multi \"word"`}},
+		{"non-escapes", `echo "${MESSAGE}" "multi \"word"`, true, true, []string{"echo", `"${MESSAGE}"`, `"multi \"word"`}},
+		{"non-escapes", `echo "single argument \\\"keepQuotes\\\""`, true, true, []string{"echo", `"single argument \\\"keepQuotes\\\""`}},
 	}
 	for _, test := range tests {
 		t.Run(test.desc, func(t *testing.T) {
 			require := require.New(t)
-			result, err := splitArgs(test.input)
+			result, err := splitArgs(test.input, test.keepQuotes)
 			if test.succeed {
 				require.NoError(err)
 				require.Equal(test.output, result)
diff --git a/lib/snapshot/copy_op_test.go b/lib/snapshot/copy_op_test.go
index 7337839..a7d71f6 100644
--- a/lib/snapshot/copy_op_test.go
+++ b/lib/snapshot/copy_op_test.go
@@ -94,7 +94,7 @@ func TestExecuteCopyOperation(t *testing.T) {
 		srcs := []string{"/test.txt"}
 		dst := filepath.Join(workDir, "test2/test.txt")
 		c, err := NewCopyOperation(
-			srcs, srcRoot, "", dst, validChown, pathutils.DefaultBlacklist, false, true)
+			srcs, srcRoot, "", dst, validChown, pathutils.DefaultBlacklist, false, false)
 		require.NoError(err)
 		require.NoError(c.Execute())
 		b, err := ioutil.ReadFile(dst)
@@ -174,7 +174,7 @@ func TestExecuteCopyOperation(t *testing.T) {
 		workDir = filepath.Join(workDir, "test2")
 		dst := "."
 		c, err := NewCopyOperation(
-			srcs, srcRoot, workDir, dst, validChown, pathutils.DefaultBlacklist, false, true)
+			srcs, srcRoot, workDir, dst, validChown, pathutils.DefaultBlacklist, false, false)
 		require.NoError(err)
 		require.NoError(c.Execute())
 		b, err := ioutil.ReadFile(filepath.Join(workDir, "test.txt"))
diff --git a/testdata/build-context/copy-from/1/1.txt b/testdata/build-context/copy-from/1/1.txt
new file mode 100644
index 0000000..e69de29
diff --git a/testdata/build-context/copy-from/Dockerfile b/testdata/build-context/copy-from/Dockerfile
index d708326..7e3d921 100644
--- a/testdata/build-context/copy-from/Dockerfile
+++ b/testdata/build-context/copy-from/Dockerfile
@@ -12,35 +12,60 @@ COPY --from=debian:8 /bin/bash /bash
 RUN ls -la /bash
 
 FROM alpine as phaseA
-RUN mkdir /mine
-RUN touch /mine/a.txt
-RUN touch /mine/b.txt #!COMMIT
-RUN mkdir /mine/c
-RUN touch /mine/c/1.txt
-RUN touch /mine/c/2.txt
+# Setup copy source.
+RUN mkdir /src
+RUN touch /src/a.txt
+RUN touch /src/b.txt #!COMMIT
+RUN mkdir /src/c && chown news:news /src/c
+RUN touch /src/c/1.txt && chown news:news /src/c/1.txt && chmod 770 /src/c/1.txt
+RUN touch /src/c/2.txt
 
 FROM alpine
-RUN mkdir /mine
-RUN touch /mine/a.txt
-RUN touch /mine/b.txt #!COMMIT
-# Copy files that were not cached from previous stage.
-COPY --from=phaseA /mine/c/ /mine/c/
-RUN cat /mine/c/1.txt && cat /mine/c/2.txt
-RUN rm -rf /mine/c/ #!COMMIT
+RUN mkdir /src
+RUN touch /src/a.txt
+RUN touch /src/b.txt #!COMMIT
+# Copy files that were not committed from previous stage.
+COPY --from=phaseA /src/c/ /src/c/
+RUN cat /src/c/1.txt && cat /src/c/2.txt
+RUN rm -rf /src/c/ #!COMMIT
 
 FROM alpine AS phaseB
-RUN mkdir /mine
-# Copy files again that were deleted from previous stage.
-COPY --from=phaseA /mine/c/ /mine/c/
-RUN cat /mine/c/1.txt && cat /mine/c/2.txt
+RUN mkdir /dst
 # Copy to existing dir with changed permission.
-RUN mkdir /mine/d && chown daemon:daemon /mine/d && chmod 777 /mine/d
-COPY --from=phaseA --archive /mine/c/ /mine/d/
-RUN mkdir /mine/e && chown daemon:daemon /mine/e
-COPY --from=phaseA /mine/c/ /mine/e/
+RUN mkdir /dst/a && chown mail:mail /dst/a && chmod 777 /dst/a
+COPY --from=phaseA /src/c/ /dst/a/
+RUN mkdir /dst/b && chown daemon:daemon /dst/b && chmod 777 /dst/b
+COPY --from=phaseA --archive /src/c/ /dst/b/
+RUN mkdir /dst/c && chown games:games /dst/c && chmod 777 /dst/c
+COPY --from=phaseA --chown=ftp:ftp /src/c/ /dst/c/
+RUN mkdir /dst/d && chown man:man /dst/d && chmod 777 /dst/d
+COPY --chown=ftp:ftp 1/ /dst/d/
+# Copy to non-existing dir.
+COPY --from=phaseA /src/c/ /dst/dst/a
+COPY --from=phaseA --archive /src/c/ /dst/dst/b
+COPY --from=phaseA --chown=ftp:ftp /src/c/ /dst/dst/c
+COPY --chown=ftp:ftp 1/ /dst/dst/d
 
 FROM alpine
 RUN mkdir /mine
 # Copy root.
-COPY --from=phaseB --archive / /
-ENTRYPOINT ["/bin/sh", "-c", "cat /mine/c/1.txt && cat /mine/c/2.txt && cat /mine/d/1.txt && cat /mine/d/2.txt && test $(stat -c '%U:%G' /mine/d) = 'daemon:daemon' && test $(stat -c '%a' /mine/d) = 777 && test $(stat -c '%U:%G' /mine/e) = 'daemon:daemon'"]
+COPY --from=phaseB / /
+# Verify existing dir.
+RUN cat /dst/a/2.txt && test $(stat -c '%U:%G' /dst/a/1.txt) = 'news:news' && test $(stat -c '%a' /dst/a/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/a) = 'mail:mail' && test $(stat -c '%a' /dst/a) = 777
+RUN cat /dst/b/2.txt && test $(stat -c '%U:%G' /dst/b/1.txt) = 'news:news' && test $(stat -c '%a' /dst/b/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/b) = 'daemon:daemon' && test $(stat -c '%a' /dst/b) = 777
+RUN cat /dst/c/1.txt && cat /dst/c/2.txt && test $(stat -c '%U:%G' /dst/c/1.txt) = 'ftp:ftp' && test $(stat -c '%a' /dst/c/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/c) = 'games:games' && test $(stat -c '%a' /dst/c) = 777
+RUN cat /dst/d/1.txt
+RUN test $(stat -c '%U:%G' /dst/d) = 'man:man' && test $(stat -c '%a' /dst/d) = 777
+# Verify non-existing dir.
+RUN cat /dst/dst/a/2.txt && test $(stat -c '%U:%G' /dst/dst/a/1.txt) = 'news:news' && test $(stat -c '%a' /dst/dst/a/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/dst/a) = 'root:root' && test $(stat -c '%a' /dst/dst/a) = 755
+RUN cat /dst/dst/b/2.txt && test $(stat -c '%U:%G' /dst/dst/b/1.txt) = 'news:news' && test $(stat -c '%a' /dst/dst/b/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/dst/b) = 'news:news' && test $(stat -c '%a' /dst/dst/b) = 755
+RUN cat /dst/dst/c/1.txt && cat /dst/dst/c/2.txt && test $(stat -c '%U:%G' /dst/dst/c/1.txt) = 'ftp:ftp' && test $(stat -c '%a' /dst/dst/c/1.txt) = 770
+RUN test $(stat -c '%U:%G' /dst/dst/c) = 'ftp:ftp' && test $(stat -c '%a' /dst/dst/c) = 755
+RUN cat /dst/dst/d/1.txt
+RUN test $(stat -c '%U:%G' /dst/dst/d) = 'ftp:ftp' && test $(stat -c '%a' /dst/dst/d) = 755
+ENTRYPOINT ["/bin/sh", "-c", "echo hello"]