github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/integration-cli/docker_cli_cp_to_container_test.go (about)

     1  package main
     2  
     3  import (
     4  	"os"
     5  	"testing"
     6  
     7  	"gotest.tools/v3/assert"
     8  )
     9  
    10  // Try all of the test cases from the archive package which implements the
    11  // internals of `docker cp` and ensure that the behavior matches when actually
    12  // copying to and from containers.
    13  
    14  // Basic assumptions about SRC and DST:
    15  // 1. SRC must exist.
    16  // 2. If SRC ends with a trailing separator, it must be a directory.
    17  // 3. DST parent directory must exist.
    18  // 4. If DST exists as a file, it must not end with a trailing separator.
    19  
    20  // Check that copying from a local path to a symlink in a container copies to
    21  // the symlink target and does not overwrite the container symlink itself.
    22  func (s *DockerCLICpSuite) TestCpToSymlinkDestination(c *testing.T) {
    23  	//  stat /tmp/test-cp-to-symlink-destination-262430901/vol3 gets permission denied for the user
    24  	testRequires(c, NotUserNamespace)
    25  	testRequires(c, DaemonIsLinux)
    26  	testRequires(c, testEnv.IsLocalDaemon) // Requires local volume mount bind.
    27  
    28  	testVol := getTestDir(c, "test-cp-to-symlink-destination-")
    29  	defer os.RemoveAll(testVol)
    30  
    31  	makeTestContentInDir(c, testVol)
    32  
    33  	containerID := makeTestContainer(c, testContainerOptions{
    34  		volumes: defaultVolumes(testVol), // Our bind mount is at /vol2
    35  	})
    36  
    37  	// First, copy a local file to a symlink to a file in the container. This
    38  	// should overwrite the symlink target contents with the source contents.
    39  	srcPath := cpPath(testVol, "file2")
    40  	dstPath := containerCpPath(containerID, "/vol2/symlinkToFile1")
    41  
    42  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
    43  	assert.NilError(c, symlinkTargetEquals(c, cpPath(testVol, "symlinkToFile1"), "file1"), "The symlink should not have been modified")
    44  	assert.NilError(c, fileContentEquals(c, cpPath(testVol, "file1"), "file2\n"), `The file should have the contents of "file2" now`)
    45  
    46  	// Next, copy a local file to a symlink to a directory in the container.
    47  	// This should copy the file into the symlink target directory.
    48  	dstPath = containerCpPath(containerID, "/vol2/symlinkToDir1")
    49  
    50  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
    51  	assert.NilError(c, symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"), "The symlink should not have been modified")
    52  	assert.NilError(c, fileContentEquals(c, cpPath(testVol, "file2"), "file2\n"), `The file should have the contents of "file2"" now`)
    53  
    54  	// Next, copy a file to a symlink to a file that does not exist (a broken
    55  	// symlink) in the container. This should create the target file with the
    56  	// contents of the source file.
    57  	dstPath = containerCpPath(containerID, "/vol2/brokenSymlinkToFileX")
    58  
    59  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
    60  	assert.NilError(c, symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToFileX"), "fileX"), "The symlink should not have been modified")
    61  	assert.NilError(c, fileContentEquals(c, cpPath(testVol, "fileX"), "file2\n"), `The file should have the contents of "file2"" now`)
    62  
    63  	// Next, copy a local directory to a symlink to a directory in the
    64  	// container. This should copy the directory into the symlink target
    65  	// directory and not modify the symlink.
    66  	srcPath = cpPath(testVol, "/dir2")
    67  	dstPath = containerCpPath(containerID, "/vol2/symlinkToDir1")
    68  
    69  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
    70  	assert.NilError(c, symlinkTargetEquals(c, cpPath(testVol, "symlinkToDir1"), "dir1"), "The symlink should not have been modified")
    71  	assert.NilError(c, fileContentEquals(c, cpPath(testVol, "dir1/dir2/file2-1"), "file2-1\n"), `The directory should now contain a copy of "dir2"`)
    72  
    73  	// Next, copy a local directory to a symlink to a local directory that does
    74  	// not exist (a broken symlink) in the container. This should create the
    75  	// target as a directory with the contents of the source directory. It
    76  	// should not modify the symlink.
    77  	dstPath = containerCpPath(containerID, "/vol2/brokenSymlinkToDirX")
    78  
    79  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
    80  	assert.NilError(c, symlinkTargetEquals(c, cpPath(testVol, "brokenSymlinkToDirX"), "dirX"), "The symlink should not have been modified")
    81  	assert.NilError(c, fileContentEquals(c, cpPath(testVol, "dirX/file2-1"), "file2-1\n"), `The "dirX" directory should now be a copy of "dir2"`)
    82  }
    83  
    84  // Possibilities are reduced to the remaining 10 cases:
    85  //
    86  //  case | srcIsDir | onlyDirContents | dstExists | dstIsDir | dstTrSep | action
    87  // ===================================================================================================
    88  //   A   |  no      |  -              |  no       |  -       |  no      |  create file
    89  //   B   |  no      |  -              |  no       |  -       |  yes     |  error
    90  //   C   |  no      |  -              |  yes      |  no      |  -       |  overwrite file
    91  //   D   |  no      |  -              |  yes      |  yes     |  -       |  create file in dst dir
    92  //   E   |  yes     |  no             |  no       |  -       |  -       |  create dir, copy contents
    93  //   F   |  yes     |  no             |  yes      |  no      |  -       |  error
    94  //   G   |  yes     |  no             |  yes      |  yes     |  -       |  copy dir and contents
    95  //   H   |  yes     |  yes            |  no       |  -       |  -       |  create dir, copy contents
    96  //   I   |  yes     |  yes            |  yes      |  no      |  -       |  error
    97  //   J   |  yes     |  yes            |  yes      |  yes     |  -       |  copy dir contents
    98  //
    99  
   100  // A. SRC specifies a file and DST (no trailing path separator) doesn't	exist.
   101  //
   102  // This should create a file with the name DST and copy the	contents of the
   103  // source file into it.
   104  func (s *DockerCLICpSuite) TestCpToCaseA(c *testing.T) {
   105  	containerID := makeTestContainer(c, testContainerOptions{
   106  		workDir: "/root", command: makeCatFileCommand("itWorks.txt"),
   107  	})
   108  
   109  	tmpDir := getTestDir(c, "test-cp-to-case-a")
   110  	defer os.RemoveAll(tmpDir)
   111  
   112  	makeTestContentInDir(c, tmpDir)
   113  
   114  	srcPath := cpPath(tmpDir, "file1")
   115  	dstPath := containerCpPath(containerID, "/root/itWorks.txt")
   116  
   117  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
   118  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"))
   119  }
   120  
   121  // B. SRC specifies a file and DST (with trailing path separator) doesn't exist.
   122  //
   123  // This should cause an error because the copy operation cannot	create a
   124  // directory when copying a single file.
   125  func (s *DockerCLICpSuite) TestCpToCaseB(c *testing.T) {
   126  	containerID := makeTestContainer(c, testContainerOptions{
   127  		command: makeCatFileCommand("testDir/file1"),
   128  	})
   129  
   130  	tmpDir := getTestDir(c, "test-cp-to-case-b")
   131  	defer os.RemoveAll(tmpDir)
   132  
   133  	makeTestContentInDir(c, tmpDir)
   134  
   135  	srcPath := cpPath(tmpDir, "file1")
   136  	dstDir := containerCpPathTrailingSep(containerID, "testDir")
   137  
   138  	err := runDockerCp(c, srcPath, dstDir)
   139  	assert.ErrorContains(c, err, "")
   140  	assert.Assert(c, isCpDirNotExist(err), "expected DirNotExists error, but got %T: %s", err, err)
   141  }
   142  
   143  // C. SRC specifies a file and DST exists as a file.
   144  //
   145  // This should overwrite the file at DST with the contents of the source file.
   146  func (s *DockerCLICpSuite) TestCpToCaseC(c *testing.T) {
   147  	testRequires(c, DaemonIsLinux)
   148  	containerID := makeTestContainer(c, testContainerOptions{
   149  		addContent: true, workDir: "/root",
   150  		command: makeCatFileCommand("file2"),
   151  	})
   152  
   153  	tmpDir := getTestDir(c, "test-cp-to-case-c")
   154  	defer os.RemoveAll(tmpDir)
   155  
   156  	makeTestContentInDir(c, tmpDir)
   157  
   158  	srcPath := cpPath(tmpDir, "file1")
   159  	dstPath := containerCpPath(containerID, "/root/file2")
   160  
   161  	// Ensure the container's file starts with the original content.
   162  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file2\n"))
   163  	assert.NilError(c, runDockerCp(c, srcPath, dstPath))
   164  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents")
   165  }
   166  
   167  // D. SRC specifies a file and DST exists as a directory.
   168  //
   169  // This should place a copy of the source file inside it using the basename from
   170  // SRC. Ensure this works whether DST has a trailing path separator or not.
   171  func (s *DockerCLICpSuite) TestCpToCaseD(c *testing.T) {
   172  	testRequires(c, DaemonIsLinux)
   173  	containerID := makeTestContainer(c, testContainerOptions{
   174  		addContent: true,
   175  		command:    makeCatFileCommand("/dir1/file1"),
   176  	})
   177  
   178  	tmpDir := getTestDir(c, "test-cp-to-case-d")
   179  	defer os.RemoveAll(tmpDir)
   180  
   181  	makeTestContentInDir(c, tmpDir)
   182  
   183  	srcPath := cpPath(tmpDir, "file1")
   184  	dstDir := containerCpPath(containerID, "dir1")
   185  
   186  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   187  	assert.NilError(c, runDockerCp(c, srcPath, dstDir))
   188  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents")
   189  
   190  	// Now try again but using a trailing path separator for dstDir.
   191  
   192  	// Make new destination container.
   193  	containerID = makeTestContainer(c, testContainerOptions{
   194  		addContent: true,
   195  		command:    makeCatFileCommand("/dir1/file1"),
   196  	})
   197  
   198  	dstDir = containerCpPathTrailingSep(containerID, "dir1")
   199  
   200  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   201  	assert.NilError(c, runDockerCp(c, srcPath, dstDir))
   202  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents")
   203  }
   204  
   205  // E. SRC specifies a directory and DST does not exist.
   206  //
   207  // This should create a	directory at DST and copy the contents of the SRC
   208  // directory into the DST directory. Ensure this works whether DST has a
   209  // trailing path separator or not.
   210  func (s *DockerCLICpSuite) TestCpToCaseE(c *testing.T) {
   211  	containerID := makeTestContainer(c, testContainerOptions{
   212  		command: makeCatFileCommand("/testDir/file1-1"),
   213  	})
   214  
   215  	tmpDir := getTestDir(c, "test-cp-to-case-e")
   216  	defer os.RemoveAll(tmpDir)
   217  
   218  	makeTestContentInDir(c, tmpDir)
   219  
   220  	srcDir := cpPath(tmpDir, "dir1")
   221  	dstDir := containerCpPath(containerID, "testDir")
   222  
   223  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   224  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   225  
   226  	// Now try again but using a trailing path separator for dstDir.
   227  
   228  	// Make new destination container.
   229  	containerID = makeTestContainer(c, testContainerOptions{
   230  		command: makeCatFileCommand("/testDir/file1-1"),
   231  	})
   232  
   233  	dstDir = containerCpPathTrailingSep(containerID, "testDir")
   234  
   235  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   236  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   237  }
   238  
   239  // F. SRC specifies a directory and DST exists as a file.
   240  //
   241  // This should cause an error as it is not possible to overwrite a file with a
   242  // directory.
   243  func (s *DockerCLICpSuite) TestCpToCaseF(c *testing.T) {
   244  	testRequires(c, DaemonIsLinux)
   245  	containerID := makeTestContainer(c, testContainerOptions{
   246  		addContent: true, workDir: "/root",
   247  	})
   248  
   249  	tmpDir := getTestDir(c, "test-cp-to-case-f")
   250  	defer os.RemoveAll(tmpDir)
   251  
   252  	makeTestContentInDir(c, tmpDir)
   253  
   254  	srcDir := cpPath(tmpDir, "dir1")
   255  	dstFile := containerCpPath(containerID, "/root/file1")
   256  
   257  	err := runDockerCp(c, srcDir, dstFile)
   258  	assert.ErrorContains(c, err, "")
   259  	assert.Assert(c, isCpCannotCopyDir(err), "expected ErrCannotCopyDir error, but got %T: %s", err, err)
   260  }
   261  
   262  // G. SRC specifies a directory and DST exists as a directory.
   263  //
   264  // This should copy the SRC directory and all its contents to the DST directory.
   265  // Ensure this works whether DST has a trailing path separator or not.
   266  func (s *DockerCLICpSuite) TestCpToCaseG(c *testing.T) {
   267  	testRequires(c, DaemonIsLinux)
   268  	containerID := makeTestContainer(c, testContainerOptions{
   269  		addContent: true, workDir: "/root",
   270  		command: makeCatFileCommand("dir2/dir1/file1-1"),
   271  	})
   272  
   273  	tmpDir := getTestDir(c, "test-cp-to-case-g")
   274  	defer os.RemoveAll(tmpDir)
   275  
   276  	makeTestContentInDir(c, tmpDir)
   277  
   278  	srcDir := cpPath(tmpDir, "dir1")
   279  	dstDir := containerCpPath(containerID, "/root/dir2")
   280  
   281  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   282  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   283  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   284  
   285  	// Now try again but using a trailing path separator for dstDir.
   286  
   287  	// Make new destination container.
   288  	containerID = makeTestContainer(c, testContainerOptions{
   289  		addContent: true,
   290  		command:    makeCatFileCommand("/dir2/dir1/file1-1"),
   291  	})
   292  
   293  	dstDir = containerCpPathTrailingSep(containerID, "/dir2")
   294  
   295  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   296  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   297  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   298  }
   299  
   300  // H. SRC specifies a directory's contents only and DST does not exist.
   301  //
   302  // This should create a directory at DST and copy the contents of the SRC
   303  // directory (but not the directory itself) into the DST directory. Ensure
   304  // this works whether DST has a trailing path separator or not.
   305  func (s *DockerCLICpSuite) TestCpToCaseH(c *testing.T) {
   306  	containerID := makeTestContainer(c, testContainerOptions{
   307  		command: makeCatFileCommand("/testDir/file1-1"),
   308  	})
   309  
   310  	tmpDir := getTestDir(c, "test-cp-to-case-h")
   311  	defer os.RemoveAll(tmpDir)
   312  
   313  	makeTestContentInDir(c, tmpDir)
   314  
   315  	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
   316  	dstDir := containerCpPath(containerID, "testDir")
   317  
   318  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   319  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   320  
   321  	// Now try again but using a trailing path separator for dstDir.
   322  
   323  	// Make new destination container.
   324  	containerID = makeTestContainer(c, testContainerOptions{
   325  		command: makeCatFileCommand("/testDir/file1-1"),
   326  	})
   327  
   328  	dstDir = containerCpPathTrailingSep(containerID, "testDir")
   329  
   330  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   331  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents")
   332  }
   333  
   334  // I. SRC specifies a directory's contents only and DST exists as a file.
   335  //
   336  // This	should cause an error as it is not possible to overwrite a file with a
   337  // directory.
   338  func (s *DockerCLICpSuite) TestCpToCaseI(c *testing.T) {
   339  	testRequires(c, DaemonIsLinux)
   340  	containerID := makeTestContainer(c, testContainerOptions{
   341  		addContent: true, workDir: "/root",
   342  	})
   343  
   344  	tmpDir := getTestDir(c, "test-cp-to-case-i")
   345  	defer os.RemoveAll(tmpDir)
   346  
   347  	makeTestContentInDir(c, tmpDir)
   348  
   349  	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
   350  	dstFile := containerCpPath(containerID, "/root/file1")
   351  
   352  	err := runDockerCp(c, srcDir, dstFile)
   353  	assert.ErrorContains(c, err, "")
   354  	assert.Assert(c, isCpCannotCopyDir(err), "expected ErrCannotCopyDir error, but got %T: %s", err, err)
   355  }
   356  
   357  // J. SRC specifies a directory's contents only and DST exists as a directory.
   358  //
   359  // This should copy the contents of the SRC directory (but not the directory
   360  // itself) into the DST directory. Ensure this works whether DST has a
   361  // trailing path separator or not.
   362  func (s *DockerCLICpSuite) TestCpToCaseJ(c *testing.T) {
   363  	testRequires(c, DaemonIsLinux)
   364  	containerID := makeTestContainer(c, testContainerOptions{
   365  		addContent: true, workDir: "/root",
   366  		command: makeCatFileCommand("/dir2/file1-1"),
   367  	})
   368  
   369  	tmpDir := getTestDir(c, "test-cp-to-case-j")
   370  	defer os.RemoveAll(tmpDir)
   371  
   372  	makeTestContentInDir(c, tmpDir)
   373  
   374  	srcDir := cpPathTrailingSep(tmpDir, "dir1") + "."
   375  	dstDir := containerCpPath(containerID, "/dir2")
   376  
   377  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   378  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   379  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should've contained file1-1's contents")
   380  
   381  	// Now try again but using a trailing path separator for dstDir.
   382  
   383  	// Make new destination container.
   384  	containerID = makeTestContainer(c, testContainerOptions{
   385  		command: makeCatFileCommand("/dir2/file1-1"),
   386  	})
   387  
   388  	dstDir = containerCpPathTrailingSep(containerID, "/dir2")
   389  
   390  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   391  	assert.NilError(c, runDockerCp(c, srcDir, dstDir))
   392  	assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should've contained file1-1's contents")
   393  }
   394  
   395  // The `docker cp` command should also ensure that you cannot
   396  // write to a container rootfs that is marked as read-only.
   397  func (s *DockerCLICpSuite) TestCpToErrReadOnlyRootfs(c *testing.T) {
   398  	// --read-only + userns has remount issues
   399  	testRequires(c, DaemonIsLinux, NotUserNamespace)
   400  	tmpDir := getTestDir(c, "test-cp-to-err-read-only-rootfs")
   401  	defer os.RemoveAll(tmpDir)
   402  
   403  	makeTestContentInDir(c, tmpDir)
   404  
   405  	containerID := makeTestContainer(c, testContainerOptions{
   406  		readOnly: true, workDir: "/root",
   407  		command: makeCatFileCommand("shouldNotExist"),
   408  	})
   409  
   410  	srcPath := cpPath(tmpDir, "file1")
   411  	dstPath := containerCpPath(containerID, "/root/shouldNotExist")
   412  
   413  	err := runDockerCp(c, srcPath, dstPath)
   414  	assert.ErrorContains(c, err, "marked read-only")
   415  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   416  }
   417  
   418  // The `docker cp` command should also ensure that you
   419  // cannot write to a volume that is mounted as read-only.
   420  func (s *DockerCLICpSuite) TestCpToErrReadOnlyVolume(c *testing.T) {
   421  	// --read-only + userns has remount issues
   422  	testRequires(c, DaemonIsLinux, NotUserNamespace)
   423  	tmpDir := getTestDir(c, "test-cp-to-err-read-only-volume")
   424  	defer os.RemoveAll(tmpDir)
   425  
   426  	makeTestContentInDir(c, tmpDir)
   427  
   428  	containerID := makeTestContainer(c, testContainerOptions{
   429  		volumes: defaultVolumes(tmpDir), workDir: "/root",
   430  		command: makeCatFileCommand("/vol_ro/shouldNotExist"),
   431  	})
   432  
   433  	srcPath := cpPath(tmpDir, "file1")
   434  	dstPath := containerCpPath(containerID, "/vol_ro/shouldNotExist")
   435  
   436  	err := runDockerCp(c, srcPath, dstPath)
   437  	assert.ErrorContains(c, err, "marked read-only")
   438  
   439  	assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed")
   440  }