github.com/kaisenlinux/docker@v0.0.0-20230510090727-ea55db55fac7/engine/layer/layer_test.go (about) 1 package layer // import "github.com/docker/docker/layer" 2 3 import ( 4 "bytes" 5 "io" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "testing" 11 12 "github.com/containerd/continuity/driver" 13 "github.com/docker/docker/daemon/graphdriver" 14 "github.com/docker/docker/daemon/graphdriver/vfs" 15 "github.com/docker/docker/pkg/archive" 16 "github.com/docker/docker/pkg/containerfs" 17 "github.com/docker/docker/pkg/idtools" 18 "github.com/docker/docker/pkg/stringid" 19 digest "github.com/opencontainers/go-digest" 20 ) 21 22 func init() { 23 graphdriver.ApplyUncompressedLayer = archive.UnpackLayer 24 defaultArchiver := archive.NewDefaultArchiver() 25 vfs.CopyDir = defaultArchiver.CopyWithTar 26 } 27 28 func newVFSGraphDriver(td string) (graphdriver.Driver, error) { 29 uidMap := []idtools.IDMap{ 30 { 31 ContainerID: 0, 32 HostID: os.Getuid(), 33 Size: 1, 34 }, 35 } 36 gidMap := []idtools.IDMap{ 37 { 38 ContainerID: 0, 39 HostID: os.Getgid(), 40 Size: 1, 41 }, 42 } 43 44 options := graphdriver.Options{Root: td, UIDMaps: uidMap, GIDMaps: gidMap} 45 return graphdriver.GetDriver("vfs", nil, options) 46 } 47 48 func newTestGraphDriver(t *testing.T) (graphdriver.Driver, func()) { 49 td, err := os.MkdirTemp("", "graph-") 50 if err != nil { 51 t.Fatal(err) 52 } 53 54 driver, err := newVFSGraphDriver(td) 55 if err != nil { 56 t.Fatal(err) 57 } 58 59 return driver, func() { 60 os.RemoveAll(td) 61 } 62 } 63 64 func newTestStore(t *testing.T) (Store, string, func()) { 65 td, err := os.MkdirTemp("", "layerstore-") 66 if err != nil { 67 t.Fatal(err) 68 } 69 70 graph, graphcleanup := newTestGraphDriver(t) 71 72 ls, err := newStoreFromGraphDriver(td, graph, runtime.GOOS) 73 if err != nil { 74 t.Fatal(err) 75 } 76 77 return ls, td, func() { 78 graphcleanup() 79 os.RemoveAll(td) 80 } 81 } 82 83 type layerInit func(root containerfs.ContainerFS) error 84 85 func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { 86 containerID := stringid.GenerateRandomID() 87 mount, err := ls.CreateRWLayer(containerID, parent, nil) 88 if err != nil { 89 return nil, err 90 } 91 92 pathFS, err := mount.Mount("") 93 if err != nil { 94 return nil, err 95 } 96 97 if err := layerFunc(pathFS); err != nil { 98 return nil, err 99 } 100 101 ts, err := mount.TarStream() 102 if err != nil { 103 return nil, err 104 } 105 defer ts.Close() 106 107 layer, err := ls.Register(ts, parent) 108 if err != nil { 109 return nil, err 110 } 111 112 if err := mount.Unmount(); err != nil { 113 return nil, err 114 } 115 116 if _, err := ls.ReleaseRWLayer(mount); err != nil { 117 return nil, err 118 } 119 120 return layer, nil 121 } 122 123 type FileApplier interface { 124 ApplyFile(root containerfs.ContainerFS) error 125 } 126 127 type testFile struct { 128 name string 129 content []byte 130 permission os.FileMode 131 } 132 133 func newTestFile(name string, content []byte, perm os.FileMode) FileApplier { 134 return &testFile{ 135 name: name, 136 content: content, 137 permission: perm, 138 } 139 } 140 141 func (tf *testFile) ApplyFile(root containerfs.ContainerFS) error { 142 fullPath := root.Join(root.Path(), tf.name) 143 if err := root.MkdirAll(root.Dir(fullPath), 0755); err != nil { 144 return err 145 } 146 // Check if already exists 147 if stat, err := root.Stat(fullPath); err == nil && stat.Mode().Perm() != tf.permission { 148 if err := root.Lchmod(fullPath, tf.permission); err != nil { 149 return err 150 } 151 } 152 return driver.WriteFile(root, fullPath, tf.content, tf.permission) 153 } 154 155 func initWithFiles(files ...FileApplier) layerInit { 156 return func(root containerfs.ContainerFS) error { 157 for _, f := range files { 158 if err := f.ApplyFile(root); err != nil { 159 return err 160 } 161 } 162 return nil 163 } 164 } 165 166 func getCachedLayer(l Layer) *roLayer { 167 if rl, ok := l.(*referencedCacheLayer); ok { 168 return rl.roLayer 169 } 170 return l.(*roLayer) 171 } 172 173 func createMetadata(layers ...Layer) []Metadata { 174 metadata := make([]Metadata, len(layers)) 175 for i := range layers { 176 size, err := layers[i].Size() 177 if err != nil { 178 panic(err) 179 } 180 181 metadata[i].ChainID = layers[i].ChainID() 182 metadata[i].DiffID = layers[i].DiffID() 183 metadata[i].Size = size 184 metadata[i].DiffSize = getCachedLayer(layers[i]).size 185 } 186 187 return metadata 188 } 189 190 func assertMetadata(t *testing.T, metadata, expectedMetadata []Metadata) { 191 if len(metadata) != len(expectedMetadata) { 192 t.Fatalf("Unexpected number of deletes %d, expected %d", len(metadata), len(expectedMetadata)) 193 } 194 195 for i := range metadata { 196 if metadata[i] != expectedMetadata[i] { 197 t.Errorf("Unexpected metadata\n\tExpected: %#v\n\tActual: %#v", expectedMetadata[i], metadata[i]) 198 } 199 } 200 if t.Failed() { 201 t.FailNow() 202 } 203 } 204 205 func releaseAndCheckDeleted(t *testing.T, ls Store, layer Layer, removed ...Layer) { 206 layerCount := len(ls.(*layerStore).layerMap) 207 expectedMetadata := createMetadata(removed...) 208 metadata, err := ls.Release(layer) 209 if err != nil { 210 t.Fatal(err) 211 } 212 213 assertMetadata(t, metadata, expectedMetadata) 214 215 if expected := layerCount - len(removed); len(ls.(*layerStore).layerMap) != expected { 216 t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected) 217 } 218 } 219 220 func cacheID(l Layer) string { 221 return getCachedLayer(l).cacheID 222 } 223 224 func assertLayerEqual(t *testing.T, l1, l2 Layer) { 225 if l1.ChainID() != l2.ChainID() { 226 t.Fatalf("Mismatched ChainID: %s vs %s", l1.ChainID(), l2.ChainID()) 227 } 228 if l1.DiffID() != l2.DiffID() { 229 t.Fatalf("Mismatched DiffID: %s vs %s", l1.DiffID(), l2.DiffID()) 230 } 231 232 size1, err := l1.Size() 233 if err != nil { 234 t.Fatal(err) 235 } 236 237 size2, err := l2.Size() 238 if err != nil { 239 t.Fatal(err) 240 } 241 242 if size1 != size2 { 243 t.Fatalf("Mismatched size: %d vs %d", size1, size2) 244 } 245 246 if cacheID(l1) != cacheID(l2) { 247 t.Fatalf("Mismatched cache id: %s vs %s", cacheID(l1), cacheID(l2)) 248 } 249 250 p1 := l1.Parent() 251 p2 := l2.Parent() 252 if p1 != nil && p2 != nil { 253 assertLayerEqual(t, p1, p2) 254 } else if p1 != nil || p2 != nil { 255 t.Fatalf("Mismatched parents: %v vs %v", p1, p2) 256 } 257 } 258 259 func TestMountAndRegister(t *testing.T) { 260 ls, _, cleanup := newTestStore(t) 261 defer cleanup() 262 263 li := initWithFiles(newTestFile("testfile.txt", []byte("some test data"), 0644)) 264 layer, err := createLayer(ls, "", li) 265 if err != nil { 266 t.Fatal(err) 267 } 268 269 size, _ := layer.Size() 270 t.Logf("Layer size: %d", size) 271 272 mount2, err := ls.CreateRWLayer("new-test-mount", layer.ChainID(), nil) 273 if err != nil { 274 t.Fatal(err) 275 } 276 277 path2, err := mount2.Mount("") 278 if err != nil { 279 t.Fatal(err) 280 } 281 282 b, err := driver.ReadFile(path2, path2.Join(path2.Path(), "testfile.txt")) 283 if err != nil { 284 t.Fatal(err) 285 } 286 287 if expected := "some test data"; string(b) != expected { 288 t.Fatalf("Wrong file data, expected %q, got %q", expected, string(b)) 289 } 290 291 if err := mount2.Unmount(); err != nil { 292 t.Fatal(err) 293 } 294 295 if _, err := ls.ReleaseRWLayer(mount2); err != nil { 296 t.Fatal(err) 297 } 298 } 299 300 func TestLayerRelease(t *testing.T) { 301 // TODO Windows: Figure out why this is failing 302 if runtime.GOOS == "windows" { 303 t.Skip("Failing on Windows") 304 } 305 ls, _, cleanup := newTestStore(t) 306 defer cleanup() 307 308 layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644))) 309 if err != nil { 310 t.Fatal(err) 311 } 312 313 layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644))) 314 if err != nil { 315 t.Fatal(err) 316 } 317 318 if _, err := ls.Release(layer1); err != nil { 319 t.Fatal(err) 320 } 321 322 layer3a, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3a file"), 0644))) 323 if err != nil { 324 t.Fatal(err) 325 } 326 327 layer3b, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3b file"), 0644))) 328 if err != nil { 329 t.Fatal(err) 330 } 331 332 if _, err := ls.Release(layer2); err != nil { 333 t.Fatal(err) 334 } 335 336 t.Logf("Layer1: %s", layer1.ChainID()) 337 t.Logf("Layer2: %s", layer2.ChainID()) 338 t.Logf("Layer3a: %s", layer3a.ChainID()) 339 t.Logf("Layer3b: %s", layer3b.ChainID()) 340 341 if expected := 4; len(ls.(*layerStore).layerMap) != expected { 342 t.Fatalf("Unexpected number of layers %d, expected %d", len(ls.(*layerStore).layerMap), expected) 343 } 344 345 releaseAndCheckDeleted(t, ls, layer3b, layer3b) 346 releaseAndCheckDeleted(t, ls, layer3a, layer3a, layer2, layer1) 347 } 348 349 func TestStoreRestore(t *testing.T) { 350 // TODO Windows: Figure out why this is failing 351 if runtime.GOOS == "windows" { 352 t.Skip("Failing on Windows") 353 } 354 ls, _, cleanup := newTestStore(t) 355 defer cleanup() 356 357 layer1, err := createLayer(ls, "", initWithFiles(newTestFile("layer1.txt", []byte("layer 1 file"), 0644))) 358 if err != nil { 359 t.Fatal(err) 360 } 361 362 layer2, err := createLayer(ls, layer1.ChainID(), initWithFiles(newTestFile("layer2.txt", []byte("layer 2 file"), 0644))) 363 if err != nil { 364 t.Fatal(err) 365 } 366 367 if _, err := ls.Release(layer1); err != nil { 368 t.Fatal(err) 369 } 370 371 layer3, err := createLayer(ls, layer2.ChainID(), initWithFiles(newTestFile("layer3.txt", []byte("layer 3 file"), 0644))) 372 if err != nil { 373 t.Fatal(err) 374 } 375 376 if _, err := ls.Release(layer2); err != nil { 377 t.Fatal(err) 378 } 379 380 m, err := ls.CreateRWLayer("some-mount_name", layer3.ChainID(), nil) 381 if err != nil { 382 t.Fatal(err) 383 } 384 385 pathFS, err := m.Mount("") 386 if err != nil { 387 t.Fatal(err) 388 } 389 390 if err := driver.WriteFile(pathFS, pathFS.Join(pathFS.Path(), "testfile.txt"), []byte("nothing here"), 0644); err != nil { 391 t.Fatal(err) 392 } 393 394 if err := m.Unmount(); err != nil { 395 t.Fatal(err) 396 } 397 398 ls2, err := newStoreFromGraphDriver(ls.(*layerStore).store.root, ls.(*layerStore).driver, runtime.GOOS) 399 if err != nil { 400 t.Fatal(err) 401 } 402 403 layer3b, err := ls2.Get(layer3.ChainID()) 404 if err != nil { 405 t.Fatal(err) 406 } 407 408 assertLayerEqual(t, layer3b, layer3) 409 410 // Create again with same name, should return error 411 if _, err := ls2.CreateRWLayer("some-mount_name", layer3b.ChainID(), nil); err == nil { 412 t.Fatal("Expected error creating mount with same name") 413 } else if err != ErrMountNameConflict { 414 t.Fatal(err) 415 } 416 417 m2, err := ls2.GetRWLayer("some-mount_name") 418 if err != nil { 419 t.Fatal(err) 420 } 421 422 if mountPath, err := m2.Mount(""); err != nil { 423 t.Fatal(err) 424 } else if pathFS.Path() != mountPath.Path() { 425 t.Fatalf("Unexpected path %s, expected %s", mountPath.Path(), pathFS.Path()) 426 } 427 428 if mountPath, err := m2.Mount(""); err != nil { 429 t.Fatal(err) 430 } else if pathFS.Path() != mountPath.Path() { 431 t.Fatalf("Unexpected path %s, expected %s", mountPath.Path(), pathFS.Path()) 432 } 433 if err := m2.Unmount(); err != nil { 434 t.Fatal(err) 435 } 436 437 b, err := driver.ReadFile(pathFS, pathFS.Join(pathFS.Path(), "testfile.txt")) 438 if err != nil { 439 t.Fatal(err) 440 } 441 if expected := "nothing here"; string(b) != expected { 442 t.Fatalf("Unexpected content %q, expected %q", string(b), expected) 443 } 444 445 if err := m2.Unmount(); err != nil { 446 t.Fatal(err) 447 } 448 449 if metadata, err := ls2.ReleaseRWLayer(m2); err != nil { 450 t.Fatal(err) 451 } else if len(metadata) != 0 { 452 t.Fatalf("Unexpectedly deleted layers: %#v", metadata) 453 } 454 455 if metadata, err := ls2.ReleaseRWLayer(m2); err != nil { 456 t.Fatal(err) 457 } else if len(metadata) != 0 { 458 t.Fatalf("Unexpectedly deleted layers: %#v", metadata) 459 } 460 461 releaseAndCheckDeleted(t, ls2, layer3b, layer3, layer2, layer1) 462 } 463 464 func TestTarStreamStability(t *testing.T) { 465 // TODO Windows: Figure out why this is failing 466 if runtime.GOOS == "windows" { 467 t.Skip("Failing on Windows") 468 } 469 ls, _, cleanup := newTestStore(t) 470 defer cleanup() 471 472 files1 := []FileApplier{ 473 newTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644), 474 newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644), 475 } 476 addedFile := newTestFile("/etc/shadow", []byte("root:::::::"), 0644) 477 files2 := []FileApplier{ 478 newTestFile("/etc/hosts", []byte("mydomain 10.0.0.2"), 0644), 479 newTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0664), 480 newTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644), 481 } 482 483 tar1, err := tarFromFiles(files1...) 484 if err != nil { 485 t.Fatal(err) 486 } 487 488 tar2, err := tarFromFiles(files2...) 489 if err != nil { 490 t.Fatal(err) 491 } 492 493 layer1, err := ls.Register(bytes.NewReader(tar1), "") 494 if err != nil { 495 t.Fatal(err) 496 } 497 498 // hack layer to add file 499 p, err := ls.(*layerStore).driver.Get(layer1.(*referencedCacheLayer).cacheID, "") 500 if err != nil { 501 t.Fatal(err) 502 } 503 504 if err := addedFile.ApplyFile(p); err != nil { 505 t.Fatal(err) 506 } 507 508 if err := ls.(*layerStore).driver.Put(layer1.(*referencedCacheLayer).cacheID); err != nil { 509 t.Fatal(err) 510 } 511 512 layer2, err := ls.Register(bytes.NewReader(tar2), layer1.ChainID()) 513 if err != nil { 514 t.Fatal(err) 515 } 516 517 id1 := layer1.ChainID() 518 t.Logf("Layer 1: %s", layer1.ChainID()) 519 t.Logf("Layer 2: %s", layer2.ChainID()) 520 521 if _, err := ls.Release(layer1); err != nil { 522 t.Fatal(err) 523 } 524 525 assertLayerDiff(t, tar2, layer2) 526 527 layer1b, err := ls.Get(id1) 528 if err != nil { 529 t.Logf("Content of layer map: %#v", ls.(*layerStore).layerMap) 530 t.Fatal(err) 531 } 532 533 if _, err := ls.Release(layer2); err != nil { 534 t.Fatal(err) 535 } 536 537 assertLayerDiff(t, tar1, layer1b) 538 539 if _, err := ls.Release(layer1b); err != nil { 540 t.Fatal(err) 541 } 542 } 543 544 func assertLayerDiff(t *testing.T, expected []byte, layer Layer) { 545 expectedDigest := digest.FromBytes(expected) 546 547 if digest.Digest(layer.DiffID()) != expectedDigest { 548 t.Fatalf("Mismatched diff id for %s, got %s, expected %s", layer.ChainID(), layer.DiffID(), expected) 549 } 550 551 ts, err := layer.TarStream() 552 if err != nil { 553 t.Fatal(err) 554 } 555 defer ts.Close() 556 557 actual, err := io.ReadAll(ts) 558 if err != nil { 559 t.Fatal(err) 560 } 561 562 if len(actual) != len(expected) { 563 logByteDiff(t, actual, expected) 564 t.Fatalf("Mismatched tar stream size for %s, got %d, expected %d", layer.ChainID(), len(actual), len(expected)) 565 } 566 567 actualDigest := digest.FromBytes(actual) 568 569 if actualDigest != expectedDigest { 570 logByteDiff(t, actual, expected) 571 t.Fatalf("Wrong digest of tar stream, got %s, expected %s", actualDigest, expectedDigest) 572 } 573 } 574 575 const maxByteLog = 4 * 1024 576 577 func logByteDiff(t *testing.T, actual, expected []byte) { 578 d1, d2 := byteDiff(actual, expected) 579 if len(d1) == 0 && len(d2) == 0 { 580 return 581 } 582 583 prefix := len(actual) - len(d1) 584 if len(d1) > maxByteLog || len(d2) > maxByteLog { 585 t.Logf("Byte diff after %d matching bytes", prefix) 586 } else { 587 t.Logf("Byte diff after %d matching bytes\nActual bytes after prefix:\n%x\nExpected bytes after prefix:\n%x", prefix, d1, d2) 588 } 589 } 590 591 // byteDiff returns the differing bytes after the matching prefix 592 func byteDiff(b1, b2 []byte) ([]byte, []byte) { 593 i := 0 594 for i < len(b1) && i < len(b2) { 595 if b1[i] != b2[i] { 596 break 597 } 598 i++ 599 } 600 601 return b1[i:], b2[i:] 602 } 603 604 func tarFromFiles(files ...FileApplier) ([]byte, error) { 605 td, err := os.MkdirTemp("", "tar-") 606 if err != nil { 607 return nil, err 608 } 609 defer os.RemoveAll(td) 610 611 for _, f := range files { 612 if err := f.ApplyFile(containerfs.NewLocalContainerFS(td)); err != nil { 613 return nil, err 614 } 615 } 616 617 r, err := archive.Tar(td, archive.Uncompressed) 618 if err != nil { 619 return nil, err 620 } 621 622 buf := bytes.NewBuffer(nil) 623 if _, err := io.Copy(buf, r); err != nil { 624 return nil, err 625 } 626 627 return buf.Bytes(), nil 628 } 629 630 // assertReferences asserts that all the references are to the same 631 // image and represent the full set of references to that image. 632 func assertReferences(t *testing.T, references ...Layer) { 633 if len(references) == 0 { 634 return 635 } 636 base := references[0].(*referencedCacheLayer).roLayer 637 seenReferences := map[Layer]struct{}{ 638 references[0]: {}, 639 } 640 for i := 1; i < len(references); i++ { 641 other := references[i].(*referencedCacheLayer).roLayer 642 if base != other { 643 t.Fatalf("Unexpected referenced cache layer %s, expecting %s", other.ChainID(), base.ChainID()) 644 } 645 if _, ok := base.references[references[i]]; !ok { 646 t.Fatalf("Reference not part of reference list: %v", references[i]) 647 } 648 if _, ok := seenReferences[references[i]]; ok { 649 t.Fatalf("Duplicated reference %v", references[i]) 650 } 651 } 652 if rc := len(base.references); rc != len(references) { 653 t.Fatalf("Unexpected number of references %d, expecting %d", rc, len(references)) 654 } 655 } 656 657 func TestRegisterExistingLayer(t *testing.T) { 658 ls, _, cleanup := newTestStore(t) 659 defer cleanup() 660 661 baseFiles := []FileApplier{ 662 newTestFile("/etc/profile", []byte("# Base configuration"), 0644), 663 } 664 665 layerFiles := []FileApplier{ 666 newTestFile("/root/.bashrc", []byte("# Root configuration"), 0644), 667 } 668 669 li := initWithFiles(baseFiles...) 670 layer1, err := createLayer(ls, "", li) 671 if err != nil { 672 t.Fatal(err) 673 } 674 675 tar1, err := tarFromFiles(layerFiles...) 676 if err != nil { 677 t.Fatal(err) 678 } 679 680 layer2a, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) 681 if err != nil { 682 t.Fatal(err) 683 } 684 685 layer2b, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) 686 if err != nil { 687 t.Fatal(err) 688 } 689 690 assertReferences(t, layer2a, layer2b) 691 } 692 693 func TestTarStreamVerification(t *testing.T) { 694 // TODO Windows: Figure out why this is failing 695 if runtime.GOOS == "windows" { 696 t.Skip("Failing on Windows") 697 } 698 ls, tmpdir, cleanup := newTestStore(t) 699 defer cleanup() 700 701 files1 := []FileApplier{ 702 newTestFile("/foo", []byte("abc"), 0644), 703 newTestFile("/bar", []byte("def"), 0644), 704 } 705 files2 := []FileApplier{ 706 newTestFile("/foo", []byte("abc"), 0644), 707 newTestFile("/bar", []byte("def"), 0600), // different perm 708 } 709 710 tar1, err := tarFromFiles(files1...) 711 if err != nil { 712 t.Fatal(err) 713 } 714 715 tar2, err := tarFromFiles(files2...) 716 if err != nil { 717 t.Fatal(err) 718 } 719 720 layer1, err := ls.Register(bytes.NewReader(tar1), "") 721 if err != nil { 722 t.Fatal(err) 723 } 724 725 layer2, err := ls.Register(bytes.NewReader(tar2), "") 726 if err != nil { 727 t.Fatal(err) 728 } 729 id1 := digest.Digest(layer1.ChainID()) 730 id2 := digest.Digest(layer2.ChainID()) 731 732 // Replace tar data files 733 src, err := os.Open(filepath.Join(tmpdir, id1.Algorithm().String(), id1.Hex(), "tar-split.json.gz")) 734 if err != nil { 735 t.Fatal(err) 736 } 737 defer src.Close() 738 739 dst, err := os.Create(filepath.Join(tmpdir, id2.Algorithm().String(), id2.Hex(), "tar-split.json.gz")) 740 if err != nil { 741 t.Fatal(err) 742 } 743 defer dst.Close() 744 745 if _, err := io.Copy(dst, src); err != nil { 746 t.Fatal(err) 747 } 748 749 src.Sync() 750 dst.Sync() 751 752 ts, err := layer2.TarStream() 753 if err != nil { 754 t.Fatal(err) 755 } 756 _, err = io.Copy(io.Discard, ts) 757 if err == nil { 758 t.Fatal("expected data verification to fail") 759 } 760 if !strings.Contains(err.Error(), "could not verify layer data") { 761 t.Fatalf("wrong error returned from tarstream: %q", err) 762 } 763 }