github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/pkg/archive/changes_test.go (about) 1 package archive // import "github.com/docker/docker/pkg/archive" 2 3 import ( 4 "os" 5 "os/exec" 6 "path" 7 "path/filepath" 8 "runtime" 9 "sort" 10 "syscall" 11 "testing" 12 "time" 13 14 "github.com/docker/docker/pkg/system" 15 "gotest.tools/v3/assert" 16 "gotest.tools/v3/skip" 17 ) 18 19 func max(x, y int) int { 20 if x >= y { 21 return x 22 } 23 return y 24 } 25 26 func copyDir(src, dst string) error { 27 if runtime.GOOS != "windows" { 28 return exec.Command("cp", "-a", src, dst).Run() 29 } 30 31 // Could have used xcopy src dst /E /I /H /Y /B. However, xcopy has the 32 // unfortunate side effect of not preserving timestamps of newly created 33 // directories in the target directory, so we don't get accurate changes. 34 // Use robocopy instead. Note this isn't available in microsoft/nanoserver. 35 // But it has gotchas. See https://weblogs.sqlteam.com/robv/archive/2010/02/17/61106.aspx 36 err := exec.Command("robocopy", filepath.FromSlash(src), filepath.FromSlash(dst), "/SL", "/COPYALL", "/MIR").Run() 37 if exiterr, ok := err.(*exec.ExitError); ok { 38 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 39 if status.ExitStatus()&24 == 0 { 40 return nil 41 } 42 } 43 } 44 return err 45 } 46 47 type FileType uint32 48 49 const ( 50 Regular FileType = iota 51 Dir 52 Symlink 53 ) 54 55 type FileData struct { 56 filetype FileType 57 path string 58 contents string 59 permissions os.FileMode 60 } 61 62 func createSampleDir(t *testing.T, root string) { 63 files := []FileData{ 64 {filetype: Regular, path: "file1", contents: "file1\n", permissions: 0600}, 65 {filetype: Regular, path: "file2", contents: "file2\n", permissions: 0666}, 66 {filetype: Regular, path: "file3", contents: "file3\n", permissions: 0404}, 67 {filetype: Regular, path: "file4", contents: "file4\n", permissions: 0600}, 68 {filetype: Regular, path: "file5", contents: "file5\n", permissions: 0600}, 69 {filetype: Regular, path: "file6", contents: "file6\n", permissions: 0600}, 70 {filetype: Regular, path: "file7", contents: "file7\n", permissions: 0600}, 71 {filetype: Dir, path: "dir1", contents: "", permissions: 0740}, 72 {filetype: Regular, path: "dir1/file1-1", contents: "file1-1\n", permissions: 01444}, 73 {filetype: Regular, path: "dir1/file1-2", contents: "file1-2\n", permissions: 0666}, 74 {filetype: Dir, path: "dir2", contents: "", permissions: 0700}, 75 {filetype: Regular, path: "dir2/file2-1", contents: "file2-1\n", permissions: 0666}, 76 {filetype: Regular, path: "dir2/file2-2", contents: "file2-2\n", permissions: 0666}, 77 {filetype: Dir, path: "dir3", contents: "", permissions: 0700}, 78 {filetype: Regular, path: "dir3/file3-1", contents: "file3-1\n", permissions: 0666}, 79 {filetype: Regular, path: "dir3/file3-2", contents: "file3-2\n", permissions: 0666}, 80 {filetype: Dir, path: "dir4", contents: "", permissions: 0700}, 81 {filetype: Regular, path: "dir4/file3-1", contents: "file4-1\n", permissions: 0666}, 82 {filetype: Regular, path: "dir4/file3-2", contents: "file4-2\n", permissions: 0666}, 83 {filetype: Symlink, path: "symlink1", contents: "target1", permissions: 0666}, 84 {filetype: Symlink, path: "symlink2", contents: "target2", permissions: 0666}, 85 {filetype: Symlink, path: "symlink3", contents: root + "/file1", permissions: 0666}, 86 {filetype: Symlink, path: "symlink4", contents: root + "/symlink3", permissions: 0666}, 87 {filetype: Symlink, path: "dirSymlink", contents: root + "/dir1", permissions: 0740}, 88 } 89 provisionSampleDir(t, root, files) 90 } 91 92 func provisionSampleDir(t *testing.T, root string, files []FileData) { 93 now := time.Now() 94 for _, info := range files { 95 p := path.Join(root, info.path) 96 if info.filetype == Dir { 97 err := os.MkdirAll(p, info.permissions) 98 assert.NilError(t, err) 99 } else if info.filetype == Regular { 100 err := os.WriteFile(p, []byte(info.contents), info.permissions) 101 assert.NilError(t, err) 102 } else if info.filetype == Symlink { 103 err := os.Symlink(info.contents, p) 104 assert.NilError(t, err) 105 } 106 107 if info.filetype != Symlink { 108 // Set a consistent ctime, atime for all files and dirs 109 err := system.Chtimes(p, now, now) 110 assert.NilError(t, err) 111 } 112 } 113 } 114 115 func TestChangeString(t *testing.T) { 116 modifyChange := Change{"change", ChangeModify} 117 toString := modifyChange.String() 118 if toString != "C change" { 119 t.Fatalf("String() of a change with ChangeModify Kind should have been %s but was %s", "C change", toString) 120 } 121 addChange := Change{"change", ChangeAdd} 122 toString = addChange.String() 123 if toString != "A change" { 124 t.Fatalf("String() of a change with ChangeAdd Kind should have been %s but was %s", "A change", toString) 125 } 126 deleteChange := Change{"change", ChangeDelete} 127 toString = deleteChange.String() 128 if toString != "D change" { 129 t.Fatalf("String() of a change with ChangeDelete Kind should have been %s but was %s", "D change", toString) 130 } 131 } 132 133 func TestChangesWithNoChanges(t *testing.T) { 134 rwLayer, err := os.MkdirTemp("", "docker-changes-test") 135 assert.NilError(t, err) 136 defer os.RemoveAll(rwLayer) 137 layer, err := os.MkdirTemp("", "docker-changes-test-layer") 138 assert.NilError(t, err) 139 defer os.RemoveAll(layer) 140 createSampleDir(t, layer) 141 changes, err := Changes([]string{layer}, rwLayer) 142 assert.NilError(t, err) 143 if len(changes) != 0 { 144 t.Fatalf("Changes with no difference should have detect no changes, but detected %d", len(changes)) 145 } 146 } 147 148 func TestChangesWithChanges(t *testing.T) { 149 // Mock the readonly layer 150 layer, err := os.MkdirTemp("", "docker-changes-test-layer") 151 assert.NilError(t, err) 152 defer os.RemoveAll(layer) 153 createSampleDir(t, layer) 154 os.MkdirAll(path.Join(layer, "dir1/subfolder"), 0740) 155 156 // Mock the RW layer 157 rwLayer, err := os.MkdirTemp("", "docker-changes-test") 158 assert.NilError(t, err) 159 defer os.RemoveAll(rwLayer) 160 161 // Create a folder in RW layer 162 dir1 := path.Join(rwLayer, "dir1") 163 os.MkdirAll(dir1, 0740) 164 deletedFile := path.Join(dir1, ".wh.file1-2") 165 os.WriteFile(deletedFile, []byte{}, 0600) 166 modifiedFile := path.Join(dir1, "file1-1") 167 os.WriteFile(modifiedFile, []byte{0x00}, 01444) 168 // Let's add a subfolder for a newFile 169 subfolder := path.Join(dir1, "subfolder") 170 os.MkdirAll(subfolder, 0740) 171 newFile := path.Join(subfolder, "newFile") 172 os.WriteFile(newFile, []byte{}, 0740) 173 174 changes, err := Changes([]string{layer}, rwLayer) 175 assert.NilError(t, err) 176 177 expectedChanges := []Change{ 178 {filepath.FromSlash("/dir1"), ChangeModify}, 179 {filepath.FromSlash("/dir1/file1-1"), ChangeModify}, 180 {filepath.FromSlash("/dir1/file1-2"), ChangeDelete}, 181 {filepath.FromSlash("/dir1/subfolder"), ChangeModify}, 182 {filepath.FromSlash("/dir1/subfolder/newFile"), ChangeAdd}, 183 } 184 checkChanges(expectedChanges, changes, t) 185 } 186 187 // See https://github.com/docker/docker/pull/13590 188 func TestChangesWithChangesGH13590(t *testing.T) { 189 // TODO Windows. Needs further investigation to identify the failure 190 if runtime.GOOS == "windows" { 191 t.Skip("needs more investigation") 192 } 193 baseLayer, err := os.MkdirTemp("", "docker-changes-test.") 194 assert.NilError(t, err) 195 defer os.RemoveAll(baseLayer) 196 197 dir3 := path.Join(baseLayer, "dir1/dir2/dir3") 198 os.MkdirAll(dir3, 07400) 199 200 file := path.Join(dir3, "file.txt") 201 os.WriteFile(file, []byte("hello"), 0666) 202 203 layer, err := os.MkdirTemp("", "docker-changes-test2.") 204 assert.NilError(t, err) 205 defer os.RemoveAll(layer) 206 207 // Test creating a new file 208 if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil { 209 t.Fatalf("Cmd failed: %q", err) 210 } 211 212 os.Remove(path.Join(layer, "dir1/dir2/dir3/file.txt")) 213 file = path.Join(layer, "dir1/dir2/dir3/file1.txt") 214 os.WriteFile(file, []byte("bye"), 0666) 215 216 changes, err := Changes([]string{baseLayer}, layer) 217 assert.NilError(t, err) 218 219 expectedChanges := []Change{ 220 {"/dir1/dir2/dir3", ChangeModify}, 221 {"/dir1/dir2/dir3/file1.txt", ChangeAdd}, 222 } 223 checkChanges(expectedChanges, changes, t) 224 225 // Now test changing a file 226 layer, err = os.MkdirTemp("", "docker-changes-test3.") 227 assert.NilError(t, err) 228 defer os.RemoveAll(layer) 229 230 if err := copyDir(baseLayer+"/dir1", layer+"/"); err != nil { 231 t.Fatalf("Cmd failed: %q", err) 232 } 233 234 file = path.Join(layer, "dir1/dir2/dir3/file.txt") 235 os.WriteFile(file, []byte("bye"), 0666) 236 237 changes, err = Changes([]string{baseLayer}, layer) 238 assert.NilError(t, err) 239 240 expectedChanges = []Change{ 241 {"/dir1/dir2/dir3/file.txt", ChangeModify}, 242 } 243 checkChanges(expectedChanges, changes, t) 244 } 245 246 // Create a directory, copy it, make sure we report no changes between the two 247 func TestChangesDirsEmpty(t *testing.T) { 248 src, err := os.MkdirTemp("", "docker-changes-test") 249 assert.NilError(t, err) 250 defer os.RemoveAll(src) 251 createSampleDir(t, src) 252 dst := src + "-copy" 253 err = copyDir(src, dst) 254 assert.NilError(t, err) 255 defer os.RemoveAll(dst) 256 changes, err := ChangesDirs(dst, src) 257 assert.NilError(t, err) 258 259 if len(changes) != 0 { 260 t.Fatalf("Reported changes for identical dirs: %v", changes) 261 } 262 os.RemoveAll(src) 263 os.RemoveAll(dst) 264 } 265 266 func mutateSampleDir(t *testing.T, root string) { 267 // Remove a regular file 268 err := os.RemoveAll(path.Join(root, "file1")) 269 assert.NilError(t, err) 270 271 // Remove a directory 272 err = os.RemoveAll(path.Join(root, "dir1")) 273 assert.NilError(t, err) 274 275 // Remove a symlink 276 err = os.RemoveAll(path.Join(root, "symlink1")) 277 assert.NilError(t, err) 278 279 // Rewrite a file 280 err = os.WriteFile(path.Join(root, "file2"), []byte("fileNN\n"), 0777) 281 assert.NilError(t, err) 282 283 // Replace a file 284 err = os.RemoveAll(path.Join(root, "file3")) 285 assert.NilError(t, err) 286 err = os.WriteFile(path.Join(root, "file3"), []byte("fileMM\n"), 0404) 287 assert.NilError(t, err) 288 289 // Touch file 290 err = system.Chtimes(path.Join(root, "file4"), time.Now().Add(time.Second), time.Now().Add(time.Second)) 291 assert.NilError(t, err) 292 293 // Replace file with dir 294 err = os.RemoveAll(path.Join(root, "file5")) 295 assert.NilError(t, err) 296 err = os.MkdirAll(path.Join(root, "file5"), 0666) 297 assert.NilError(t, err) 298 299 // Create new file 300 err = os.WriteFile(path.Join(root, "filenew"), []byte("filenew\n"), 0777) 301 assert.NilError(t, err) 302 303 // Create new dir 304 err = os.MkdirAll(path.Join(root, "dirnew"), 0766) 305 assert.NilError(t, err) 306 307 // Create a new symlink 308 err = os.Symlink("targetnew", path.Join(root, "symlinknew")) 309 assert.NilError(t, err) 310 311 // Change a symlink 312 err = os.RemoveAll(path.Join(root, "symlink2")) 313 assert.NilError(t, err) 314 315 err = os.Symlink("target2change", path.Join(root, "symlink2")) 316 assert.NilError(t, err) 317 318 // Replace dir with file 319 err = os.RemoveAll(path.Join(root, "dir2")) 320 assert.NilError(t, err) 321 err = os.WriteFile(path.Join(root, "dir2"), []byte("dir2\n"), 0777) 322 assert.NilError(t, err) 323 324 // Touch dir 325 err = system.Chtimes(path.Join(root, "dir3"), time.Now().Add(time.Second), time.Now().Add(time.Second)) 326 assert.NilError(t, err) 327 } 328 329 func TestChangesDirsMutated(t *testing.T) { 330 src, err := os.MkdirTemp("", "docker-changes-test") 331 assert.NilError(t, err) 332 createSampleDir(t, src) 333 dst := src + "-copy" 334 err = copyDir(src, dst) 335 assert.NilError(t, err) 336 defer os.RemoveAll(src) 337 defer os.RemoveAll(dst) 338 339 mutateSampleDir(t, dst) 340 341 changes, err := ChangesDirs(dst, src) 342 assert.NilError(t, err) 343 344 sort.Sort(changesByPath(changes)) 345 346 expectedChanges := []Change{ 347 {filepath.FromSlash("/dir1"), ChangeDelete}, 348 {filepath.FromSlash("/dir2"), ChangeModify}, 349 } 350 351 // Note there is slight difference between the Linux and Windows 352 // implementations here. Due to https://github.com/moby/moby/issues/9874, 353 // and the fix at https://github.com/moby/moby/pull/11422, Linux does not 354 // consider a change to the directory time as a change. Windows on NTFS 355 // does. See https://github.com/moby/moby/pull/37982 for more information. 356 // 357 // Note also: https://github.com/moby/moby/pull/37982#discussion_r223523114 358 // that differences are ordered in the way the test is currently written, hence 359 // this is in the middle of the list of changes rather than at the start or 360 // end. Potentially can be addressed later. 361 if runtime.GOOS == "windows" { 362 expectedChanges = append(expectedChanges, Change{filepath.FromSlash("/dir3"), ChangeModify}) 363 } 364 365 expectedChanges = append(expectedChanges, []Change{ 366 {filepath.FromSlash("/dirnew"), ChangeAdd}, 367 {filepath.FromSlash("/file1"), ChangeDelete}, 368 {filepath.FromSlash("/file2"), ChangeModify}, 369 {filepath.FromSlash("/file3"), ChangeModify}, 370 {filepath.FromSlash("/file4"), ChangeModify}, 371 {filepath.FromSlash("/file5"), ChangeModify}, 372 {filepath.FromSlash("/filenew"), ChangeAdd}, 373 {filepath.FromSlash("/symlink1"), ChangeDelete}, 374 {filepath.FromSlash("/symlink2"), ChangeModify}, 375 {filepath.FromSlash("/symlinknew"), ChangeAdd}, 376 }...) 377 378 for i := 0; i < max(len(changes), len(expectedChanges)); i++ { 379 if i >= len(expectedChanges) { 380 t.Fatalf("unexpected change %s\n", changes[i].String()) 381 } 382 if i >= len(changes) { 383 t.Fatalf("no change for expected change %s\n", expectedChanges[i].String()) 384 } 385 if changes[i].Path == expectedChanges[i].Path { 386 if changes[i] != expectedChanges[i] { 387 t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String()) 388 } 389 } else if changes[i].Path < expectedChanges[i].Path { 390 t.Fatalf("unexpected change %q %q\n", changes[i].String(), expectedChanges[i].Path) 391 } else { 392 t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String()) 393 } 394 } 395 } 396 397 func TestApplyLayer(t *testing.T) { 398 // TODO Windows. This is very close to working, but it fails with changes 399 // to \symlinknew and \symlink2. The destination has an updated 400 // Access/Modify/Change/Birth date to the source (~3/100th sec different). 401 // Needs further investigation as to why, but I currently believe this is 402 // just the way NTFS works. I don't think it's a bug in this test or archive. 403 if runtime.GOOS == "windows" { 404 t.Skip("needs further investigation") 405 } 406 src, err := os.MkdirTemp("", "docker-changes-test") 407 assert.NilError(t, err) 408 createSampleDir(t, src) 409 defer os.RemoveAll(src) 410 dst := src + "-copy" 411 err = copyDir(src, dst) 412 assert.NilError(t, err) 413 mutateSampleDir(t, dst) 414 defer os.RemoveAll(dst) 415 416 changes, err := ChangesDirs(dst, src) 417 assert.NilError(t, err) 418 419 layer, err := ExportChanges(dst, changes, nil, nil) 420 assert.NilError(t, err) 421 422 layerCopy, err := NewTempArchive(layer, "") 423 assert.NilError(t, err) 424 425 _, err = ApplyLayer(src, layerCopy) 426 assert.NilError(t, err) 427 428 changes2, err := ChangesDirs(src, dst) 429 assert.NilError(t, err) 430 431 if len(changes2) != 0 { 432 t.Fatalf("Unexpected differences after reapplying mutation: %v", changes2) 433 } 434 } 435 436 func TestChangesSizeWithHardlinks(t *testing.T) { 437 // TODO Windows. Needs further investigation. Likely in ChangeSizes not 438 // coping correctly with hardlinks on Windows. 439 if runtime.GOOS == "windows" { 440 t.Skip("needs further investigation") 441 } 442 srcDir, err := os.MkdirTemp("", "docker-test-srcDir") 443 assert.NilError(t, err) 444 defer os.RemoveAll(srcDir) 445 446 destDir, err := os.MkdirTemp("", "docker-test-destDir") 447 assert.NilError(t, err) 448 defer os.RemoveAll(destDir) 449 450 creationSize, err := prepareUntarSourceDirectory(100, destDir, true) 451 assert.NilError(t, err) 452 453 changes, err := ChangesDirs(destDir, srcDir) 454 assert.NilError(t, err) 455 456 got := ChangesSize(destDir, changes) 457 if got != int64(creationSize) { 458 t.Errorf("Expected %d bytes of changes, got %d", creationSize, got) 459 } 460 } 461 462 func TestChangesSizeWithNoChanges(t *testing.T) { 463 size := ChangesSize("/tmp", nil) 464 if size != 0 { 465 t.Fatalf("ChangesSizes with no changes should be 0, was %d", size) 466 } 467 } 468 469 func TestChangesSizeWithOnlyDeleteChanges(t *testing.T) { 470 changes := []Change{ 471 {Path: "deletedPath", Kind: ChangeDelete}, 472 } 473 size := ChangesSize("/tmp", changes) 474 if size != 0 { 475 t.Fatalf("ChangesSizes with only delete changes should be 0, was %d", size) 476 } 477 } 478 479 func TestChangesSize(t *testing.T) { 480 parentPath, err := os.MkdirTemp("", "docker-changes-test") 481 assert.NilError(t, err) 482 defer os.RemoveAll(parentPath) 483 addition := path.Join(parentPath, "addition") 484 err = os.WriteFile(addition, []byte{0x01, 0x01, 0x01}, 0744) 485 assert.NilError(t, err) 486 modification := path.Join(parentPath, "modification") 487 err = os.WriteFile(modification, []byte{0x01, 0x01, 0x01}, 0744) 488 assert.NilError(t, err) 489 490 changes := []Change{ 491 {Path: "addition", Kind: ChangeAdd}, 492 {Path: "modification", Kind: ChangeModify}, 493 } 494 size := ChangesSize(parentPath, changes) 495 if size != 6 { 496 t.Fatalf("Expected 6 bytes of changes, got %d", size) 497 } 498 } 499 500 func checkChanges(expectedChanges, changes []Change, t *testing.T) { 501 skip.If(t, runtime.GOOS != "windows" && os.Getuid() != 0, "skipping test that requires root") 502 sort.Sort(changesByPath(expectedChanges)) 503 sort.Sort(changesByPath(changes)) 504 for i := 0; i < max(len(changes), len(expectedChanges)); i++ { 505 if i >= len(expectedChanges) { 506 t.Fatalf("unexpected change %s\n", changes[i].String()) 507 } 508 if i >= len(changes) { 509 t.Fatalf("no change for expected change %s\n", expectedChanges[i].String()) 510 } 511 if changes[i].Path == expectedChanges[i].Path { 512 if changes[i] != expectedChanges[i] { 513 t.Fatalf("Wrong change for %s, expected %s, got %s\n", changes[i].Path, changes[i].String(), expectedChanges[i].String()) 514 } 515 } else if changes[i].Path < expectedChanges[i].Path { 516 t.Fatalf("unexpected change %s\n", changes[i].String()) 517 } else { 518 t.Fatalf("no change for expected change %s != %s\n", expectedChanges[i].String(), changes[i].String()) 519 } 520 } 521 }