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