github.com/afbjorklund/moby@v20.10.5+incompatible/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 *DockerSuite) 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 101 // exist. This should create a file with the name DST and copy the 102 // contents of the source file into it. 103 func (s *DockerSuite) TestCpToCaseA(c *testing.T) { 104 containerID := makeTestContainer(c, testContainerOptions{ 105 workDir: "/root", command: makeCatFileCommand("itWorks.txt"), 106 }) 107 108 tmpDir := getTestDir(c, "test-cp-to-case-a") 109 defer os.RemoveAll(tmpDir) 110 111 makeTestContentInDir(c, tmpDir) 112 113 srcPath := cpPath(tmpDir, "file1") 114 dstPath := containerCpPath(containerID, "/root/itWorks.txt") 115 116 assert.NilError(c, runDockerCp(c, srcPath, dstPath)) 117 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n")) 118 } 119 120 // B. SRC specifies a file and DST (with trailing path separator) doesn't 121 // exist. This should cause an error because the copy operation cannot 122 // create a directory when copying a single file. 123 func (s *DockerSuite) TestCpToCaseB(c *testing.T) { 124 containerID := makeTestContainer(c, testContainerOptions{ 125 command: makeCatFileCommand("testDir/file1"), 126 }) 127 128 tmpDir := getTestDir(c, "test-cp-to-case-b") 129 defer os.RemoveAll(tmpDir) 130 131 makeTestContentInDir(c, tmpDir) 132 133 srcPath := cpPath(tmpDir, "file1") 134 dstDir := containerCpPathTrailingSep(containerID, "testDir") 135 136 err := runDockerCp(c, srcPath, dstDir) 137 assert.ErrorContains(c, err, "") 138 assert.Assert(c, isCpDirNotExist(err), "expected DirNotExists error, but got %T: %s", err, err) 139 } 140 141 // C. SRC specifies a file and DST exists as a file. This should overwrite 142 // the file at DST with the contents of the source file. 143 func (s *DockerSuite) TestCpToCaseC(c *testing.T) { 144 testRequires(c, DaemonIsLinux) 145 containerID := makeTestContainer(c, testContainerOptions{ 146 addContent: true, workDir: "/root", 147 command: makeCatFileCommand("file2"), 148 }) 149 150 tmpDir := getTestDir(c, "test-cp-to-case-c") 151 defer os.RemoveAll(tmpDir) 152 153 makeTestContentInDir(c, tmpDir) 154 155 srcPath := cpPath(tmpDir, "file1") 156 dstPath := containerCpPath(containerID, "/root/file2") 157 158 // Ensure the container's file starts with the original content. 159 assert.NilError(c, containerStartOutputEquals(c, containerID, "file2\n")) 160 assert.NilError(c, runDockerCp(c, srcPath, dstPath)) 161 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents") 162 } 163 164 // D. SRC specifies a file and DST exists as a directory. This should place 165 // a copy of the source file inside it using the basename from SRC. Ensure 166 // this works whether DST has a trailing path separator or not. 167 func (s *DockerSuite) TestCpToCaseD(c *testing.T) { 168 testRequires(c, DaemonIsLinux) 169 containerID := makeTestContainer(c, testContainerOptions{ 170 addContent: true, 171 command: makeCatFileCommand("/dir1/file1"), 172 }) 173 174 tmpDir := getTestDir(c, "test-cp-to-case-d") 175 defer os.RemoveAll(tmpDir) 176 177 makeTestContentInDir(c, tmpDir) 178 179 srcPath := cpPath(tmpDir, "file1") 180 dstDir := containerCpPath(containerID, "dir1") 181 182 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 183 assert.NilError(c, runDockerCp(c, srcPath, dstDir)) 184 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents") 185 186 // Now try again but using a trailing path separator for dstDir. 187 188 // Make new destination container. 189 containerID = makeTestContainer(c, testContainerOptions{ 190 addContent: true, 191 command: makeCatFileCommand("/dir1/file1"), 192 }) 193 194 dstDir = containerCpPathTrailingSep(containerID, "dir1") 195 196 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 197 assert.NilError(c, runDockerCp(c, srcPath, dstDir)) 198 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1\n"), "Should now contain file1's contents") 199 } 200 201 // E. SRC specifies a directory and DST does not exist. This should create a 202 // directory at DST and copy the contents of the SRC directory into the DST 203 // directory. Ensure this works whether DST has a trailing path separator or 204 // not. 205 func (s *DockerSuite) TestCpToCaseE(c *testing.T) { 206 containerID := makeTestContainer(c, testContainerOptions{ 207 command: makeCatFileCommand("/testDir/file1-1"), 208 }) 209 210 tmpDir := getTestDir(c, "test-cp-to-case-e") 211 defer os.RemoveAll(tmpDir) 212 213 makeTestContentInDir(c, tmpDir) 214 215 srcDir := cpPath(tmpDir, "dir1") 216 dstDir := containerCpPath(containerID, "testDir") 217 218 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 219 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 220 221 // Now try again but using a trailing path separator for dstDir. 222 223 // Make new destination container. 224 containerID = makeTestContainer(c, testContainerOptions{ 225 command: makeCatFileCommand("/testDir/file1-1"), 226 }) 227 228 dstDir = containerCpPathTrailingSep(containerID, "testDir") 229 230 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 231 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 232 } 233 234 // F. SRC specifies a directory and DST exists as a file. This should cause an 235 // error as it is not possible to overwrite a file with a directory. 236 func (s *DockerSuite) TestCpToCaseF(c *testing.T) { 237 testRequires(c, DaemonIsLinux) 238 containerID := makeTestContainer(c, testContainerOptions{ 239 addContent: true, workDir: "/root", 240 }) 241 242 tmpDir := getTestDir(c, "test-cp-to-case-f") 243 defer os.RemoveAll(tmpDir) 244 245 makeTestContentInDir(c, tmpDir) 246 247 srcDir := cpPath(tmpDir, "dir1") 248 dstFile := containerCpPath(containerID, "/root/file1") 249 250 err := runDockerCp(c, srcDir, dstFile) 251 assert.ErrorContains(c, err, "") 252 assert.Assert(c, isCpCannotCopyDir(err), "expected ErrCannotCopyDir error, but got %T: %s", err, err) 253 } 254 255 // G. SRC specifies a directory and DST exists as a directory. This should copy 256 // the SRC directory and all its contents to the DST directory. Ensure this 257 // works whether DST has a trailing path separator or not. 258 func (s *DockerSuite) TestCpToCaseG(c *testing.T) { 259 testRequires(c, DaemonIsLinux) 260 containerID := makeTestContainer(c, testContainerOptions{ 261 addContent: true, workDir: "/root", 262 command: makeCatFileCommand("dir2/dir1/file1-1"), 263 }) 264 265 tmpDir := getTestDir(c, "test-cp-to-case-g") 266 defer os.RemoveAll(tmpDir) 267 268 makeTestContentInDir(c, tmpDir) 269 270 srcDir := cpPath(tmpDir, "dir1") 271 dstDir := containerCpPath(containerID, "/root/dir2") 272 273 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 274 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 275 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 276 277 // Now try again but using a trailing path separator for dstDir. 278 279 // Make new destination container. 280 containerID = makeTestContainer(c, testContainerOptions{ 281 addContent: true, 282 command: makeCatFileCommand("/dir2/dir1/file1-1"), 283 }) 284 285 dstDir = containerCpPathTrailingSep(containerID, "/dir2") 286 287 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 288 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 289 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 290 } 291 292 // H. SRC specifies a directory's contents only and DST does not exist. This 293 // should create a directory at DST and copy the contents of the SRC 294 // directory (but not the directory itself) into the DST directory. Ensure 295 // this works whether DST has a trailing path separator or not. 296 func (s *DockerSuite) TestCpToCaseH(c *testing.T) { 297 containerID := makeTestContainer(c, testContainerOptions{ 298 command: makeCatFileCommand("/testDir/file1-1"), 299 }) 300 301 tmpDir := getTestDir(c, "test-cp-to-case-h") 302 defer os.RemoveAll(tmpDir) 303 304 makeTestContentInDir(c, tmpDir) 305 306 srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." 307 dstDir := containerCpPath(containerID, "testDir") 308 309 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 310 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 311 312 // Now try again but using a trailing path separator for dstDir. 313 314 // Make new destination container. 315 containerID = makeTestContainer(c, testContainerOptions{ 316 command: makeCatFileCommand("/testDir/file1-1"), 317 }) 318 319 dstDir = containerCpPathTrailingSep(containerID, "testDir") 320 321 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 322 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should now contain file1-1's contents") 323 } 324 325 // I. SRC specifies a directory's contents only and DST exists as a file. This 326 // should cause an error as it is not possible to overwrite a file with a 327 // directory. 328 func (s *DockerSuite) TestCpToCaseI(c *testing.T) { 329 testRequires(c, DaemonIsLinux) 330 containerID := makeTestContainer(c, testContainerOptions{ 331 addContent: true, workDir: "/root", 332 }) 333 334 tmpDir := getTestDir(c, "test-cp-to-case-i") 335 defer os.RemoveAll(tmpDir) 336 337 makeTestContentInDir(c, tmpDir) 338 339 srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." 340 dstFile := containerCpPath(containerID, "/root/file1") 341 342 err := runDockerCp(c, srcDir, dstFile) 343 assert.ErrorContains(c, err, "") 344 assert.Assert(c, isCpCannotCopyDir(err), "expected ErrCannotCopyDir error, but got %T: %s", err, err) 345 } 346 347 // J. SRC specifies a directory's contents only and DST exists as a directory. 348 // This should copy the contents of the SRC directory (but not the directory 349 // itself) into the DST directory. Ensure this works whether DST has a 350 // trailing path separator or not. 351 func (s *DockerSuite) TestCpToCaseJ(c *testing.T) { 352 testRequires(c, DaemonIsLinux) 353 containerID := makeTestContainer(c, testContainerOptions{ 354 addContent: true, workDir: "/root", 355 command: makeCatFileCommand("/dir2/file1-1"), 356 }) 357 358 tmpDir := getTestDir(c, "test-cp-to-case-j") 359 defer os.RemoveAll(tmpDir) 360 361 makeTestContentInDir(c, tmpDir) 362 363 srcDir := cpPathTrailingSep(tmpDir, "dir1") + "." 364 dstDir := containerCpPath(containerID, "/dir2") 365 366 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 367 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 368 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should've contained file1-1's contents") 369 370 // Now try again but using a trailing path separator for dstDir. 371 372 // Make new destination container. 373 containerID = makeTestContainer(c, testContainerOptions{ 374 command: makeCatFileCommand("/dir2/file1-1"), 375 }) 376 377 dstDir = containerCpPathTrailingSep(containerID, "/dir2") 378 379 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 380 assert.NilError(c, runDockerCp(c, srcDir, dstDir)) 381 assert.NilError(c, containerStartOutputEquals(c, containerID, "file1-1\n"), "Should've contained file1-1's contents") 382 } 383 384 // The `docker cp` command should also ensure that you cannot 385 // write to a container rootfs that is marked as read-only. 386 func (s *DockerSuite) TestCpToErrReadOnlyRootfs(c *testing.T) { 387 // --read-only + userns has remount issues 388 testRequires(c, DaemonIsLinux, NotUserNamespace) 389 tmpDir := getTestDir(c, "test-cp-to-err-read-only-rootfs") 390 defer os.RemoveAll(tmpDir) 391 392 makeTestContentInDir(c, tmpDir) 393 394 containerID := makeTestContainer(c, testContainerOptions{ 395 readOnly: true, workDir: "/root", 396 command: makeCatFileCommand("shouldNotExist"), 397 }) 398 399 srcPath := cpPath(tmpDir, "file1") 400 dstPath := containerCpPath(containerID, "/root/shouldNotExist") 401 402 err := runDockerCp(c, srcPath, dstPath) 403 assert.ErrorContains(c, err, "") 404 405 assert.Assert(c, isCpCannotCopyReadOnly(err), "expected ErrContainerRootfsReadonly error, but got %T: %s", err, err) 406 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 407 } 408 409 // The `docker cp` command should also ensure that you 410 // cannot write to a volume that is mounted as read-only. 411 func (s *DockerSuite) TestCpToErrReadOnlyVolume(c *testing.T) { 412 // --read-only + userns has remount issues 413 testRequires(c, DaemonIsLinux, NotUserNamespace) 414 tmpDir := getTestDir(c, "test-cp-to-err-read-only-volume") 415 defer os.RemoveAll(tmpDir) 416 417 makeTestContentInDir(c, tmpDir) 418 419 containerID := makeTestContainer(c, testContainerOptions{ 420 volumes: defaultVolumes(tmpDir), workDir: "/root", 421 command: makeCatFileCommand("/vol_ro/shouldNotExist"), 422 }) 423 424 srcPath := cpPath(tmpDir, "file1") 425 dstPath := containerCpPath(containerID, "/vol_ro/shouldNotExist") 426 427 err := runDockerCp(c, srcPath, dstPath) 428 assert.ErrorContains(c, err, "") 429 430 assert.Assert(c, isCpCannotCopyReadOnly(err), "expected ErrVolumeReadonly error, but got %T: %s", err, err) 431 assert.NilError(c, containerStartOutputEquals(c, containerID, ""), "dstPath should not have existed") 432 }