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