浏览代码

integration-cli: New `docker cp` integration tests

Adds several integration tests for `docker cp` behavior with over a dozen
tests for each of:

  container -> local
  local -> container

Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
Josh Hawn 10 年之前
父节点
当前提交
418135e7ea

+ 503 - 0
integration-cli/docker_cli_cp_from_container_test.go

@@ -0,0 +1,503 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+
+	"github.com/go-check/check"
+)
+
+// docker cp CONTAINER:PATH LOCALPATH
+
+// Try all of the test cases from the archive package which implements the
+// internals of `docker cp` and ensure that the behavior matches when actually
+// copying to and from containers.
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func (s *DockerSuite) TestCpFromErrSrcNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-src-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	err := runDockerCp(c, containerCpPath(cID, "file1"), tmpDir)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func (s *DockerSuite) TestCpFromErrSrcNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-src-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	err := runDockerCp(c, containerCpPathTrailingSep(cID, "file1"), tmpDir)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// bu the DST parent directory does not exist.
+func (s *DockerSuite) TestCpFromErrDstParentNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-dst-parent-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := containerCpPath(cID, "/file1")
+	dstPath := cpPath(tmpDir, "notExists", "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = containerCpPath(cID, "/dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing
+// path separator but exists as a file.
+func (s *DockerSuite) TestCpFromErrDstNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-err-dst-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := containerCpPath(cID, "/file1")
+	dstPath := cpPathTrailingSep(tmpDir, "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = containerCpPath(cID, "/dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func (s *DockerSuite) TestCpFromCaseA(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-a")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := containerCpPath(cID, "/root/file1")
+	dstPath := cpPath(tmpDir, "itWorks.txt")
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func (s *DockerSuite) TestCpFromCaseB(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-b")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := containerCpPath(cID, "/file1")
+	dstDir := cpPathTrailingSep(tmpDir, "testDir")
+
+	err := runDockerCp(c, srcPath, dstDir)
+	if err == nil {
+		c.Fatal("expected DirNotExists error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func (s *DockerSuite) TestCpFromCaseC(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-c")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := containerCpPath(cID, "/root/file1")
+	dstPath := cpPath(tmpDir, "file2")
+
+	// Ensure the local file starts with different content.
+	if err := fileContentEquals(c, dstPath, "file2\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseD(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-d")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := containerCpPath(cID, "/file1")
+	dstDir := cpPath(tmpDir, "dir1")
+	dstPath := filepath.Join(dstDir, "file1")
+
+	// Ensure that dstPath doesn't exist.
+	if _, err := os.Stat(dstPath); !os.IsNotExist(err) {
+		c.Fatalf("did not expect dstPath %q to exist", dstPath)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir1")
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func (s *DockerSuite) TestCpFromCaseE(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-e")
+	defer os.RemoveAll(tmpDir)
+
+	srcDir := containerCpPath(cID, "dir1")
+	dstDir := cpPath(tmpDir, "testDir")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func (s *DockerSuite) TestCpFromCaseF(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-f")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPath(cID, "/root/dir1")
+	dstFile := cpPath(tmpDir, "file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseG(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-g")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPath(cID, "/root/dir1")
+	dstDir := cpPath(tmpDir, "dir2")
+	resultDir := filepath.Join(dstDir, "dir1")
+	dstPath := filepath.Join(resultDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir2")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseH(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-h")
+	defer os.RemoveAll(tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "dir1") + "."
+	dstDir := cpPath(tmpDir, "testDir")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove resultDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func (s *DockerSuite) TestCpFromCaseI(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-i")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "/root/dir1") + "."
+	dstFile := cpPath(tmpDir, "file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func (s *DockerSuite) TestCpFromCaseJ(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-from-case-j")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := containerCpPathTrailingSep(cID, "/root/dir1") + "."
+	dstDir := cpPath(tmpDir, "dir2")
+	dstPath := filepath.Join(dstDir, "file1-1")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	if err := os.RemoveAll(dstDir); err != nil {
+		c.Fatalf("unable to remove dstDir: %s", err)
+	}
+
+	if err := os.MkdirAll(dstDir, os.FileMode(0755)); err != nil {
+		c.Fatalf("unable to make dstDir: %s", err)
+	}
+
+	dstDir = cpPathTrailingSep(tmpDir, "dir2")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := fileContentEquals(c, dstPath, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}

+ 12 - 0
integration-cli/docker_cli_cp_test.go

@@ -23,6 +23,18 @@ const (
 	cpHostContents      = "hello, i am the host"
 )
 
+// Ensure that an all-local path case returns an error.
+func (s *DockerSuite) TestCpLocalOnly(c *check.C) {
+	err := runDockerCp(c, "foo", "bar")
+	if err == nil {
+		c.Fatal("expected failure, got success")
+	}
+
+	if !strings.Contains(err.Error(), "must specify at least one container source") {
+		c.Fatalf("unexpected output: %s", err.Error())
+	}
+}
+
 // Test for #5656
 // Check that garbage paths don't escape the container's rootfs
 func (s *DockerSuite) TestCpGarbagePath(c *check.C) {

+ 634 - 0
integration-cli/docker_cli_cp_to_container_test.go

@@ -0,0 +1,634 @@
+package main
+
+import (
+	"os"
+
+	"github.com/go-check/check"
+)
+
+// docker cp LOCALPATH CONTAINER:PATH
+
+// Try all of the test cases from the archive package which implements the
+// internals of `docker cp` and ensure that the behavior matches when actually
+// copying to and from containers.
+
+// Basic assumptions about SRC and DST:
+// 1. SRC must exist.
+// 2. If SRC ends with a trailing separator, it must be a directory.
+// 3. DST parent directory must exist.
+// 4. If DST exists as a file, it must not end with a trailing separator.
+
+// First get these easy error cases out of the way.
+
+// Test for error when SRC does not exist.
+func (s *DockerSuite) TestCpToErrSrcNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-src-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC ends in a trailing
+// path separator but it exists as a file.
+func (s *DockerSuite) TestCpToErrSrcNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-src-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPathTrailingSep(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "testDir")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotDir error, but got nil instead")
+	}
+
+	if !isCpNotDir(err) {
+		c.Fatalf("expected IsNotDir error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when SRC is a valid file or directory,
+// bu the DST parent directory does not exist.
+func (s *DockerSuite) TestCpToErrDstParentNotExists(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-dst-parent-not-exists")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/notExists", "file1")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = cpPath(tmpDir, "dir1")
+
+	if err := runDockerCp(c, srcPath, dstPath); err == nil {
+		c.Fatal("expected IsNotExist error, but got nil instead")
+	}
+
+	if !isCpNotExist(err) {
+		c.Fatalf("expected IsNotExist error, but got %T: %s", err, err)
+	}
+}
+
+// Test for error when DST ends in a trailing path separator but exists as a
+// file. Also test that we cannot overwirite an existing directory with a
+// non-directory and cannot overwrite an existing
+func (s *DockerSuite) TestCpToErrDstNotDir(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{addContent: true})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-err-dst-not-dir")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	// Try with a file source.
+	srcPath := cpPath(tmpDir, "dir1/file1-1")
+	dstPath := containerCpPathTrailingSep(cID, "file1")
+
+	// The client should encounter an error trying to stat the destination
+	// and then be unable to copy since the destination is asserted to be a
+	// directory but does not exist.
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected DirNotExist error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExist error, but got %T: %s", err, err)
+	}
+
+	// Try with a directory source.
+	srcPath = cpPath(tmpDir, "dir1")
+
+	// The client should encounter an error trying to stat the destination and
+	// then decide to extract to the parent directory instead with a rebased
+	// name in the source archive, but this directory would overwrite the
+	// existing file with the same name.
+	err = runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected CannotOverwriteNonDirWithDir error, but got nil instead")
+	}
+
+	if !isCannotOverwriteNonDirWithDir(err) {
+		c.Fatalf("expected CannotOverwriteNonDirWithDir error, but got %T: %s", err, err)
+	}
+}
+
+// Possibilities are reduced to the remaining 10 cases:
+//
+//  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
+// ===================================================================================================
+//   A   |  no      |  -              |  no       |  -       |  no      |  create file
+//   B   |  no      |  -              |  no       |  -       |  yes     |  error
+//   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
+//   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
+//   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
+//   F   |  yes     |  no             |  yes      |  no      |  -       |  error
+//   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
+//   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
+//   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
+//   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
+//
+
+// A. SRC specifies a file and DST (no trailing path separator) doesn't
+//    exist. This should create a file with the name DST and copy the
+//    contents of the source file into it.
+func (s *DockerSuite) TestCpToCaseA(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		workDir: "/root", command: makeCatFileCommand("itWorks.txt"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-a")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/itWorks.txt")
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// B. SRC specifies a file and DST (with trailing path separator) doesn't
+//    exist. This should cause an error because the copy operation cannot
+//    create a directory when copying a single file.
+func (s *DockerSuite) TestCpToCaseB(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("testDir/file1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-b")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstDir := containerCpPathTrailingSep(cID, "testDir")
+
+	err := runDockerCp(c, srcPath, dstDir)
+	if err == nil {
+		c.Fatal("expected DirNotExists error, but got nil instead")
+	}
+
+	if !isCpDirNotExist(err) {
+		c.Fatalf("expected DirNotExists error, but got %T: %s", err, err)
+	}
+}
+
+// C. SRC specifies a file and DST exists as a file. This should overwrite
+//    the file at DST with the contents of the source file.
+func (s *DockerSuite) TestCpToCaseC(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("file2"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-c")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/file2")
+
+	// Ensure the container's file starts with the original content.
+	if err := containerStartOutputEquals(c, cID, "file2\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstPath); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// D. SRC specifies a file and DST exists as a directory. This should place
+//    a copy of the source file inside it using the basename from SRC. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseD(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir1/file1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-d")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstDir := containerCpPath(cID, "dir1")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir1/file1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "dir1")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcPath, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// E. SRC specifies a directory and DST does not exist. This should create a
+//    directory at DST and copy the contents of the SRC directory into the DST
+//    directory. Ensure this works whether DST has a trailing path separator or
+//    not.
+func (s *DockerSuite) TestCpToCaseE(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-e")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstDir := containerCpPath(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "testDir")
+
+	err := runDockerCp(c, srcDir, dstDir)
+	if err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// F. SRC specifies a directory and DST exists as a file. This should cause an
+//    error as it is not possible to overwrite a file with a directory.
+func (s *DockerSuite) TestCpToCaseF(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-f")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstFile := containerCpPath(cID, "/root/file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// G. SRC specifies a directory and DST exists as a directory. This should copy
+//    the SRC directory and all its contents to the DST directory. Ensure this
+//    works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseG(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("dir2/dir1/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-g")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPath(tmpDir, "dir1")
+	dstDir := containerCpPath(cID, "/root/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		addContent: true,
+		command:    makeCatFileCommand("/dir2/dir1/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// H. SRC specifies a directory's contents only and DST does not exist. This
+//    should create a directory at DST and copy the contents of the SRC
+//    directory (but not the directory itself) into the DST directory. Ensure
+//    this works whether DST has a trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseH(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-h")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstDir := containerCpPath(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/testDir/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "testDir")
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// I. SRC specifies a direcotry's contents only and DST exists as a file. This
+//    should cause an error as it is not possible to overwrite a file with a
+//    directory.
+func (s *DockerSuite) TestCpToCaseI(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-i")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstFile := containerCpPath(cID, "/root/file1")
+
+	err := runDockerCp(c, srcDir, dstFile)
+	if err == nil {
+		c.Fatal("expected ErrCannotCopyDir error, but got nil instead")
+	}
+
+	if !isCpCannotCopyDir(err) {
+		c.Fatalf("expected ErrCannotCopyDir error, but got %T: %s", err, err)
+	}
+}
+
+// J. SRC specifies a directory's contents only and DST exists as a directory.
+//    This should copy the contents of the SRC directory (but not the directory
+//    itself) into the DST directory. Ensure this works whether DST has a
+//    trailing path separator or not.
+func (s *DockerSuite) TestCpToCaseJ(c *check.C) {
+	cID := makeTestContainer(c, testContainerOptions{
+		addContent: true, workDir: "/root",
+		command: makeCatFileCommand("/dir2/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	tmpDir := getTestDir(c, "test-cp-to-case-j")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
+	dstDir := containerCpPath(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+
+	// Now try again but using a trailing path separator for dstDir.
+
+	// Make new destination container.
+	cID = makeTestContainer(c, testContainerOptions{
+		command: makeCatFileCommand("/dir2/file1-1"),
+	})
+	defer deleteContainer(cID)
+
+	dstDir = containerCpPathTrailingSep(cID, "/dir2")
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+
+	if err := runDockerCp(c, srcDir, dstDir); err != nil {
+		c.Fatalf("unexpected error %T: %s", err, err)
+	}
+
+	// Should now contain file1-1's contents.
+	if err := containerStartOutputEquals(c, cID, "file1-1\n"); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// The `docker cp` command should also ensure that you cannot
+// write to a container rootfs that is marked as read-only.
+func (s *DockerSuite) TestCpToErrReadOnlyRootfs(c *check.C) {
+	tmpDir := getTestDir(c, "test-cp-to-err-read-only-rootfs")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	cID := makeTestContainer(c, testContainerOptions{
+		readOnly: true, workDir: "/root",
+		command: makeCatFileCommand("shouldNotExist"),
+	})
+	defer deleteContainer(cID)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/root/shouldNotExist")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected ErrContainerRootfsReadonly error, but got nil instead")
+	}
+
+	if !isCpCannotCopyReadOnly(err) {
+		c.Fatalf("expected ErrContainerRootfsReadonly error, but got %T: %s", err, err)
+	}
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+}
+
+// The `docker cp` command should also ensure that you
+// cannot write to a volume that is mounted as read-only.
+func (s *DockerSuite) TestCpToErrReadOnlyVolume(c *check.C) {
+	tmpDir := getTestDir(c, "test-cp-to-err-read-only-volume")
+	defer os.RemoveAll(tmpDir)
+
+	makeTestContentInDir(c, tmpDir)
+
+	cID := makeTestContainer(c, testContainerOptions{
+		volumes: defaultVolumes(tmpDir), workDir: "/root",
+		command: makeCatFileCommand("/vol_ro/shouldNotExist"),
+	})
+	defer deleteContainer(cID)
+
+	srcPath := cpPath(tmpDir, "file1")
+	dstPath := containerCpPath(cID, "/vol_ro/shouldNotExist")
+
+	err := runDockerCp(c, srcPath, dstPath)
+	if err == nil {
+		c.Fatal("expected ErrVolumeReadonly error, but got nil instead")
+	}
+
+	if !isCpCannotCopyReadOnly(err) {
+		c.Fatalf("expected ErrVolumeReadonly error, but got %T: %s", err, err)
+	}
+
+	// Ensure that dstPath doesn't exist.
+	if err := containerStartOutputEquals(c, cID, ""); err != nil {
+		c.Fatal(err)
+	}
+}

+ 298 - 0
integration-cli/docker_cli_cp_utils.go

@@ -0,0 +1,298 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/docker/docker/pkg/archive"
+	"github.com/go-check/check"
+)
+
+type FileType uint32
+
+const (
+	Regular FileType = iota
+	Dir
+	Symlink
+)
+
+type FileData struct {
+	filetype FileType
+	path     string
+	contents string
+}
+
+func (fd FileData) creationCommand() string {
+	var command string
+
+	switch fd.filetype {
+	case Regular:
+		// Don't overwrite the file if it already exists!
+		command = fmt.Sprintf("if [ ! -f %s ]; then echo %q > %s; fi", fd.path, fd.contents, fd.path)
+	case Dir:
+		command = fmt.Sprintf("mkdir -p %s", fd.path)
+	case Symlink:
+		command = fmt.Sprintf("ln -fs %s %s", fd.contents, fd.path)
+	}
+
+	return command
+}
+
+func mkFilesCommand(fds []FileData) string {
+	commands := make([]string, len(fds))
+
+	for i, fd := range fds {
+		commands[i] = fd.creationCommand()
+	}
+
+	return strings.Join(commands, " && ")
+}
+
+var defaultFileData = []FileData{
+	{Regular, "file1", "file1"},
+	{Regular, "file2", "file2"},
+	{Regular, "file3", "file3"},
+	{Regular, "file4", "file4"},
+	{Regular, "file5", "file5"},
+	{Regular, "file6", "file6"},
+	{Regular, "file7", "file7"},
+	{Dir, "dir1", ""},
+	{Regular, "dir1/file1-1", "file1-1"},
+	{Regular, "dir1/file1-2", "file1-2"},
+	{Dir, "dir2", ""},
+	{Regular, "dir2/file2-1", "file2-1"},
+	{Regular, "dir2/file2-2", "file2-2"},
+	{Dir, "dir3", ""},
+	{Regular, "dir3/file3-1", "file3-1"},
+	{Regular, "dir3/file3-2", "file3-2"},
+	{Dir, "dir4", ""},
+	{Regular, "dir4/file3-1", "file4-1"},
+	{Regular, "dir4/file3-2", "file4-2"},
+	{Dir, "dir5", ""},
+	{Symlink, "symlink1", "target1"},
+	{Symlink, "symlink2", "target2"},
+}
+
+func defaultMkContentCommand() string {
+	return mkFilesCommand(defaultFileData)
+}
+
+func makeTestContentInDir(c *check.C, dir string) {
+	for _, fd := range defaultFileData {
+		path := filepath.Join(dir, filepath.FromSlash(fd.path))
+		switch fd.filetype {
+		case Regular:
+			if err := ioutil.WriteFile(path, []byte(fd.contents+"\n"), os.FileMode(0666)); err != nil {
+				c.Fatal(err)
+			}
+		case Dir:
+			if err := os.Mkdir(path, os.FileMode(0777)); err != nil {
+				c.Fatal(err)
+			}
+		case Symlink:
+			if err := os.Symlink(fd.contents, path); err != nil {
+				c.Fatal(err)
+			}
+		}
+	}
+}
+
+type testContainerOptions struct {
+	addContent bool
+	readOnly   bool
+	volumes    []string
+	workDir    string
+	command    string
+}
+
+func makeTestContainer(c *check.C, options testContainerOptions) (containerID string) {
+	if options.addContent {
+		mkContentCmd := defaultMkContentCommand()
+		if options.command == "" {
+			options.command = mkContentCmd
+		} else {
+			options.command = fmt.Sprintf("%s && %s", defaultMkContentCommand(), options.command)
+		}
+	}
+
+	if options.command == "" {
+		options.command = "#(nop)"
+	}
+
+	args := []string{"run", "-d"}
+
+	for _, volume := range options.volumes {
+		args = append(args, "-v", volume)
+	}
+
+	if options.workDir != "" {
+		args = append(args, "-w", options.workDir)
+	}
+
+	if options.readOnly {
+		args = append(args, "--read-only")
+	}
+
+	args = append(args, "busybox", "/bin/sh", "-c", options.command)
+
+	out, status := dockerCmd(c, args...)
+	if status != 0 {
+		c.Fatalf("failed to run container, status %d: %s", status, out)
+	}
+
+	containerID = strings.TrimSpace(out)
+
+	out, status = dockerCmd(c, "wait", containerID)
+	if status != 0 {
+		c.Fatalf("failed to wait for test container container, status %d: %s", status, out)
+	}
+
+	if exitCode := strings.TrimSpace(out); exitCode != "0" {
+		logs, status := dockerCmd(c, "logs", containerID)
+		if status != 0 {
+			logs = "UNABLE TO GET LOGS"
+		}
+		c.Fatalf("failed to make test container, exit code (%d): %s", exitCode, logs)
+	}
+
+	return
+}
+
+func makeCatFileCommand(path string) string {
+	return fmt.Sprintf("if [ -f %s ]; then cat %s; fi", path, path)
+}
+
+func cpPath(pathElements ...string) string {
+	localizedPathElements := make([]string, len(pathElements))
+	for i, path := range pathElements {
+		localizedPathElements[i] = filepath.FromSlash(path)
+	}
+	return strings.Join(localizedPathElements, string(filepath.Separator))
+}
+
+func cpPathTrailingSep(pathElements ...string) string {
+	return fmt.Sprintf("%s%c", cpPath(pathElements...), filepath.Separator)
+}
+
+func containerCpPath(containerID string, pathElements ...string) string {
+	joined := strings.Join(pathElements, "/")
+	return fmt.Sprintf("%s:%s", containerID, joined)
+}
+
+func containerCpPathTrailingSep(containerID string, pathElements ...string) string {
+	return fmt.Sprintf("%s/", containerCpPath(containerID, pathElements...))
+}
+
+func runDockerCp(c *check.C, src, dst string) (err error) {
+	c.Logf("running `docker cp %s %s`", src, dst)
+
+	args := []string{"cp", src, dst}
+
+	out, _, err := runCommandWithOutput(exec.Command(dockerBinary, args...))
+	if err != nil {
+		err = fmt.Errorf("error executing `docker cp` command: %s: %s", err, out)
+	}
+
+	return
+}
+
+func startContainerGetOutput(c *check.C, cID string) (out string, err error) {
+	c.Logf("running `docker start -a %s`", cID)
+
+	args := []string{"start", "-a", cID}
+
+	out, _, err = runCommandWithOutput(exec.Command(dockerBinary, args...))
+	if err != nil {
+		err = fmt.Errorf("error executing `docker start` command: %s: %s", err, out)
+	}
+
+	return
+}
+
+func getTestDir(c *check.C, label string) (tmpDir string) {
+	var err error
+
+	if tmpDir, err = ioutil.TempDir("", label); err != nil {
+		c.Fatalf("unable to make temporary directory: %s", err)
+	}
+
+	return
+}
+
+func isCpNotExist(err error) bool {
+	return strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "cannot find the file specified")
+}
+
+func isCpDirNotExist(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrDirNotExists.Error())
+}
+
+func isCpNotDir(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrNotDirectory.Error()) || strings.Contains(err.Error(), "filename, directory name, or volume label syntax is incorrect")
+}
+
+func isCpCannotCopyDir(err error) bool {
+	return strings.Contains(err.Error(), archive.ErrCannotCopyDir.Error())
+}
+
+func isCpCannotCopyReadOnly(err error) bool {
+	return strings.Contains(err.Error(), "marked read-only")
+}
+
+func isCannotOverwriteNonDirWithDir(err error) bool {
+	return strings.Contains(err.Error(), "cannot overwrite non-directory")
+}
+
+func fileContentEquals(c *check.C, filename, contents string) (err error) {
+	c.Logf("checking that file %q contains %q\n", filename, contents)
+
+	fileBytes, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return
+	}
+
+	expectedBytes, err := ioutil.ReadAll(strings.NewReader(contents))
+	if err != nil {
+		return
+	}
+
+	if !bytes.Equal(fileBytes, expectedBytes) {
+		err = fmt.Errorf("file content not equal - expected %q, got %q", string(expectedBytes), string(fileBytes))
+	}
+
+	return
+}
+
+func containerStartOutputEquals(c *check.C, cID, contents string) (err error) {
+	c.Logf("checking that container %q start output contains %q\n", cID, contents)
+
+	out, err := startContainerGetOutput(c, cID)
+	if err != nil {
+		return err
+	}
+
+	if out != contents {
+		err = fmt.Errorf("output contents not equal - expected %q, got %q", contents, out)
+	}
+
+	return
+}
+
+func defaultVolumes(tmpDir string) []string {
+	if SameHostDaemon.Condition() {
+		return []string{
+			"/vol1",
+			fmt.Sprintf("%s:/vol2", tmpDir),
+			fmt.Sprintf("%s:/vol3", filepath.Join(tmpDir, "vol3")),
+			fmt.Sprintf("%s:/vol_ro:ro", filepath.Join(tmpDir, "vol_ro")),
+		}
+	}
+
+	// Can't bind-mount volumes with separate host daemon.
+	return []string{"/vol1", "/vol2", "/vol3", "/vol_ro:/vol_ro:ro"}
+}

+ 26 - 4
integration-cli/docker_cli_events_test.go

@@ -3,7 +3,9 @@ package main
 import (
 	"bufio"
 	"fmt"
+	"io/ioutil"
 	"net/http"
+	"os"
 	"os/exec"
 	"regexp"
 	"strconv"
@@ -519,6 +521,7 @@ func (s *DockerSuite) TestEventsCommit(c *check.C) {
 func (s *DockerSuite) TestEventsCopy(c *check.C) {
 	since := daemonTime(c).Unix()
 
+	// Build a test image.
 	id, err := buildImage("cpimg", `
 		  FROM busybox
 		  RUN echo HI > /tmp/file`, true)
@@ -526,12 +529,31 @@ func (s *DockerSuite) TestEventsCopy(c *check.C) {
 		c.Fatalf("Couldn't create image: %q", err)
 	}
 
-	dockerCmd(c, "run", "--name=cptest", id, "true")
-	dockerCmd(c, "cp", "cptest:/tmp/file", "-")
+	// Create an empty test file.
+	tempFile, err := ioutil.TempFile("", "test-events-copy-")
+	if err != nil {
+		c.Fatal(err)
+	}
+	defer os.Remove(tempFile.Name())
+
+	if err := tempFile.Close(); err != nil {
+		c.Fatal(err)
+	}
+
+	dockerCmd(c, "create", "--name=cptest", id)
+
+	dockerCmd(c, "cp", "cptest:/tmp/file", tempFile.Name())
 
 	out, _ := dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
-	if !strings.Contains(out, " copy\n") {
-		c.Fatalf("Missing 'copy' log event\n%s", out)
+	if !strings.Contains(out, " archive-path\n") {
+		c.Fatalf("Missing 'archive-path' log event\n%s", out)
+	}
+
+	dockerCmd(c, "cp", tempFile.Name(), "cptest:/tmp/filecopy")
+
+	out, _ = dockerCmd(c, "events", "--since=0", "-f", "container=cptest", "--until="+strconv.Itoa(int(since)))
+	if !strings.Contains(out, " extract-to-dir\n") {
+		c.Fatalf("Missing 'extract-to-dir' log event\n%s", out)
 	}
 }